using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using Siemens.Engineering;
using Siemens.Engineering.HW;
using Siemens.Engineering.HW.Features;
using Siemens.Engineering.SW;
using Siemens.Engineering.SW.Blocks;
using Siemens.Engineering.SW.Tags;

namespace SiemensTagOku
{
    class TagServices
    {
        private readonly TiaPortal portal;
        private Project project;

        public TagServices(TiaPortal portal)
        {
            this.portal = portal;
        }

        public void OpenProject(string projectPath)
        {
            try { project?.Close(); } catch { }
            project = portal.Projects.Open(new FileInfo(projectPath));
        }

        public void CloseProject()
        {
            try { project?.Close(); } catch { }
            project = null;
        }

        public List<TagSourceInfo> ReadAllTagSources()
        {
            var sources = new List<TagSourceInfo>();

            foreach (var device in project.Devices)
            {
                string deviceName = device.Name;

                foreach (DeviceItem item in device.DeviceItems)
                {
                    try
                    {
                        var swContainer = item.GetService<SoftwareContainer>();
                        if (swContainer?.Software is PlcSoftware plc)
                        {
                            ReadTagTables(plc, sources, deviceName);
                            ReadDataBlocks(plc, sources, deviceName);
                        }
                    }
                    catch { }
                }
            }

            foreach (var group in project.DeviceGroups)
                CollectFromGroup(group, sources);

            return sources;
        }

        private void CollectFromGroup(DeviceUserGroup group, List<TagSourceInfo> sources)
        {
            foreach (var subGroup in group.Groups)
                CollectFromGroup(subGroup, sources);

            foreach (var device in group.Devices)
            {
                string deviceName = device.Name;
                foreach (DeviceItem item in device.DeviceItems)
                {
                    try
                    {
                        var swContainer = item.GetService<SoftwareContainer>();
                        if (swContainer?.Software is PlcSoftware plc)
                        {
                            ReadTagTables(plc, sources, deviceName);
                            ReadDataBlocks(plc, sources, deviceName);
                        }
                    }
                    catch { }
                }
            }
        }

        private void ReadTagTables(PlcSoftware plc, List<TagSourceInfo> sources, string deviceName)
        {
            foreach (PlcTagTable tagTable in plc.TagTableGroup.TagTables)
            {
                var tags = new List<TagInfo>();
                foreach (PlcTag tag in tagTable.Tags)
                {
                    tags.Add(new TagInfo
                    {
                        DeviceName = deviceName,
                        TableName = tagTable.Name,
                        Category = "TagTable",
                        Name = tag.Name,
                        DataType = SafeGet(() => tag.DataTypeName, ""),
                        LogicalAddress = SafeGet(() => tag.LogicalAddress, ""),
                        Comment = GetCommentText(tag.Comment)
                    });
                }

                sources.Add(new TagSourceInfo
                {
                    DeviceName = deviceName,
                    SourceName = tagTable.Name,
                    Category = "TagTable",
                    Tags = tags
                });
            }
        }

        private void ReadDataBlocks(PlcSoftware plc, List<TagSourceInfo> sources, string deviceName)
        {
            string tempDir = Path.Combine(Path.GetTempPath(), "SiemensTagOku_Export");
            Directory.CreateDirectory(tempDir);

            try
            {
                foreach (PlcBlock block in plc.BlockGroup.Blocks)
                {
                    if (block is GlobalDB globalDb)
                    {
                        int dbNumber = globalDb.Number;
                        var tags = new List<TagInfo>();

                        try
                        {
                            string xmlPath = Path.Combine(tempDir, $"DB{dbNumber}.xml");
                            globalDb.Export(new FileInfo(xmlPath), ExportOptions.None);
                            tags = ParseExportedDbXml(xmlPath, deviceName, globalDb.Name, dbNumber);
                        }
                        catch { }

                        sources.Add(new TagSourceInfo
                        {
                            DeviceName = deviceName,
                            SourceName = $"{globalDb.Name} (DB{dbNumber})",
                            Category = "DB",
                            Tags = tags
                        });
                    }
                }
            }
            finally
            {
                try { Directory.Delete(tempDir, true); } catch { }
            }
        }

        private List<TagInfo> ParseExportedDbXml(string xmlPath, string deviceName, string dbName, int dbNumber)
        {
            var result = new List<TagInfo>();
            var doc = new XmlDocument();
            doc.Load(xmlPath);

            var memberNodes = doc.SelectNodes("//*[local-name()='Member']");

            if (memberNodes == null)
                return result;

            string memoryLayout = doc.SelectSingleNode("//MemoryLayout")?.InnerText ?? "Standard";
            int currentOffset = 0;

            foreach (XmlNode memberNode in memberNodes)
            {
                string name = memberNode.Attributes?["Name"]?.Value ?? "";
                string dataType = memberNode.Attributes?["Datatype"]?.Value ?? "";
                bool isRetain = memberNode.Attributes?["Remanence"]?.Value == "Retain";

                int calculatedOffset = CalculateOffset(ref currentOffset, dataType, memoryLayout);

                string address = memoryLayout == "Optimized"
                    ? $"DB{dbNumber}.%DB<{dataType}>\"{name}\""
                    : $"DB{dbNumber}.DBX{calculatedOffset}";

                result.Add(new TagInfo
                {
                    DeviceName = deviceName,
                    TableName = $"{dbName} (DB{dbNumber})",
                    Category = "DB",
                    Name = name,
                    DataType = dataType,
                    LogicalAddress = address,
                    Comment = isRetain ? "Retain" : ""
                });
            }

            return result;
        }

        private static readonly Dictionary<string, int> TypeSizes = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
        {
            { "Bool", 1 }, { "Byte", 1 }, { "SInt", 1 }, { "USInt", 1 },
            { "Word", 2 }, { "Int", 2 }, { "UInt", 2 },
            { "DWord", 4 }, { "DInt", 4 }, { "UDInt", 4 }, { "Real", 4 }, { "Time", 4 },
            { "LWord", 8 }, { "LInt", 8 }, { "ULInt", 8 }, { "LReal", 8 },
            { "String", 256 }, { "Char", 1 },
            { "IEC_TIMER", 16 }, { "IEC_TCONTROLLER", 20 }, { "IEC_SF_B", 4 },
            { "IEC_HR_TIME", 4 }, { "IEC_LTIME", 8 },
        };

        private static int CalculateOffset(ref int currentOffset, string dataType, string memoryLayout)
        {
            if (memoryLayout == "Optimized")
                return 0;

            if (!TypeSizes.TryGetValue(dataType, out int size))
                size = 2;

            int alignment = size > 4 ? 4 : (size == 2 ? 2 : 1);
            if (currentOffset % alignment != 0)
                currentOffset += alignment - (currentOffset % alignment);

            int offset = currentOffset;
            currentOffset += size;
            return offset;
        }

        private static string GetCommentText(MultilingualText mlText)
        {
            if (mlText == null) return "";
            try
            {
                foreach (MultilingualTextItem item in mlText.Items)
                {
                    if (!string.IsNullOrEmpty(item.Text))
                        return item.Text;
                }
            }
            catch { }
            return "";
        }

        public List<BlockGroupInfo> ReadAllCodeBlocks()
        {
            var groups = new List<BlockGroupInfo>();

            foreach (var device in project.Devices)
                CollectCodeBlocks(device, groups);

            foreach (var group in project.DeviceGroups)
                CollectCodeBlocksFromGroup(group, groups);

            return groups;
        }

        private void CollectCodeBlocks(Device device, List<BlockGroupInfo> groups)
        {
            string deviceName = device.Name;
            foreach (DeviceItem item in device.DeviceItems)
            {
                try
                {
                    var swContainer = item.GetService<SoftwareContainer>();
                    if (swContainer?.Software is PlcSoftware plc)
                    {
                        foreach (PlcBlock block in plc.BlockGroup.Blocks)
                        {
                            string blockType = GetBlockType(block);
                            if (blockType == null) continue;

                            var grp = groups.FirstOrDefault(g =>
                                g.DeviceName == deviceName && g.BlockType == blockType);
                            if (grp == null)
                            {
                                grp = new BlockGroupInfo
                                {
                                    DeviceName = deviceName,
                                    GroupName = $"{blockType} Blokları",
                                    BlockType = blockType
                                };
                                groups.Add(grp);
                            }

                            grp.Blocks.Add(new BlockInfo
                            {
                                DeviceName = deviceName,
                                Name = block.Name,
                                Number = SafeGet(() => block.Number, 0),
                                BlockType = blockType,
                                ProgrammingLanguage = GetProgrammingLanguage(block),
                                Title = GetBlockStringAttribute(block, "Title"),
                                Comment = GetBlockComment(block),
                                BlockRef = block
                            });
                        }
                    }
                }
                catch { }
            }
        }

        private void CollectCodeBlocksFromGroup(DeviceUserGroup group, List<BlockGroupInfo> groups)
        {
            foreach (var subGroup in group.Groups)
                CollectCodeBlocksFromGroup(subGroup, groups);

            foreach (var device in group.Devices)
                CollectCodeBlocks(device, groups);
        }

        private static string GetBlockType(PlcBlock block)
        {
            if (block is GlobalDB || block is InstanceDB || block is ArrayDB) return null;
            if (block is OB) return "OB";
            if (block is FB) return "FB";
            if (block is FC) return "FC";
            return null;
        }

        private static string GetBlockStringAttribute(PlcBlock block, string attributeName)
        {
            try
            {
                var prop = block.GetType().GetProperty(attributeName);
                if (prop != null)
                {
                    var val = prop.GetValue(block);
                    if (val != null) return val.ToString();
                }
            }
            catch { }
            return "";
        }

        private static string GetBlockComment(PlcBlock block)
        {
            try
            {
                var prop = block.GetType().GetProperty("Comment");
                if (prop != null)
                {
                    var val = prop.GetValue(block);
                    if (val is MultilingualText mlText)
                        return GetCommentText(mlText);
                    if (val != null) return val.ToString();
                }
            }
            catch { }
            return "";
        }

        private static string GetProgrammingLanguage(PlcBlock block)
        {
            return GetBlockStringAttribute(block, "ProgrammingLanguage");
        }

        public void ExportBlocksToXml(IEnumerable<BlockInfo> blocksToExport, string folder)
        {
            foreach (var target in blocksToExport)
            {
                if (target.BlockRef != null)
                {
                    string fileName = $"{target.BlockType}{target.Number}_{SanitizeFileName(target.Name)}.xml";
                    string filePath = Path.Combine(folder, fileName);
                    target.BlockRef.Export(new FileInfo(filePath), ExportOptions.None);
                }
            }
        }

        private static string SanitizeFileName(string name)
        {
            foreach (char c in Path.GetInvalidFileNameChars())
                name = name.Replace(c, '_');
            return name;
        }

        private T SafeGet<T>(Func<T> func, T defaultValue)
        {
            try { return func(); }
            catch { return defaultValue; }
        }

        public void Dispose()
        {
            try { project?.Close(); } catch { }
        }
    }

    class TiaPortalHost : IDisposable
    {
        private readonly BlockingCollection<Action> queue = new BlockingCollection<Action>();
        private Thread workerThread;
        private TagServices services;
        private readonly TaskCompletionSource<object> readySignal = new TaskCompletionSource<object>();

        public Task StartAsync()
        {
            workerThread = new Thread(() =>
            {
                try
                {
                    TiaPortal portal;
                    try
                    {
                        portal = new TiaPortal(TiaPortalMode.WithoutUserInterface);
                    }
                    catch (Exception)
                    {
                        portal = new TiaPortal(TiaPortalMode.WithUserInterface);
                    }
                    services = new TagServices(portal);
                    readySignal.TrySetResult(null);
                }
                catch (Exception ex)
                {
                    readySignal.TrySetException(ex);
                    return;
                }

                foreach (var action in queue.GetConsumingEnumerable())
                    action();

                services?.Dispose();
            });
            workerThread.SetApartmentState(ApartmentState.STA);
            workerThread.IsBackground = true;
            workerThread.Start();
            return readySignal.Task;
        }

        public async Task<T> Enqueue<T>(Func<TagServices, T> func)
        {
            await readySignal.Task;
            var tcs = new TaskCompletionSource<T>();
            queue.Add(() =>
            {
                try { tcs.TrySetResult(func(services)); }
                catch (Exception ex) { tcs.TrySetException(ex); }
            });
            return await tcs.Task;
        }

        public async Task Enqueue(Action<TagServices> action)
        {
            await readySignal.Task;
            var tcs = new TaskCompletionSource<object>();
            queue.Add(() =>
            {
                try { action(services); tcs.TrySetResult(null); }
                catch (Exception ex) { tcs.TrySetException(ex); }
            });
            await tcs.Task;
        }

        public void Dispose()
        {
            queue.CompleteAdding();
            workerThread?.Join(TimeSpan.FromSeconds(10));
        }
    }

    class TagSourceInfo
    {
        public string DeviceName { get; set; }
        public string SourceName { get; set; }
        public string Category { get; set; }
        public List<TagInfo> Tags { get; set; } = new List<TagInfo>();
        public int TagCount => Tags?.Count ?? 0;
    }

    class TagInfo
    {
        public string DeviceName { get; set; }
        public string TableName { get; set; }
        public string Category { get; set; }
        public string Name { get; set; }
        public string DataType { get; set; }
        public string LogicalAddress { get; set; }
        public string Comment { get; set; }
    }

    class BlockInfo
    {
        public string DeviceName { get; set; }
        public string Name { get; set; }
        public int Number { get; set; }
        public string BlockType { get; set; }
        public string ProgrammingLanguage { get; set; }
        public string Title { get; set; }
        public string Comment { get; set; }
        public PlcBlock BlockRef { get; set; }
    }

    class BlockGroupInfo
    {
        public string DeviceName { get; set; }
        public string GroupName { get; set; }
        public string BlockType { get; set; }
        public List<BlockInfo> Blocks { get; set; } = new List<BlockInfo>();
        public int BlockCount => Blocks?.Count ?? 0;
    }
}
