using System;
using System.IO;
using System.Linq;

namespace pkNX.Structures.FlatBuffers;

public static class ReflectionDumper
{
    public static void DumpSchema(ReflectionSchema schema, TextWriter writer, bool stripNamespace = true, string fileNameSpace = "pkNX.Structures.FlatBuffers")
    {
        writer.WriteLine("using System.Collections.Generic;");
        writer.WriteLine("using System.ComponentModel;");
        writer.WriteLine("using FlatSharp.Attributes;");
        writer.WriteLine("// ReSharper disable UnusedMember.Global");
        writer.WriteLine("// ReSharper disable ClassNeverInstantiated.Global");
        writer.WriteLine("// ReSharper disable UnusedType.Global");
        writer.WriteLine("#nullable disable");
        writer.WriteLine();
        writer.WriteLine($"namespace {fileNameSpace};");
        writer.WriteLine();
        writer.WriteLine("// Generated by FlatSharp");
        writer.WriteLine($"// FileIdentifier: {schema.FileIdentifier}");
        writer.WriteLine($"// FileExtension: {schema.FileExtension}");
        writer.WriteLine($"// Object Count: {schema.Objects.Count}");
        writer.WriteLine($"// Enum Count: {schema.Enums.Count}");
        writer.WriteLine($"// Root Type: {schema.RootTable?.Name ?? "None Specified"}");

        DumpObjects(schema, writer, stripNamespace);
        DumpEnums(schema, writer, stripNamespace);
    }

    private static ReadOnlySpan<char> GetName(ReadOnlySpan<char> defName, bool stripNamespace, bool forcePascalCase = true)
    {
        if (forcePascalCase)
            defName = defName.ToPascalCase();
        if (!stripNamespace)
            return defName;
        var idx = defName.LastIndexOf('.');
        return idx < 0 ? defName : defName[(idx + 1)..];
    }

    private static string ToPascalCase(this ReadOnlySpan<char> str)
    {
        if (str.IsEmpty)
            return string.Empty;
        Span<char> result = stackalloc char[str.Length];
        var upper = true;
        int p = 0;
        foreach (char c in str)
        {
            if (c == '_')
            {
                upper = true;
                continue;
            }
            result[p++] = upper ? char.ToUpperInvariant(c) : c;
            upper = false;
        }
        return new string(result[..p]);
    }

    private static void DumpObjects(ReflectionSchema schema, TextWriter writer, bool stripNamespace)
    {
        var list = schema.Objects;
        for (var i = list.Count - 1; i >= 0; i--)
        {
            writer.WriteLine();
            //writer.WriteLine($"// Object {i}");
            var def = list[i];

            writer.WriteLine($"[{(def.IsStruct ? "FlatBufferStruct" : "FlatBufferTable")}, TypeConverter(typeof(ExpandableObjectConverter))]");
            writer.WriteLine($"public class {GetName(def.Name, stripNamespace)}");
            writer.WriteLine("{");
            {
                int maxFieldId = def.Fields.Max(z => z.Value.Id);
                var digits = maxFieldId.ToString().Length;
                var format = new string('0', digits);
                foreach (var field in def.Fields.OrderBy(z => z.Value.Id))
                {
                    var prop = field.Value;
                    var required = prop.Required ? ", Required = true" : string.Empty;
                    var key = prop.Key ? ", Key = true" : string.Empty;
                    const string split = " ";
                    (string name, bool isPrimitive) = schema.ToClr(prop.Type);
                    var type = GetName(name, stripNamespace, !isPrimitive);
                    var propName = GetName(prop.Name, stripNamespace);
                    var attribute = $"FlatBufferItem({field.Value.Id.ToString(format)}{required}{key})";
                    writer.WriteLine($"    [{attribute}]{split}public {type} {propName} {{ get; set; }}");
                }
            }
            writer.WriteLine("}");
        }
    }

    private static void DumpEnums(ReflectionSchema schema, TextWriter writer, bool stripNamespace)
    {
        var list = schema.Enums;
        foreach (var def in list)
        {
            writer.WriteLine();
            (string typeName, _) = schema.ToClr(def.UnderlyingType, true);
            writer.WriteLine("[FlatBufferEnum(typeof({0}))]", typeName);

            // C# enums impl int by default, don't be too explicit.
            var implType = typeName == "int" ? string.Empty : $" : {typeName}";
            var defName = GetName(def.Name, stripNamespace);
            writer.WriteLine($"public enum {defName}{implType}");
            writer.WriteLine("{");
            {
                foreach (var value in def.Values.OrderBy(z => z.Value.Value))
                {
                    var kvp = value.Value;
                    writer.WriteLine($"    {kvp.Key} = {kvp.Value},");
                }
            }
            writer.WriteLine("}");
        }
    }

    public static (string Name, bool IsPrimitive) ToClr(this FlatBufferType type, ReflectionSchema schema, bool force = false)
    {
        var index = type.Index;
        if (index != -1)
        {
            if (type.BaseType == BaseType.Array)
            {
                var child = type.ElementType;
                var idx = type.Index;
                if (idx == -1)
                {
                    (string name, bool isPrimitive) = child.ToClr();
                    return ($"{name}[]", IsPrimitive: isPrimitive);
                }
                if (child is BaseType.Obj)
                    return ($"{schema.Objects[idx].Name}[]", false);
                return ($"{schema.Enums[idx].Name}[]", false);
            }
            if (type.BaseType == BaseType.Vector)
            {
                var child = type.ElementType;
                var idx = type.Index;
                if (idx == -1)
                {
                    (string name, bool isPrimitive) = child.ToClr();
                    return ($"IList<{name}>", IsPrimitive: isPrimitive);
                }
                if (child is BaseType.Obj)
                    return ($"IList<{schema.Objects[idx].Name}>", false);
                return ($"IList<{schema.Enums[idx].Name}>", false);
            }
            if (type.BaseType == BaseType.Obj)
            {
                var idx = type.Index;
                return ($"{schema.Objects[idx].Name}", false);
            }
            if (!force)
            {
                var idx = type.Index;
                return ($"{schema.Enums[idx].Name}", false);
            }
        }

        var baseType = type.BaseType;
        if (baseType is BaseType.Array)
        {
            var child = type.ElementType;
            (string name, bool isPrimitive) = child.ToClr();
            return ($"{name}[]", IsPrimitive: isPrimitive);
        }
        if (baseType is BaseType.Vector)
        {
            var child = type.ElementType;
            (string name, bool isPrimitive) = child.ToClr();
            return ($"IList<{name}>", IsPrimitive: isPrimitive);
        }

        var x = baseType.ToClr();
        if (type.ElementType != BaseType.None)
            return ($"{x.Name}[]", x.IsPrimitive);
        return x;
    }

    public static (string Name, bool IsPrimitive) ToClr(this BaseType type) => type switch
    {
        BaseType.Bool => ("bool", true),
        BaseType.UByte => ("byte", true),
        BaseType.Byte => ("sbyte", true),
        BaseType.UShort => ("ushort", true),
        BaseType.Short => ("short", true),
        BaseType.UInt => ("uint", true),
        BaseType.Int => ("int", true),
        BaseType.ULong => ("ulong", true),
        BaseType.Long => ("long", true),
        BaseType.Float => ("float", true),
        BaseType.Double => ("double", true),
        BaseType.String => ("string", true),
        BaseType.Vector => ("object", false),
        BaseType.Union =>  ("FlatBufferObject", false),
        _ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
    };
}
