using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

namespace CloakBrowser.Generators;

/// <summary>
/// Roslyn source generator that auto-implements the boilerplate "delegate everything
/// to the wrapped object" members for a decorator class.
///
/// Usage: annotate a <c>partial class</c> with
/// <c>[GenerateInterfaceDelegation(typeof(IPage))]</c> and provide a field/property
/// named <c>_inner</c> (or the name passed to the attribute) of the interface type.
/// The generator emits delegating implementations for every interface member the
/// class does NOT already declare itself. Members you declare manually (the
/// "intercepted" / humanized ones) are left untouched.
///
/// This lets us hand-write only the handful of methods we want to humanize while the
/// remaining ~hundreds of interface members are forwarded verbatim with full static
/// typing, correct async signatures, optional parameters, and no reflection.
/// </summary>
[Generator(Microsoft.CodeAnalysis.LanguageNames.CSharp)]
public sealed class InterfaceDelegationGenerator : IIncrementalGenerator
{
    private const string AttributeNamespace = "CloakBrowser.Wrappers";
    private const string AttributeName = "GenerateInterfaceDelegationAttribute";
    private const string AttributeFullName = AttributeNamespace + "." + AttributeName;

    private const string AttributeSource = @"// <auto-generated/>
#nullable enable
namespace CloakBrowser.Wrappers
{
    /// <summary>
    /// Marks a partial decorator class for automatic generation of delegating members
    /// for the given interface. The class must expose a backing member (default name
    /// <c>_inner</c>) of the interface type that calls are forwarded to.
    /// </summary>
    [System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
    internal sealed class GenerateInterfaceDelegationAttribute : System.Attribute
    {
        public GenerateInterfaceDelegationAttribute(System.Type interfaceType, string innerMemberName = ""_inner"")
        {
            InterfaceType = interfaceType;
            InnerMemberName = innerMemberName;
        }

        public System.Type InterfaceType { get; }
        public string InnerMemberName { get; }
    }
}
";

    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        // Emit the marker attribute into the consuming compilation.
        context.RegisterPostInitializationOutput(ctx =>
            ctx.AddSource("GenerateInterfaceDelegationAttribute.g.cs",
                SourceText.From(AttributeSource, Encoding.UTF8)));

        var candidates = context.SyntaxProvider
            .CreateSyntaxProvider(
                predicate: static (node, _) =>
                    node is ClassDeclarationSyntax c &&
                    c.AttributeLists.Count > 0 &&
                    c.Modifiers.Any(m => m.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.PartialKeyword)),
                transform: static (ctx, _) => (INamedTypeSymbol?)ctx.SemanticModel.GetDeclaredSymbol(ctx.Node))
            .Where(static s => s is not null)
            .Select(static (s, _) => s!);

        var compilationAndClasses = context.CompilationProvider.Combine(candidates.Collect());

        context.RegisterSourceOutput(compilationAndClasses, static (spc, source) =>
            Execute(spc, source.Left, source.Right));
    }

    private static void Execute(SourceProductionContext spc, Compilation compilation,
        System.Collections.Immutable.ImmutableArray<INamedTypeSymbol> classes)
    {
        var attrSymbol = compilation.GetTypeByMetadataName(AttributeFullName);
        if (attrSymbol is null)
            return;

        var seen = new HashSet<string>(StringComparer.Ordinal);
        foreach (var cls in classes.Distinct(SymbolEqualityComparer.Default).Cast<INamedTypeSymbol>())
        {
            var attrs = cls.GetAttributes()
                .Where(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attrSymbol))
                .ToList();
            if (attrs.Count == 0)
                continue;

            var sb = new StringBuilder();
            sb.AppendLine("// <auto-generated/>");
            sb.AppendLine("#nullable enable");
            sb.AppendLine("#pragma warning disable CS0108 // member hides inherited member");
            sb.AppendLine("#pragma warning disable CS0612 // member is obsolete (delegation is intentional)");
            sb.AppendLine("#pragma warning disable CS0618 // member is obsolete (delegation is intentional)");
            sb.AppendLine();

            string? ns = cls.ContainingNamespace.IsGlobalNamespace
                ? null
                : cls.ContainingNamespace.ToDisplayString();
            if (ns is not null)
            {
                sb.Append("namespace ").Append(ns).AppendLine();
                sb.AppendLine("{");
            }

            sb.Append("    partial class ").Append(cls.Name).AppendLine();
            sb.AppendLine("    {");

            // Collect members the class already declares (by signature) so we don't
            // re-emit (and conflict with) hand-written intercepted methods.
            var declared = BuildDeclaredSignatureSet(cls);

            foreach (var attr in attrs)
            {
                if (attr.ConstructorArguments.Length < 1)
                    continue;
                if (attr.ConstructorArguments[0].Value is not INamedTypeSymbol iface)
                    continue;
                string innerName = attr.ConstructorArguments.Length >= 2 &&
                                   attr.ConstructorArguments[1].Value is string s
                    ? s
                    : "_inner";

                EmitForInterface(sb, iface, innerName, declared);
            }

            sb.AppendLine("    }");
            if (ns is not null)
                sb.AppendLine("}");

            string hint = (ns is null ? "" : ns + ".") + cls.Name + ".Delegation.g.cs";
            // De-dup hint names across partials.
            if (!seen.Add(hint))
                hint = hint + "." + Guid.NewGuid().ToString("N").Substring(0, 6);
            spc.AddSource(hint, SourceText.From(sb.ToString(), Encoding.UTF8));
        }
    }

    private static HashSet<string> BuildDeclaredSignatureSet(INamedTypeSymbol cls)
    {
        var set = new HashSet<string>(StringComparer.Ordinal);
        foreach (var m in cls.GetMembers())
        {
            switch (m)
            {
                case IMethodSymbol method when method.MethodKind == MethodKind.Ordinary:
                    set.Add(MethodSignature(method));
                    break;
                case IPropertySymbol prop when prop.IsIndexer:
                    set.Add(IndexerSignature(prop));
                    break;
                case IPropertySymbol prop:
                    set.Add("prop:" + prop.Name);
                    break;
                case IEventSymbol evt:
                    set.Add("event:" + evt.Name);
                    break;
            }
        }
        return set;
    }

    private static void EmitForInterface(StringBuilder sb, INamedTypeSymbol iface,
        string innerName, HashSet<string> declared)
    {
        // Walk the interface and every interface it inherits.
        var allIfaces = new List<INamedTypeSymbol> { iface };
        allIfaces.AddRange(iface.AllInterfaces);

        var emitted = new HashSet<string>(StringComparer.Ordinal);

        foreach (var i in allIfaces)
        {
            foreach (var member in i.GetMembers())
            {
                switch (member)
                {
                    case IMethodSymbol method when method.MethodKind == MethodKind.Ordinary:
                    {
                        string sig = MethodSignature(method);
                        if (declared.Contains(sig) || !emitted.Add(sig))
                            continue;
                        EmitMethod(sb, method, innerName);
                        break;
                    }
                    case IPropertySymbol prop when prop.IsIndexer:
                    {
                        string sig = IndexerSignature(prop);
                        if (declared.Contains(sig) || !emitted.Add(sig))
                            continue;
                        EmitIndexer(sb, prop, innerName);
                        break;
                    }
                    case IPropertySymbol prop:
                    {
                        string key = "prop:" + prop.Name;
                        if (declared.Contains(key) || !emitted.Add(key))
                            continue;
                        EmitProperty(sb, prop, innerName);
                        break;
                    }
                    case IEventSymbol evt:
                    {
                        string key = "event:" + evt.Name;
                        if (declared.Contains(key) || !emitted.Add(key))
                            continue;
                        EmitEvent(sb, evt, innerName);
                        break;
                    }
                }
            }
        }
    }

    // -----------------------------------------------------------------------
    // Emitters
    // -----------------------------------------------------------------------

    // Marker on every generated (delegating) member, so consumers/tests can tell a
    // generator-delegated member apart from a hand-written (intercepted) one.
    private const string GenAttr =
        "        [global::System.CodeDom.Compiler.GeneratedCode(\"CloakBrowser.Generators\", \"1.0\")]";

    private static void EmitMethod(StringBuilder sb, IMethodSymbol method, string innerName)
    {
        string ret = method.ReturnType.ToDisplayString(FullyQualified);
        string retKeyword = method.ReturnsVoid ? "void" : ret;

        var pars = new List<string>();
        var args = new List<string>();
        foreach (var p in method.Parameters)
            BuildParam(p, pars, args);

        string typeParams = method.IsGenericMethod
            ? "<" + string.Join(", ", method.TypeParameters.Select(t => t.Name)) + ">"
            : "";
        string constraints = BuildConstraints(method);

        sb.AppendLine(GenAttr);
        sb.Append("        public ")
          .Append(retKeyword).Append(' ')
          .Append(method.Name).Append(typeParams).Append('(')
          .Append(string.Join(", ", pars)).Append(')')
          .Append(constraints)
          .Append(" => ")
          .Append(innerName).Append('.').Append(method.Name).Append(typeParams)
          .Append('(').Append(string.Join(", ", args)).Append(");")
          .AppendLine();
    }

    private static void EmitProperty(StringBuilder sb, IPropertySymbol prop, string innerName)
    {
        string type = prop.Type.ToDisplayString(FullyQualified);
        sb.AppendLine(GenAttr);
        sb.Append("        public ").Append(type).Append(' ').Append(prop.Name).Append(" { ");
        if (prop.GetMethod is not null)
            sb.Append("get => ").Append(innerName).Append('.').Append(prop.Name).Append("; ");
        if (prop.SetMethod is not null)
            sb.Append("set => ").Append(innerName).Append('.').Append(prop.Name).Append(" = value; ");
        sb.AppendLine("}");
    }

    private static void EmitIndexer(StringBuilder sb, IPropertySymbol prop, string innerName)
    {
        string type = prop.Type.ToDisplayString(FullyQualified);
        var pars = new List<string>();
        var args = new List<string>();
        foreach (var p in prop.Parameters)
            BuildParam(p, pars, args);

        sb.AppendLine(GenAttr);
        sb.Append("        public ").Append(type).Append(" this[")
          .Append(string.Join(", ", pars)).Append("] { ");
        if (prop.GetMethod is not null)
            sb.Append("get => ").Append(innerName).Append('[').Append(string.Join(", ", args)).Append("]; ");
        if (prop.SetMethod is not null)
            sb.Append("set => ").Append(innerName).Append('[').Append(string.Join(", ", args)).Append("] = value; ");
        sb.AppendLine("}");
    }

    private static void EmitEvent(StringBuilder sb, IEventSymbol evt, string innerName)
    {
        string type = evt.Type.ToDisplayString(FullyQualified);
        sb.AppendLine(GenAttr);
        sb.Append("        public event ").Append(type).Append(' ').Append(evt.Name).AppendLine();
        sb.AppendLine("        {");
        sb.Append("            add => ").Append(innerName).Append('.').Append(evt.Name).AppendLine(" += value;");
        sb.Append("            remove => ").Append(innerName).Append('.').Append(evt.Name).AppendLine(" -= value;");
        sb.AppendLine("        }");
    }

    private static void BuildParam(IParameterSymbol p, List<string> pars, List<string> args)
    {
        string refKind = p.RefKind switch
        {
            RefKind.Ref => "ref ",
            RefKind.Out => "out ",
            RefKind.In => "in ",
            _ => "",
        };
        string ptype = p.Type.ToDisplayString(FullyQualified);
        // Special types (object, string, ...) sometimes drop their nullable annotation
        // in the display string; re-attach it so `object? arg = null` overrides match
        // and we don't trip CS8625.
        if (p.NullableAnnotation == NullableAnnotation.Annotated &&
            !p.Type.IsValueType && !ptype.EndsWith("?"))
        {
            ptype += "?";
        }
        string pname = SafeName(p.Name);

        string paramsKw = p.IsParams ? "params " : "";
        string decl = paramsKw + refKind + ptype + " " + pname;

        if (p.HasExplicitDefaultValue)
            decl += " = " + FormatDefault(p);

        pars.Add(decl);
        args.Add(refKind + pname);
    }

    private static string FormatDefault(IParameterSymbol p)
    {
        var v = p.ExplicitDefaultValue;
        if (v is null)
        {
            // For a null default, value types use the literal `default`; reference
            // types use `default!` so we never trip CS8625 regardless of whether the
            // interface was compiled in a nullable-aware context.
            return p.Type.IsValueType ? "default" : "default!";
        }
        return v switch
        {
            bool b => b ? "true" : "false",
            string s => "\"" + s.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"",
            char c => "'" + c + "'",
            float f => f.ToString(System.Globalization.CultureInfo.InvariantCulture) + "f",
            double d => d.ToString(System.Globalization.CultureInfo.InvariantCulture),
            _ when p.Type.TypeKind == TypeKind.Enum =>
                "(" + p.Type.ToDisplayString(FullyQualified) + ")" + v,
            _ => System.Convert.ToString(v, System.Globalization.CultureInfo.InvariantCulture) ?? "default",
        };
    }

    private static string BuildConstraints(IMethodSymbol method)
    {
        if (!method.IsGenericMethod)
            return "";
        var sb = new StringBuilder();
        foreach (var tp in method.TypeParameters)
        {
            var parts = new List<string>();
            if (tp.HasReferenceTypeConstraint) parts.Add("class");
            if (tp.HasValueTypeConstraint) parts.Add("struct");
            if (tp.HasNotNullConstraint) parts.Add("notnull");
            if (tp.HasUnmanagedTypeConstraint) parts.Add("unmanaged");
            foreach (var ct in tp.ConstraintTypes)
                parts.Add(ct.ToDisplayString(FullyQualified));
            if (tp.HasConstructorConstraint) parts.Add("new()");
            if (parts.Count > 0)
                sb.Append(" where ").Append(tp.Name).Append(" : ").Append(string.Join(", ", parts));
        }
        return sb.ToString();
    }

    // -----------------------------------------------------------------------
    // Signature helpers (must match how we detect already-declared members)
    // -----------------------------------------------------------------------

    private static string MethodSignature(IMethodSymbol m)
    {
        var sb = new StringBuilder();
        sb.Append(m.Name);
        sb.Append('`').Append(m.TypeParameters.Length);
        sb.Append('(');
        sb.Append(string.Join(",", m.Parameters.Select(p =>
            (p.RefKind == RefKind.None ? "" : p.RefKind.ToString().ToLowerInvariant() + " ")
            + p.Type.ToDisplayString(SignatureFormat))));
        sb.Append(')');
        return sb.ToString();
    }

    private static string IndexerSignature(IPropertySymbol p)
    {
        return "this[" + string.Join(",", p.Parameters.Select(x =>
            x.Type.ToDisplayString(SignatureFormat))) + "]";
    }

    private static string SafeName(string name)
    {
        return SyntaxFactsKeywords.Contains(name) ? "@" + name : name;
    }

    private static readonly HashSet<string> SyntaxFactsKeywords = new(StringComparer.Ordinal)
    {
        "abstract","as","base","bool","break","byte","case","catch","char","checked","class","const",
        "continue","decimal","default","delegate","do","double","else","enum","event","explicit","extern",
        "false","finally","fixed","float","for","foreach","goto","if","implicit","in","int","interface",
        "internal","is","lock","long","namespace","new","null","object","operator","out","override","params",
        "private","protected","public","readonly","ref","return","sbyte","sealed","short","sizeof","stackalloc",
        "static","string","struct","switch","this","throw","true","try","typeof","uint","ulong","unchecked",
        "unsafe","ushort","using","virtual","void","volatile","while",
    };

    private static readonly SymbolDisplayFormat FullyQualified =
        SymbolDisplayFormat.FullyQualifiedFormat
            .WithMiscellaneousOptions(
                SymbolDisplayMiscellaneousOptions.UseSpecialTypes |
                SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier |
                SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers);

    // For signature comparison we want stable type names without nullable annotations,
    // so the manual override matches the interface member regardless of NRT modifiers.
    private static readonly SymbolDisplayFormat SignatureFormat =
        SymbolDisplayFormat.FullyQualifiedFormat
            .WithMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.UseSpecialTypes);
}
