using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using GalaxyBudsClient.Generators.Utils;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;
using System.Threading;

namespace GalaxyBudsClient.Generators.Localization;

[Generator]
public class LocalizationKeySourceGenerator : IIncrementalGenerator
{
    private readonly struct Language(string langCode, string langName)
    {
        public string LangCode { get; } = langCode;
        public string LangName { get; } = langName;
    }

    private readonly struct LocalizationFile
    {
        public LocalizationFile(string langCode, string langName, string dictionarySource, string? keysSource, string? stringsSource)
        {
            LangCode = langCode;
            LangName = langName;
            DictionarySource = dictionarySource;
            KeysSource = keysSource;
            StringsSource = stringsSource;
        }

        public string LangCode { get; }
        public string LangName { get; }
        public string DictionarySource { get; }
        public string? KeysSource { get; }
        public string? StringsSource { get; }
    }
    
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        var axamlFiles = context.AdditionalTextsProvider
            .Where(static file => file.Path.EndsWith(".axaml", StringComparison.OrdinalIgnoreCase) &&
                                  file.Path.Contains("i18n", StringComparison.OrdinalIgnoreCase))
            .Select(static (file, ct) => ParseFile(file, ct));

        context.RegisterSourceOutput(axamlFiles,
            static (spc, file) =>
            {
                spc.AddSource("LocalizationDictionary_" + file.LangCode + ".g.cs", SourceText.From(file.DictionarySource, Encoding.UTF8));
                
                if (file.KeysSource != null)
                {
                    spc.AddSource("LocalizationKeys.g.cs", SourceText.From(file.KeysSource, Encoding.UTF8));
                }
                
                if (file.StringsSource != null)
                {
                    spc.AddSource("LocalizationStrings.g.cs", SourceText.From(file.StringsSource, Encoding.UTF8));
                }
            });

        context.RegisterSourceOutput(axamlFiles.Collect(),
            static (spc, files) => ExecuteLookup(spc, files));
    }

    private static LocalizationFile ParseFile(AdditionalText additionalText, CancellationToken ct)
    {
        ct.ThrowIfCancellationRequested();
        
        var langCode = Path.GetFileNameWithoutExtension(additionalText.Path);
        var isEnglish = additionalText.Path.EndsWith("en.axaml", StringComparison.OrdinalIgnoreCase);

        var keyClassMembers = new List<string>();
        var stringClassMembers = new List<string>();
        string? englishLangName = null;
        
        var dictionarySource = new CodeGenerator();
        dictionarySource.AppendLines("""
                                     // <auto-generated/>
                                     namespace GalaxyBudsClient.Generated.I18N;
                                     """);
        dictionarySource.EnterScope("internal static partial class LocalizationDictionaries");
        dictionarySource.EnterScope($"private static global::System.Collections.Generic.Dictionary<global::System.String, global::System.String> @{langCode} => new()");

        var xaml = additionalText.GetText(ct);
        if (xaml is not null)
        {
            var doc = XDocument.Parse(xaml.ToString());
            var nodes = doc.Root?.Nodes();

            if (nodes is not null)
            {
                foreach (var node in nodes)
                {
                    if (node is not XElement element)
                        continue;

                    // Example for xmlNamespace: clr-namespace:System;assembly=mscorlib
                    var xmlNamespace = element.Name.NamespaceName.Split(';');
                    var namespaceName = xmlNamespace.First().Split(':').Last();
                    var assemblyName = xmlNamespace.Last().Split('=').Last();
                    var typeName = element.Name.LocalName;
                    var englishString = element.Value;

                    var key = element.Attributes().FirstOrDefault(x => x.Name.LocalName == "Key");
                    if (key == null)
                    {
                        keyClassMembers.Add($"#warning {additionalText.Path}: x:Key attribute not found for XAML element of type {namespaceName}.{typeName}");
                        continue;
                    }

                    // Generate the key/string class members based on the english translation
                    if (isEnglish)
                    {
                        // Snake case to Pascal case
                        var memberName = key.Value.Split(["_"], StringSplitOptions.RemoveEmptyEntries)
                            .Select(s => char.ToUpperInvariant(s[0]) + s.Substring(1, s.Length - 1))
                            .Aggregate(string.Empty, (s1, s2) => s1 + s2);

                        keyClassMembers.Add($"/** <summary> Resolves to: '{englishString}' </summary> */");
                        keyClassMembers.Add($"public const global::System.String {memberName} = \"{key.Value}\";");
                        stringClassMembers.Add($"/** <summary> Resolves to: '{englishString}' </summary> */");
                        stringClassMembers.Add($"public static global::System.String {memberName} => Loc.Resolve(Keys.{memberName});");

                        if (englishLangName == null && memberName == "language_name_en")
                            englishLangName = englishString;
                    }

                    var sanitized = element.Value
                        .Replace(@"\", @"\\")
                        .Replace("\"", "\\\"")
                        .Replace("\r\n", "\n")
                        .Replace('\r', '\n')
                        .Split('\n')
                        .Select(x => x.Trim());
                    dictionarySource.AppendLine($$"""{"{{key.Value}}", "{{string.Join("\n", sanitized).Replace("\n", "\\n")}}"},""");
                }
            }
        }

        dictionarySource.LeaveScope(";");
        dictionarySource.LeaveScope();

        string? keysSource = null;
        string? stringsSource = null;
        
        if (isEnglish)
        {
            var keysGen = new CodeGenerator();
            keysGen.AppendLines("""
                                 // <auto-generated/>
                                 namespace GalaxyBudsClient.Generated.I18N;
                                 """);
            keysGen.EnterScope("public static class Keys");
            keyClassMembers.ForEach(keysGen.AppendLine);
            keysGen.LeaveScope();

            var stringsGen = new CodeGenerator();
            stringsGen.AppendLines("""
                                    // <auto-generated/>
                                    using GalaxyBudsClient.Utils.Interface;

                                    namespace GalaxyBudsClient.Generated.I18N;
                                    """);
            stringsGen.EnterScope("public static class Strings");
            stringClassMembers.ForEach(stringsGen.AppendLine);
            stringsGen.LeaveScope();

            keysSource = keysGen.ToString();
            stringsSource = stringsGen.ToString();
        }

        return new LocalizationFile(
            langCode,
            englishLangName ?? langCode,
            dictionarySource.ToString(),
            keysSource,
            stringsSource);
    }

    private static void ExecuteLookup(SourceProductionContext context, ImmutableArray<LocalizationFile> files)
    {
        var languages = new List<Language>(files.Length);
        
        foreach (var file in files)
        {
            languages.Add(new Language(file.LangCode, file.LangName));
        }
        
        // Add language dict lookup function
        var lookupSource = new CodeGenerator();
        lookupSource.AppendLines("""
                                 // <auto-generated/>
                                 #nullable enable
                                 namespace GalaxyBudsClient.Generated.I18N;
                                 """);
        lookupSource.EnterScope("internal static partial class LocalizationDictionaries");
        lookupSource.EnterScope("public static global::System.Collections.Generic.Dictionary<global::System.String, global::System.String>? GetByLangCode(global::System.String langCode)");
        lookupSource.EnterScope("return langCode switch");
        foreach (var lang in languages)
        {
            lookupSource.AppendLine($"\"{lang.LangCode}\" => @{lang.LangCode},");
        }
        lookupSource.AppendLine("_ => null,");
        lookupSource.LeaveScope(";");
        lookupSource.LeaveScope();
        lookupSource.LeaveScope();
        
        context.AddSource($"LocalizationDictionaries.g.cs", SourceText.From(lookupSource.ToString(), Encoding.UTF8));
        
                
        // Add locales enum
        // FIXME: Roslyn doesn't support source generation chaining yet. This enum could not be read by the JSON source generator.
        /* var localesSource = new CodeGenerator();
        localesSource.AppendLines("""
                                  // <auto-generated/>
                                  #nullable enable
                                  namespace GalaxyBudsClient.Generated.I18N;

                                  """);
        localesSource.AppendLine("[GalaxyBudsClient.Generated.Model.Attributes.CompiledEnum]");
        localesSource.EnterScope("public enum Locales");
        foreach (var lang in languages)
        {
            localesSource.AppendLine($"[System.ComponentModel.DescriptionAttribute(\"{lang.LangName}\")]");
            localesSource.AppendLine($"@{lang.LangCode},");
        }
        localesSource.AppendLine($"[System.Runtime.Serialization.IgnoreDataMemberAttribute, System.ComponentModel.DescriptionAttribute(\"custom_language.xaml\")]");
        localesSource.AppendLine("@custom,");
        
        localesSource.LeaveScope();
        
        context.AddSource($"Locales.g.cs", localesSource.ToString()); */
    }
}
