// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable enable

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;

namespace XliffTasks.Model
{
    /// <summary>
    /// A <see cref="TranslatableDocument"/> for files in CPS rule .xaml format
    /// See https://msdn.microsoft.com/en-us/library/ekyft91f(v=vs.100).aspx
    /// </summary>
    internal sealed class XamlRuleDocument : TranslatableXmlDocument
    {
        private const string XliffTasksNs = "https://github.com/dotnet/xliff-tasks";
        private const string LocalizedPropertiesAttributeName = "LocalizedProperties";

        private static readonly char[] s_attrValueTrimChars = [ ':', ' ', '\t' ];
        
        protected override IEnumerable<TranslatableNode> GetTranslatableNodes()
        {
            foreach (XElement? element in Document.Descendants())
            {
                // first, let's check if the element has a descendent by the name of {elementLocalName.Description/DisplayName}
                var descendentDisplayName = element.Elements(XName.Get($"{element.Name.LocalName}.DisplayName", element.Name.NamespaceName)).FirstOrDefault();
                var descendentDescription = element.Elements(XName.Get($"{element.Name.LocalName}.Description", element.Name.NamespaceName)).FirstOrDefault();

                if (descendentDisplayName is not null)
                {
                    yield return new TranslatableXmlElement(
                        id: GenerateIdForDisplayNameOrDescription(descendentDisplayName),
                        source: descendentDisplayName.Value,
                        note: GetComment(descendentDisplayName, XmlName(descendentDisplayName)),
                        element: descendentDisplayName
                    );
                }
                
                if (descendentDescription is not null)
                {
                    yield return new TranslatableXmlElement(
                        id: GenerateIdForDisplayNameOrDescription(descendentDescription),
                        source: descendentDescription.Value,
                        note: GetComment(descendentDescription, XmlName(descendentDescription)),
                        element: descendentDescription
                    );
                }
                
                var localizableProperties = element.Attribute(XName.Get(LocalizedPropertiesAttributeName, XliffTasksNs))?.Value?.Split(';');

                if (localizableProperties is not null)
                {
                    // we could have any number of descendent localizable properties
                    foreach (var localizableProperty in localizableProperties)
                    {
                        if (element.Elements(XName.Get($"{element.Name.LocalName}.{localizableProperty}", element.Name.NamespaceName)).FirstOrDefault() is { } descendentValue)
                        {
                            yield return new TranslatableXmlElement(
                                id: GenerateIdForPropertyMetadata(descendentValue),
                                source: descendentValue.Value,
                                note: GetComment(descendentValue, localizableProperty),
                                element: descendentValue);
                        }
                    }
                }

                foreach (XAttribute? attribute in element.Attributes())
                {
                    if ((descendentDisplayName is null && XmlName(attribute) == "DisplayName")
                        || (descendentDescription is null && XmlName(attribute) == "Description"))
                    {
                        yield return new TranslatableXmlAttribute(
                            id: GenerateIdForDisplayNameOrDescription(attribute),
                            source: attribute.Value,
                            note: GetComment(element, XmlName(attribute)),
                            attribute: attribute);
                    }
                    else if (AttributedName(element) == "SearchTerms" && (XmlName(attribute) == "Value" || element.Elements(XName.Get($"{element.Name.LocalName}.Value", element.Name.NamespaceName)).FirstOrDefault() is { }))
                    {
                        if (XmlName(attribute) == "Value")
                        {
                            yield return new TranslatableXmlAttribute(
                                id: GenerateIdForPropertyMetadata(element),
                                source: attribute.Value,
                                note: GetComment(element, XmlName(attribute)),
                                attribute: attribute);
                        }
                        // else if we have a descendent in the form of {elementLocalName}.Value, we should translate that descendent
                        else if (element.Elements(XName.Get($"{element.Name.LocalName}.Value", element.Name.NamespaceName)).FirstOrDefault() is { } descendentValue)
                        {
                            yield return new TranslatableXmlElement(
                                id: GenerateIdForPropertyMetadata(element),
                                source: descendentValue.Value,
                                note: GetComment(descendentValue, XmlName(attribute)),
                                element: descendentValue);
                        }
                    }
                    else
                    {
                        if (localizableProperties is null)
                        {
                            continue;
                        }
                        
                        // if the property value is directly specified as an attribute
                        if (localizableProperties.Contains(attribute.Name.LocalName))
                        {
                            yield return new TranslatableXmlAttribute(
                                id: GenerateIdForPropertyMetadata(element, attribute),
                                source: attribute.Value,
                                note: GetComment(element, XmlName(attribute)),
                                attribute: attribute);
                        }
                    }
                }
            }
        }

        private static string GenerateIdForDisplayNameOrDescription(XObject xObject)
        {
            var parent = xObject.Parent;
            if (parent is null)
            {
                throw new ArgumentException("Attribute must have a parent element", nameof(xObject));
            }

            if (XmlName(parent) == "EnumValue")
            {
                var grandparent = parent.Parent;
                if (grandparent is null)
                {
                    throw new ArgumentException("Attribute must have a grandparent element", nameof(xObject));
                }
                
                return $"{XmlName(parent)}|{AttributedName(grandparent)}.{AttributedName(parent)}|{XmlName(xObject)}";
            }

            return $"{XmlName(parent)}|{AttributedName(parent)}|{XmlName(xObject)}";
        }

        private static string GenerateIdForPropertyMetadata(XElement element, XAttribute? attribute = null)
        {
            var ancestorWithNameAttributeCandidate = element.Parent?.Parent; // start at grandparent
            var idBuilder = new StringBuilder();

            // if has no grandparent, we'll try parent
            if (ancestorWithNameAttributeCandidate is null)
            {
                if (element.Parent is not null)
                {
                    idBuilder.Append(element.Parent.Attribute("Name") is null ? XmlName(element.Parent) : $"{XmlName(element.Parent)}|{AttributedName(element.Parent)}");
                    idBuilder.Append("|Metadata|");
                }

                idBuilder.Append(XmlName(element));
                if (element.Attribute("Name") is null)
                {
                    idBuilder.Append($"|{AttributedName(element)}");
                }
                
                if (attribute is not null)
                {
                    idBuilder.Append($"|{XmlName(attribute)}");
                }

                return idBuilder.ToString();
            }

            // while the current ancestor has a parent and does not have a name, append its XmlName to the id and go up a level  
            while (ancestorWithNameAttributeCandidate?.Attribute("Name") is null && ancestorWithNameAttributeCandidate?.Parent is not null) {
            {
                idBuilder.Insert(0, $"{XmlName(ancestorWithNameAttributeCandidate)}|");
                ancestorWithNameAttributeCandidate = ancestorWithNameAttributeCandidate.Parent;
            }}

            idBuilder.Insert(0, $"{XmlName(ancestorWithNameAttributeCandidate!)}|{AttributedName(ancestorWithNameAttributeCandidate!)}|");

            idBuilder.Append($"Metadata|");
            idBuilder.Append(element.Attribute("Name") is not null ? AttributedName(element) : XmlName(element));

            if (attribute is not null)
            {
                idBuilder.Append($"|{attribute.Name.LocalName}");
            }


            return idBuilder.ToString();
        }

        private static string? GetComment(XElement element, string attributeName)
        {
            foreach (XComment comment in element.Nodes().OfType<XComment>())
            {
                foreach (string? line in comment.Value.Split(new[] { '\n' }, System.StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()))
                {
                    if (line.StartsWith(attributeName))
                    {
                        return line.Substring(attributeName.Length).Trim(s_attrValueTrimChars);
                    }
                }
            }

            return null;
        }

        private static string XmlName(XObject container) => container is XElement element ? XmlName(element) : XmlName((XAttribute)container);
        private static string XmlName(XElement element)
        {
            var localName = element.Name.LocalName;
            // if we have a descendent element, we should only take the last part of the name after the dot
            return localName.Contains('.') ? localName.Split('.').Last() : localName;
        }

        private static string XmlName(XAttribute attribute) => attribute.Name.LocalName;

        private static string? AttributedName(XElement element) => element.Attribute("Name")?.Value;
    }
}
