using Lua.CodeAnalysis;
using Lua.Runtime;
using static Lua.Internal.OpMode;
using static Lua.Internal.OpArgMask;

namespace Lua.Internal;

readonly struct LuaDebug : IDisposable
{
    readonly LuaDebugBuffer buffer;
    readonly uint version;

    LuaDebug(LuaDebugBuffer buffer, uint version)
    {
        this.buffer = buffer;
        this.version = version;
    }

    public string? Name
    {
        get
        {
            CheckVersion();
            return buffer.Name;
        }
    }

    public string? NameWhat
    {
        get
        {
            CheckVersion();
            return buffer.NameWhat;
        }
    }

    public string? What
    {
        get
        {
            CheckVersion();
            return buffer.What;
        }
    }

    public string? Source
    {
        get
        {
            CheckVersion();
            return buffer.Source;
        }
    }

    public int CurrentLine
    {
        get
        {
            CheckVersion();
            return buffer.CurrentLine;
        }
    }

    public int LineDefined
    {
        get
        {
            CheckVersion();
            return buffer.LineDefined;
        }
    }

    public int LastLineDefined
    {
        get
        {
            CheckVersion();
            return buffer.LastLineDefined;
        }
    }

    public int UpValueCount
    {
        get
        {
            CheckVersion();
            return buffer.UpValueCount;
        }
    }

    public int ParameterCount
    {
        get
        {
            CheckVersion();
            return buffer.ParameterCount;
        }
    }

    public bool IsVarArg
    {
        get
        {
            CheckVersion();
            return buffer.IsVarArg;
        }
    }

    public bool IsTailCall
    {
        get
        {
            CheckVersion();
            return buffer.IsTailCall;
        }
    }

    public ReadOnlySpan<char> ShortSource
    {
        get
        {
            CheckVersion();
            return buffer.ShortSource.AsSpan(0, buffer.ShortSourceLength);
        }
    }

    public static LuaDebug Create(LuaGlobalState globalState, CallStackFrame? prevFrame, CallStackFrame? frame, LuaFunction function, int pc, ReadOnlySpan<char> what, out bool isValid)
    {
        if (!globalState.DebugBufferPool.TryPop(out var buffer))
        {
            buffer = new(globalState);
        }

        isValid = buffer.GetInfo(prevFrame, frame, function, pc, what);

        return new(buffer, buffer.version);
    }

    public void CheckVersion()
    {
        if (buffer.version != version)
        {
            ThrowObjectDisposedException();
        }
    }

    public void Dispose()
    {
        if (buffer.version != version)
        {
            ThrowObjectDisposedException();
        }

        buffer.Return(version);
    }

    void ThrowObjectDisposedException()
    {
        throw new ObjectDisposedException("This has been disposed");
    }


    internal class LuaDebugBuffer(LuaGlobalState globalState)
    {
        internal uint version;
        LuaGlobalState globalState = globalState;
        public string? Name;
        public string? NameWhat;
        public string? What;
        public string? Source;
        public int CurrentLine;
        public int LineDefined;
        public int LastLineDefined;
        public int UpValueCount;
        public int ParameterCount;
        public bool IsVarArg;
        public bool IsTailCall;
        public readonly char[] ShortSource = new char[59];
        public int ShortSourceLength;

        internal void Return(uint version)
        {
            if (this.version != version)
            {
                throw new ObjectDisposedException("Buffer has been modified");
            }

            Name = null;
            NameWhat = null;
            What = null;
            Source = null;
            CurrentLine = 0;
            LineDefined = 0;
            LastLineDefined = 0;
            UpValueCount = 0;
            ParameterCount = 0;
            IsVarArg = false;
            IsTailCall = false;

            if (version < uint.MaxValue)
            {
                this.version++;
                globalState.DebugBufferPool.Push(this);
            }
        }


        internal bool GetInfo(CallStackFrame? prevFrame, CallStackFrame? frame, LuaFunction function, int pc, ReadOnlySpan<char> what)
        {
            var closure = function as LuaClosure;
            var status = 1;
            foreach (var c in what)
            {
                switch (c)
                {
                    case 'S':
                        {
                            GetFuncInfo(function);
                            break;
                        }
                    case 'l':
                        {
                            CurrentLine = pc >= 0 && closure is not null ? closure.Proto.LineInfo[pc] : -1;
                            break;
                        }
                    case 'u':
                        {
                            UpValueCount = closure is null ? 0 : closure.UpValues.Length;
                            if (closure is null)
                            {
                                IsVarArg = true;
                                ParameterCount = 0;
                            }
                            else
                            {
                                IsVarArg = closure.Proto.HasVariableArguments;
                                ParameterCount = closure.Proto.ParameterCount;
                            }

                            break;
                        }
                    case 't':
                        {
                            IsTailCall = frame.HasValue && (frame.Value.Flags | CallStackFrameFlags.TailCall) == frame.Value.Flags;
                            break;
                        }
                    case 'n':
                        {
                            /* calling function is a known Lua function? */
                            if (prevFrame is { Function: LuaClosure prevFrameClosure })
                            {
                                NameWhat = GetFuncName(prevFrameClosure.Proto, frame?.CallerInstructionIndex ?? 0, out Name);
                            }
                            else
                            {
                                NameWhat = null;
                            }

                            if (NameWhat is null)
                            {
                                NameWhat = ""; /* not found */
                                Name = null;
                            }
                            else if (NameWhat != null && Name is "?")
                            {
                                Name = function.Name;
                            }

                            break;
                        }
                    case 'L':
                    case 'f': /* handled by lua_getinfo */
                        break;
                    default:
                        status = 0; /* invalid option */
                        break;
                }
            }

            return status == 1;
        }

        void GetFuncInfo(LuaFunction f)
        {
            if (f is not LuaClosure cl)
            {
                Source = "=[C#]";
                LineDefined = -1;
                LastLineDefined = -1;
                What = "C#";
            }
            else
            {
                var p = cl.Proto;
                Source = p.ChunkName;
                LineDefined = p.LineDefined;
                LastLineDefined = p.LastLineDefined;
                What = LineDefined == 0 ? "main" : "Lua";
            }

            ShortSourceLength = WriteShortSource(Source, ShortSource);
        }
    }


    internal static string? GetLocalName(Prototype prototype, int register, int pc)
    {
        var locals = prototype.LocalVariables;
        var localId = register + 1;
        foreach (var l in locals)
        {
            if (pc < l.StartPc)
            {
                break;
            }

            if (l.EndPc <= pc)
            {
                continue;
            }

            localId--;
            if (localId == 0)
            {
                return l.Name;
            }
        }

        return null;
    }

    static int FilterPc(int pc, int jmpTarget)
    {
        if (pc < jmpTarget) /* is code conditional (inside a jump)? */
        {
            return -1; /* cannot know who sets that register */
        }
        else
        {
            return pc; /* current position sets that register */
        }
    }

    internal static int FindSetRegister(Prototype prototype, int lastPc, int reg)
    {
        int pc;
        var setReg = -1; /* keep last instruction that changed 'reg' */
        var jmpTarget = 0; /* any code before this address is conditional */
        var instructions = prototype.Code;
        for (pc = 0; pc < lastPc; pc++)
        {
            var i = instructions[pc];
            var op = i.OpCode;
            var a = i.A;
            switch (op)
            {
                case OpCode.LoadNil:
                    {
                        var b = i.B;
                        if (a <= reg && reg <= a + b) /* set registers from 'a' to 'a+b' */
                        {
                            setReg = FilterPc(pc, jmpTarget);
                        }

                        break;
                    }
                case OpCode.TForCall:
                    {
                        if (reg >= a + 2) /* affect all regs above its base */
                        {
                            setReg = FilterPc(pc, jmpTarget);
                        }

                        break;
                    }
                case OpCode.Call:
                case OpCode.TailCall:
                    {
                        if (reg >= a) /* affect all registers above base */
                        {
                            setReg = FilterPc(pc, jmpTarget);
                        }

                        break;
                    }
                case OpCode.Jmp:
                    {
                        var b = i.SBx;
                        var dest = pc + 1 + b;
                        /* jump is forward and do not skip `lastpc'? */
                        if (pc < dest && dest <= lastPc)
                        {
                            if (dest > jmpTarget)
                            {
                                jmpTarget = dest; /* update 'jmptarget' */
                            }
                        }

                        break;
                    }
                case OpCode.Test:
                    {
                        if (reg == a) /* jumped code can change 'a' */
                        {
                            setReg = FilterPc(pc, jmpTarget);
                        }

                        break;
                    }
                default:
                    if (TestAMode(op) && reg == a) /* any instruction that set A */
                    {
                        setReg = FilterPc(pc, jmpTarget);
                    }

                    break;
            }
        }

        return setReg;
    }

    static void GetConstantName(Prototype p, int pc, int c, out string name)
    {
        if (c >= 256)
        {
            /* is 'c' a constant? */
            var kvalue = p.Constants[c - 256];
            if (kvalue.TryReadString(out name))
            {
                /* literal constant? */
                /* it is its own name */
                return;
            }
            /* else no reasonable name found */
        }
        else
        {
            /* 'c' is a register */
            var what = GetName(p, pc, c, out name!); /* search for 'c' */
            if (what != null && what[0] == 'c')
            {
                /* found a constant name? */
                return; /* 'name' already filled */
            }
            /* else no reasonable name found */
        }

        name = "?"; /* no reasonable name found */
    }


    internal static string? GetName(Prototype prototype, int lastPc, int reg, out string? name)
    {
        name = GetLocalName(prototype, reg, lastPc);
        if (name != null)
        {
            return "local";
        }

        var pc = FindSetRegister(prototype, lastPc, reg);
        if (pc != -1)
        {
            /* could find instruction? */
            var i = prototype.Code[pc];
            var op = i.OpCode;
            switch (op)
            {
                case OpCode.Move:
                    {
                        var b = i.B; /* move from 'b' to 'a' */
                        if (b < i.A)
                        {
                            return GetName(prototype, pc, b, out name); /* get name for 'b' */
                        }

                        break;
                    }
                case OpCode.GetTabUp:
                case OpCode.GetTable:
                    {
                        var k = i.C; /* key index */
                        var t = i.B; /* table index */

                        var vn = op == OpCode.GetTable /* name of indexed variable */
                            ? GetLocalName(prototype, t, pc)
                            : prototype.UpValues[t].Name.ToString();
                        GetConstantName(prototype, pc, k, out name);
                        return vn is "_ENV" ? "global" : "field";
                    }
                case OpCode.GetUpVal:
                    {
                        name = prototype.UpValues[i.B].Name.ToString();
                        return "upvalue";
                    }
                case OpCode.LoadK:
                case OpCode.LoadKX:
                    {
                        var b = op != OpCode.LoadKX
                            ? i.Bx
                            : prototype.Code[pc + 1].Ax;
                        if (prototype.Constants[b].TryReadString(out name))
                        {
                            return "constant";
                        }

                        break;
                    }
                case OpCode.Self:
                    {
                        var k = i.C; /* key index */
                        GetConstantName(prototype, pc, k, out name);
                        return "method";
                    }
                default: break; /* go through to return NULL */
            }
        }

        return null; /* could not find reasonable name */
    }

    internal static string? GetFuncName(Prototype prototype, int pc, out string? name)
    {
        var i = prototype.Code[pc]; /* calling instruction */
        switch (i.OpCode)
        {
            case OpCode.Call:
            case OpCode.TailCall: /* get function name */
                return GetName(prototype, pc, i.A, out name);
            case OpCode.TForCall:
                {
                    /* for iterator */
                    name = "for iterator";
                    return "for iterator";
                }
            case OpCode.Self:
            case OpCode.GetTabUp:
            case OpCode.GetTable:
                name = "index";
                break;
            case OpCode.SetTabUp:
            case OpCode.SetTable:
                name = "newindex";
                break;
            case OpCode.Add:
                name = "add";
                break;
            case OpCode.Sub:
                name = "sub";
                break;
            case OpCode.Mul:
                name = "mul";
                break;
            case OpCode.Div:
                name = "div";
                break;
            case OpCode.Mod:
                name = "mod";
                break;
            case OpCode.Pow:
                name = "pow";
                break;
            case OpCode.Unm:
                name = "unm";
                break;
            case OpCode.Len:
                name = "len";
                break;
            case OpCode.Concat:
                name = "concat";
                break;
            case OpCode.Eq:
                name = "eq";
                break;
            case OpCode.Lt:
                name = "lt";
                break;
            case OpCode.Le:
                name = "le";
                break;
            default:
                name = null;
                return null;
        }

        return "metamethod";
    }

    internal static int WriteShortSource(ReadOnlySpan<char> source, Span<char> dest)
    {
        const string PRE = "[string \"";
        const int PRE_LEN = 9;
        const string POS = "\"]";
        const int POS_LEN = 2;
        const string RETS = "...";
        const int RETS_LEN = 3;
        const string PREPOS = "[string \"\"]";

        const int BUFFER_LEN = 59;
        if (dest.Length != BUFFER_LEN)
        {
            throw new ArgumentException("dest must be 60 chars long");
        }

        if (source.Length == 0)
        {
            PREPOS.AsSpan().CopyTo(dest);
            return PREPOS.Length;
        }

        if (source[0] == '=')
        {
            source = source[1..]; /* skip the '=' */
            /* 'literal' source */
            if (source.Length < BUFFER_LEN) /* small enough? */
            {
                source.CopyTo(dest);
                return source.Length;
            }
            else
            {
                /* truncate it */
                source[..BUFFER_LEN].CopyTo(dest);
                return BUFFER_LEN;
            }
        }
        else if (source[0] == '@')
        {
            /* file name */
            source = source[1..]; /* skip the '@' */
            if (source.Length <= BUFFER_LEN) /* small enough? */
            {
                source.CopyTo(dest);
                return source.Length;
            }
            else
            {
                /* add '...' before rest of name */
                RETS.AsSpan().CopyTo(dest);
                source[^(BUFFER_LEN - RETS_LEN)..].CopyTo(dest[RETS_LEN..]);

                return BUFFER_LEN;
            }
        }
        else
        {
            /* string; format as [string "source"] */


            PRE.AsSpan().CopyTo(dest);
            var newLine = source.IndexOfAny("\r\n");
            if (newLine == -1 && source.Length < BUFFER_LEN - (PRE_LEN + RETS_LEN + POS_LEN))
            {
                source.CopyTo(dest[PRE_LEN..]);
                POS.AsSpan().CopyTo(dest[(PRE_LEN + source.Length)..]);
                return PRE_LEN + source.Length + POS_LEN;
            }

            if (newLine != -1)
            {
                source = source[..newLine]; /* stop at first newline */
            }

            if (BUFFER_LEN - (PRE_LEN + RETS_LEN + POS_LEN) < source.Length)
            {
                source = source[..(BUFFER_LEN - PRE_LEN - RETS_LEN - POS_LEN)];
            }

            /* add '...' before rest of name */
            source.CopyTo(dest[PRE_LEN..]);
            RETS.AsSpan().CopyTo(dest[(PRE_LEN + source.Length)..]);
            POS.AsSpan().CopyTo(dest[(PRE_LEN + source.Length + RETS_LEN)..]);
            return PRE_LEN + source.Length + RETS_LEN + POS_LEN;
        }
    }

    static int GetOpMode(byte t, byte a, OpArgMask b, OpArgMask c, OpMode m)
    {
        return (t << 7) | (a << 6) | ((byte)b << 4) | ((byte)c << 2) | (byte)m;
    }

    static readonly int[] OpModes =
    [
        GetOpMode(0, 1, OpArgR, OpArgN, iABC),  /* OP_MOVE */
        GetOpMode(0, 1, OpArgK, OpArgN, iABx),  /* OP_LOADK */
        GetOpMode(0, 1, OpArgN, OpArgN, iABx),  /* OP_LOADKX */
        GetOpMode(0, 1, OpArgU, OpArgU, iABC),  /* OP_LOADBOOL */
        GetOpMode(0, 1, OpArgU, OpArgN, iABC),  /* OP_LOADNIL */
        GetOpMode(0, 1, OpArgU, OpArgN, iABC),  /* OP_GETUPVAL */
        GetOpMode(0, 1, OpArgU, OpArgK, iABC),  /* OP_GETTABUP */
        GetOpMode(0, 1, OpArgR, OpArgK, iABC),  /* OP_GETTABLE */
        GetOpMode(0, 0, OpArgK, OpArgK, iABC),  /* OP_SETTABUP */
        GetOpMode(0, 0, OpArgU, OpArgN, iABC),  /* OP_SETUPVAL */
        GetOpMode(0, 0, OpArgK, OpArgK, iABC),  /* OP_SETTABLE */
        GetOpMode(0, 1, OpArgU, OpArgU, iABC),  /* OP_NEWTABLE */
        GetOpMode(0, 1, OpArgR, OpArgK, iABC),  /* OP_SELF */
        GetOpMode(0, 1, OpArgK, OpArgK, iABC),  /* OP_ADD */
        GetOpMode(0, 1, OpArgK, OpArgK, iABC),  /* OP_SUB */
        GetOpMode(0, 1, OpArgK, OpArgK, iABC),  /* OP_MUL */
        GetOpMode(0, 1, OpArgK, OpArgK, iABC),  /* OP_DIV */
        GetOpMode(0, 1, OpArgK, OpArgK, iABC),  /* OP_MOD */
        GetOpMode(0, 1, OpArgK, OpArgK, iABC),  /* OP_POW */
        GetOpMode(0, 1, OpArgR, OpArgN, iABC),  /* OP_UNM */
        GetOpMode(0, 1, OpArgR, OpArgN, iABC),  /* OP_NOT */
        GetOpMode(0, 1, OpArgR, OpArgN, iABC),  /* OP_LEN */
        GetOpMode(0, 1, OpArgR, OpArgR, iABC),  /* OP_CONCAT */
        GetOpMode(0, 0, OpArgR, OpArgN, iAsBx), /* OP_JMP */
        GetOpMode(1, 0, OpArgK, OpArgK, iABC),  /* OP_EQ */
        GetOpMode(1, 0, OpArgK, OpArgK, iABC),  /* OP_LT */
        GetOpMode(1, 0, OpArgK, OpArgK, iABC),  /* OP_LE */
        GetOpMode(1, 0, OpArgN, OpArgU, iABC),  /* OP_TEST */
        GetOpMode(1, 1, OpArgR, OpArgU, iABC),  /* OP_TESTSET */
        GetOpMode(0, 1, OpArgU, OpArgU, iABC),  /* OP_CALL */
        GetOpMode(0, 1, OpArgU, OpArgU, iABC),  /* OP_TAILCALL */
        GetOpMode(0, 0, OpArgU, OpArgN, iABC),  /* OP_RETURN */
        GetOpMode(0, 1, OpArgR, OpArgN, iAsBx), /* OP_FORLOOP */
        GetOpMode(0, 1, OpArgR, OpArgN, iAsBx), /* OP_FORPREP */
        GetOpMode(0, 0, OpArgN, OpArgU, iABC),  /* OP_TFORCALL */
        GetOpMode(0, 1, OpArgR, OpArgN, iAsBx), /* OP_TFORLOOP */
        GetOpMode(0, 0, OpArgU, OpArgU, iABC),  /* OP_SETLIST */
        GetOpMode(0, 1, OpArgU, OpArgN, iABx),  /* OP_CLOSURE */
        GetOpMode(0, 1, OpArgU, OpArgN, iABC),  /* OP_VARARG */
        GetOpMode(0, 0, OpArgU, OpArgU, iAx)    /* OP_EXTRAARG */
    ];

    internal static OpMode GetOpMode(OpCode m)
    {
        return (OpMode)(OpModes[(int)m] & 3);
    }

    internal static OpArgMask GetBMode(OpCode m)
    {
        return (OpArgMask)((OpModes[(int)m] >> 4) & 3);
    }

    internal static OpArgMask GetCMode(OpCode m)
    {
        return (OpArgMask)((OpModes[(int)m] >> 2) & 3);
    }

    internal static bool TestAMode(OpCode m)
    {
        return (OpModes[(int)m] & (1 << 6)) != 0;
    }

    internal static bool TestTMode(OpCode m)
    {
        return (OpModes[(int)m] & (1 << 7)) != 0;
    }
}

enum OpMode : byte
{
    iABC,
    iABx,
    iAsBx,
    iAx
}

enum OpArgMask : byte
{
    OpArgN, /* argument is not used */
    OpArgU, /* argument is used */
    OpArgR, /* argument is a register or a jump offset */
    OpArgK, /* argument is a constant or register/constant */
}