using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;
using Mond.Compiler;
using Mond.Debugger;

namespace Mond.VirtualMachine
{
    partial class Machine
    {
        private readonly MondState _state;
        internal MondValue Global;

#if !NO_DEBUG
        private MondDebugAction _debugAction;
        private int _debugDepth;
        internal MondDebugger Debugger;
#endif

        public Machine(MondState state)
            : this()
        {
            _state = state;
            Global = MondValue.Object(state);

#if !NO_DEBUG
            _debugAction = MondDebugAction.Run;
            _debugDepth = 0;
            Debugger = null;
#endif
        }

        public string CurrentScript
        {
            get
            {
                if (_callStackSize == -1)
                    throw new InvalidOperationException("No scripts are running");

                return _callStack[_callStackSize].Program.DebugInfo?.FileName;
            }
        }

        public MondValue Load(MondProgram program)
        {
            if (program == null)
                throw new ArgumentNullException(nameof(program));

            var function = new MondValue(new Closure(program, 0, null, null));
            return Call(function);
        }

        public MondValue Call(MondValue function, params MondValue[] arguments)
        {
            if (function.Type == MondValueType.Object)
            {
                // insert "this" value into argument array
                Array.Resize(ref arguments, arguments.Length + 1);
                Array.Copy(arguments, 0, arguments, 1, arguments.Length - 1);
                arguments[0] = function;

                if (function.TryDispatch("__call", out var result, arguments))
                    return result;
            }

            if (function.Type != MondValueType.Function)
                throw new MondRuntimeException(RuntimeError.ValueNotCallable, function.Type.GetName());

            var closure = function.FunctionValue;

            switch (closure.Type)
            {
                case ClosureType.Mond:
                    var argFrame = closure.Arguments;
                    if (argFrame == null)
                        argFrame = new Frame(0, null, arguments.Length);
                    else
                        argFrame = new Frame(argFrame.Depth + 1, argFrame, arguments.Length);

                    for (var i = 0; i < arguments.Length; i++)
                    {
                        argFrame.Values[i] = arguments[i];
                    }

                    PushCall(new ReturnAddress(closure.Program, closure.Address, argFrame, _evalStackSize));
                    PushLocal(closure.Locals);

                    DebuggerOnCall();
                    break;

                case ClosureType.Native:
                    return closure.NativeFunction(_state, arguments);

                default:
                    throw new NotSupportedException();
            }

            return Run();
        }

        private MondValue Run()
        {
            var functionAddress = PeekCall();
            var program = functionAddress.Program;
            var code = program.Bytecode;

            var initialCallDepth = _callStackSize - 1; // "- 1" to not include values pushed by Call()
            var initialLocalDepth = _localStackSize - 1;
            var initialEvalDepth = _evalStackSize;
            var initialEvalDirtyDepth = _evalStackDirtySize;

            // to allow re-entrancy with our ref stack set up we need to preserve the dirty portion of the stack
            // this is because we may have Pop()'d a value to use but then had to run more code (ex. metamethod)
            _evalStackSize = initialEvalDirtyDepth;
            _evalStackDirtySize = initialEvalDirtyDepth;

            var ip = functionAddress.Address;
            var errorIp = 0;

            var args = functionAddress.Arguments;
            Frame locals = null;

            try
            {
                while (true)
                {
                    errorIp = ip;

                    var opcode = code[ip++];
                    var instruction = opcode >> 24;

                    //Console.WriteLine($"{ip - 1:X4} {(InstructionType)instruction}");
                    switch (instruction)
                    {
                        #region Stack Manipulation
                        case (int)InstructionType.Dup:
                            {
                                Push(Peek());
                                break;
                            }

                        case (int)InstructionType.Dup2:
                            {
                                ref readonly var value2 = ref Peek();
                                ref readonly var value1 = ref Peek(1);
                                Push(value1);
                                Push(value2);
                                break;
                            }

                        case (int)InstructionType.Drop:
                            {
                                Pop();
                                break;
                            }

                        case (int)InstructionType.Swap:
                            {
                                ref var value1 = ref Peek();
                                ref var value2 = ref Peek(1);
                                var temp = value1;
                                value1 = value2;
                                value2 = temp;
                                break;
                            }

                        case (int)InstructionType.Swap1For2:
                            {
                                ref var one = ref Peek();
                                ref var two2 = ref Peek(1);
                                ref var two1 = ref Peek(2);
                                var temp = one;
                                one = two2;
                                two2 = two1;
                                two1 = temp;
                                break;
                            }
                        #endregion

                        #region Constants
                        case (int)InstructionType.LdUndef:
                            {
                                Push(MondValue.Undefined);
                                break;
                            }

                        case (int)InstructionType.LdNull:
                            {
                                Push(MondValue.Null);
                                break;
                            }

                        case (int)InstructionType.LdTrue:
                            {
                                Push(MondValue.True);
                                break;
                            }

                        case (int)InstructionType.LdFalse:
                            {
                                Push(MondValue.False);
                                break;
                            }

                        case (int)InstructionType.LdNum:
                            {
                                var index = UnpackFirstOperand(opcode);
                                Push(program.Numbers[index]);
                                break;
                            }

                        case (int)InstructionType.LdStr:
                            {
                                var index = UnpackFirstOperand(opcode);
                                Push(program.Strings[index]);
                                break;
                            }

                        case (int)InstructionType.LdGlobal:
                            {
                                Push(Global);
                                break;
                            }
                        #endregion

                        #region Storables
                        case (int)InstructionType.LdLocF:
                            {
                                var index = UnpackFirstOperand(opcode);
                                Push(locals.Values[index]);
                                break;
                            }

                        case (int)InstructionType.StLocF:
                            {
                                var index = UnpackFirstOperand(opcode);
                                locals.Values[index] = Pop();
                                break;
                            }

                        case (int)InstructionType.LdArgF:
                            {
                                var index = UnpackFirstOperand(opcode);
                                Push(args.Get(index));
                                break;
                            }

                        case (int)InstructionType.StArgF:
                            {
                                var index = UnpackFirstOperand(opcode);
                                args.Set(index, Pop());
                                break;
                            }

                        case (int)InstructionType.LdLoc:
                            {
                                var depth = UnpackFirstOperand(opcode);
                                var index = code[ip++];

                                if (depth < 0)
                                    Push(args.Get(-depth, index));
                                else
                                    Push(locals.Get(depth, index));

                                break;
                            }

                        case (int)InstructionType.StLoc:
                            {
                                var depth = UnpackFirstOperand(opcode);
                                var index = code[ip++];

                                if (depth < 0)
                                    args.Set(-depth, index, Pop());
                                else
                                    locals.Set(depth, index, Pop());

                                break;
                            }

                        case (int)InstructionType.LdFld:
                            {
                                ref var value = ref Peek();
                                var stringIndex = UnpackFirstOperand(opcode);
                                value = value[program.Strings[stringIndex]];
                                break;
                            }

                        case (int)InstructionType.StFld:
                            {
                                ref readonly var obj = ref Pop();
                                ref readonly var value = ref Pop();
                                var stringIndex = UnpackFirstOperand(opcode);
                                obj[program.Strings[stringIndex]] = value;
                                break;
                            }

                        case (int)InstructionType.LdArr:
                            {
                                ref readonly var index = ref Pop();
                                ref var value = ref Peek();
                                value = value[index];
                                break;
                            }

                        case (int)InstructionType.StArr:
                            {
                                ref readonly var index = ref Pop();
                                ref readonly var array = ref Pop();
                                ref readonly var value = ref Pop();
                                array[index] = value;
                                break;
                            }

                        case (int)InstructionType.LdState:
                            {
                                var depth = UnpackFirstOperand(opcode);
                                var frame = locals.GetFrame(depth);
                                locals = frame.StoredFrame;

                                PopLocal();
                                PushLocal(locals);

                                var evals = frame.StoredEvals;
                                if (evals != null)
                                {
                                    for (var i = evals.Count - 1; i >= 0; i--)
                                    {
                                        Push(evals[i]); // TODO: use array
                                    }

                                    evals.Clear();
                                }

                                break;
                            }

                        case (int)InstructionType.StState:
                            {
                                var depth = UnpackFirstOperand(opcode);
                                var frame = locals.GetFrame(depth);
                                frame.StoredFrame = locals;

                                var initialEvals = _callStackSize >= 0 ? PeekCall().EvalDepth : 0;
                                var currentEvals = _evalStackSize;

                                if (currentEvals != initialEvals)
                                {
                                    var evals = frame.StoredEvals ?? (frame.StoredEvals = new List<MondValue>()); // TODO: use array

                                    while (currentEvals != initialEvals)
                                    {
                                        evals.Add(Pop()); // TODO: use array
                                        currentEvals--;
                                    }
                                }

                                break;
                            }
                        #endregion

                        #region Object Creation
                        case (int)InstructionType.NewObject:
                            {
                                var obj = MondValue.Object(_state);
                                Push(obj);
                                break;
                            }

                        case (int)InstructionType.NewArray:
                            {
                                var count = UnpackFirstOperand(opcode);
                                var array = MondValue.Array();
                                array.ArrayValue.Capacity = count;

                                for (var i = 0; i < count; i++)
                                    array.ArrayValue.Add(default);

                                Push(array);
                                break;
                            }

                        case (int)InstructionType.Slice:
                            {
                                ref readonly var step = ref Pop();
                                ref readonly var end = ref Pop();
                                ref readonly var start = ref Pop();
                                ref var array = ref Peek();
                                array = array.Slice(start, end, step);
                                break;
                            }
                        #endregion

                        #region Math
                        case (int)InstructionType.Add:
                            {
                                ref readonly var right = ref Pop();
                                ref var left = ref Peek();
                                left += right;
                                break;
                            }

                        case (int)InstructionType.Sub:
                            {
                                ref readonly var right = ref Pop();
                                ref var left = ref Peek();
                                left -= right;
                                break;
                            }

                        case (int)InstructionType.Mul:
                            {
                                ref readonly var right = ref Pop();
                                ref var left = ref Peek();
                                left *= right;
                                break;
                            }

                        case (int)InstructionType.Div:
                            {
                                ref readonly var right = ref Pop();
                                ref var left = ref Peek();
                                left /= right;
                                break;
                            }

                        case (int)InstructionType.Mod:
                            {
                                ref readonly var right = ref Pop();
                                ref var left = ref Peek();
                                left %= right;
                                break;
                            }

                        case (int)InstructionType.Exp:
                            {
                                ref readonly var right = ref Pop();
                                ref var left = ref Peek();
                                left = left.Pow(right);
                                break;
                            }

                        case (int)InstructionType.BitLShift:
                            {
                                ref readonly var right = ref Pop();
                                ref var left = ref Peek();
                                left = left.LShift(right);
                                break;
                            }

                        case (int)InstructionType.BitRShift:
                            {
                                ref readonly var right = ref Pop();
                                ref var left = ref Peek();
                                left = left.RShift(right);
                                break;
                            }

                        case (int)InstructionType.BitAnd:
                            {
                                ref readonly var right = ref Pop();
                                ref var left = ref Peek();
                                left &= right;
                                break;
                            }

                        case (int)InstructionType.BitOr:
                            {
                                ref readonly var right = ref Pop();
                                ref var left = ref Peek();
                                left |= right;
                                break;
                            }

                        case (int)InstructionType.BitXor:
                            {
                                ref readonly var right = ref Pop();
                                ref var left = ref Peek();
                                left ^= right;
                                break;
                            }

                        case (int)InstructionType.Neg:
                            {
                                ref var value = ref Peek();
                                value = -value;
                                break;
                            }

                        case (int)InstructionType.BitNot:
                            {
                                ref var value = ref Peek();
                                value = ~value;
                                break;
                            }
                        #endregion

                        #region Logic
                        case (int)InstructionType.Eq:
                            {
                                ref readonly var right = ref Pop();
                                ref var left = ref Peek();
                                left = left == right;
                                break;
                            }

                        case (int)InstructionType.Neq:
                            {
                                ref readonly var right = ref Pop();
                                ref var left = ref Peek();
                                left = left != right;
                                break;
                            }

                        case (int)InstructionType.Gt:
                            {
                                ref readonly var right = ref Pop();
                                ref var left = ref Peek();
                                left = left > right;
                                break;
                            }

                        case (int)InstructionType.Gte:
                            {
                                ref readonly var right = ref Pop();
                                ref var left = ref Peek();
                                left = left >= right;
                                break;
                            }

                        case (int)InstructionType.Lt:
                            {
                                ref readonly var right = ref Pop();
                                ref var left = ref Peek();
                                left = left < right;
                                break;
                            }

                        case (int)InstructionType.Lte:
                            {
                                ref readonly var right = ref Pop();
                                ref var left = ref Peek();
                                left = left <= right;
                                break;
                            }

                        case (int)InstructionType.Not:
                            {
                                ref var value = ref Peek();
                                value = !value;
                                break;
                            }

                        case (int)InstructionType.In:
                            {
                                ref readonly var right = ref Pop();
                                ref var left = ref Peek();
                                left = right.Contains(left);
                                break;
                            }

                        case (int)InstructionType.NotIn:
                            {
                                ref readonly var right = ref Pop();
                                ref var left = ref Peek();
                                left = !right.Contains(left);
                                break;
                            }
                        #endregion

                        #region Functions
                        case (int)InstructionType.Closure:
                            {
                                var address = UnpackFirstOperand(opcode);
                                Push(new MondValue(new Closure(program, address, args, locals)));
                                break;
                            }

                        case (int)InstructionType.Call:
                            {
                                DoCall(UnpackFirstOperand(opcode), ref code, ref ip, ref program, ref args, ref locals);
                                break;
                            }

                        case (int)InstructionType.TailCall:
                            {
                                var argCount = UnpackFirstOperand(opcode);
                                var address = code[ip++];
                                var unpackCount = code[ip++];

                                List<MondValue> unpackedArgs = null;

                                if (unpackCount > 0)
                                    unpackedArgs = UnpackArgs(code, ref ip, argCount, unpackCount);

                                ref var returnAddress = ref PeekCall();
                                var argFrame = returnAddress.Arguments;
                                var argFrameCount = unpackedArgs?.Count ?? argCount;

                                // make sure we have the correct number of values
                                if (argFrameCount != argFrame.Values.Length)
                                    argFrame.Values = new MondValue[argFrameCount];

                                // copy arguments into frame
                                if (unpackedArgs == null)
                                {
                                    for (var i = argFrameCount - 1; i >= 0; i--)
                                    {
                                        argFrame.Values[i] = Pop();
                                    }
                                }
                                else
                                {
                                    for (var i = 0; i < argFrameCount; i++)
                                    {
                                        argFrame.Values[i] = unpackedArgs[i];
                                    }
                                }

                                // get rid of old locals
                                PushLocal(PopLocal().Previous);

                                returnAddress = new ReturnAddress(returnAddress.Program, returnAddress.Address, argFrame, _evalStackSize);

                                ip = address;
                                break;
                            }

                        case (int)InstructionType.Enter:
                            {
                                var localCount = UnpackFirstOperand(opcode);

                                var frame = PopLocal();
                                frame = new Frame(frame?.Depth + 1 ?? 0, frame, localCount);

                                PushLocal(frame);
                                locals = frame;
                                break;
                            }

                        case (int)InstructionType.Leave:
                            {
                                var frame = PopLocal();
                                frame = frame.Previous;

                                PushLocal(frame);
                                locals = frame;
                                break;
                            }

                        case (int)InstructionType.Ret:
                            {
                                ref var returnAddress = ref PopCall();
                                PopLocal();

                                program = returnAddress.Program;
                                code = program.Bytecode;
                                ip = returnAddress.Address;
                                returnAddress = default;

                                args = _callStackSize >= 0 ? PeekCall().Arguments : null;
                                locals = _localStackSize >= 0 ? PeekLocal() : null;

                                DebuggerOnReturn();

                                if (_callStackSize == initialCallDepth)
                                {
                                    var result = Pop();
                                    ClearDirty();

                                    // restore the previous stack offsets
                                    _evalStackSize = initialEvalDepth;
                                    _evalStackDirtySize = initialEvalDirtyDepth;

                                    return result;
                                }

                                ClearDirty();

                                break;
                            }

                        case (int)InstructionType.VarArgs:
                            {
                                var fixedCount = UnpackFirstOperand(opcode);
                                var varArgs = MondValue.Array();

                                for (var i = fixedCount; i < args.Values.Length; i++)
                                {
                                    varArgs.ArrayValue.Add(args.Values[i]);
                                }

                                args.Set(args.Depth, fixedCount, varArgs);
                                break;
                            }
                        #endregion

                        #region Branching
                        case (int)InstructionType.Jmp:
                            {
                                var address = UnpackFirstOperand(opcode);
                                ip = address;
                                break;
                            }

                        case (int)InstructionType.JmpTrueP:
                            {
                                var address = UnpackFirstOperand(opcode);

                                if (Peek())
                                    ip = address;

                                break;
                            }

                        case (int)InstructionType.JmpFalseP:
                            {
                                var address = UnpackFirstOperand(opcode);

                                if (!Peek())
                                    ip = address;

                                break;
                            }

                        case (int)InstructionType.JmpTrue:
                            {
                                var address = UnpackFirstOperand(opcode);

                                if (Pop())
                                    ip = address;

                                break;
                            }

                        case (int)InstructionType.JmpFalse:
                            {
                                var address = UnpackFirstOperand(opcode);

                                if (!Pop())
                                    ip = address;

                                break;
                            }

                        case (int)InstructionType.JmpTable:
                            {
                                var start = UnpackFirstOperand(opcode);
                                var count = code[ip++];
                                var endIp = ip + count;

                                var value = Pop();
                                if (value.Type == MondValueType.Number)
                                {
                                    var number = (double)value;
                                    var numberInt = (int)number;

                                    if (number >= start && number < start + count &&
                                        Math.Abs(number - numberInt) <= double.Epsilon)
                                    {
                                        ip = code[ip + (numberInt - start)];
                                        break;
                                    }
                                }

                                ip = endIp;
                                break;
                            }
                        #endregion

                        case (int)InstructionType.Breakpoint:
                            {
#if !NO_DEBUG
                                if (Debugger != null)
                                {
                                    DebuggerBreak(program, locals, args, ip, initialCallDepth);
                                }
#endif
                                break;
                            }

                        case (int)InstructionType.DebugCheckpoint:
                            {
#if !NO_DEBUG
                                if (Debugger != null)
                                {
                                    var shouldStopAtStmt =
                                        (_debugAction == MondDebugAction.StepInto) ||
                                        (_debugAction == MondDebugAction.StepOver && _debugDepth == 0) ||
                                        (_debugAction == MondDebugAction.StepOut && _debugDepth < 0);

                                    var shouldBreak = shouldStopAtStmt || Debugger.ShouldBreak(program, ip);

                                    if (shouldBreak)
                                        DebuggerBreak(program, locals, args, ip, initialCallDepth);
                                }
#endif
                                break;
                            }

                        default:
                            throw new MondRuntimeException(RuntimeError.UnhandledOpcode);
                    }
                }
            }
            catch (Exception e)
            {
                var message = e.Message.Trim();

                // we skip the OOB checks in the stack methods because the CLR has issues eliminating 
                // its own checks, so we let it throw and check here for a bit of a speed boost
                if (e is IndexOutOfRangeException)
                {
                    if (_callStackSize >= CallStackCapacity || _localStackSize >= CallStackCapacity || _evalStackSize >= EvalStackCapacity)
                    {
                        message = RuntimeError.StackOverflow;
                    }
                    else if (_callStackSize < 0 || _localStackSize < 0 || _evalStackSize < 0)
                    {
                        message = RuntimeError.StackEmpty;
                    }
                }

                StringBuilder stackTraceBuilder;

                if (e is MondRuntimeException runtimeException &&
                    runtimeException.MondStackTrace != null)
                {
                    stackTraceBuilder = new StringBuilder(runtimeException.MondStackTrace);

                    // check if we are running in a wrapped function
                    var stackTrace = new System.Diagnostics.StackTrace(e, false);
                    var frames = stackTrace.GetFrames();
                    var foundWrapper = false;

                    // skip the first frame because it's this method? need to verify
                    for (var i = 1; i < frames.Length; i++)
                    {
                        var method = frames[i].GetMethod();
                        if (method == null)
                            continue; // ???

                        var type = method.DeclaringType;

                        // stop at the next call to Machine.Run because it can be recursive
                        if (type == typeof(Machine) && method.Name == "Run")
                            break;

                        // the wrapper is a lambda so it's in a compiler generated type, which will be nested
                        var parentType = type.DeclaringType;
                        if (parentType == null)
                            continue;

                        // the type and method are compiler generated so they have a weird (and compiler specific) name
                        const string wrapperMagic = "<CheckWrapFunction>";

                        // make sure the type is nested in MondValue and check both the type and method name
                        if (parentType == typeof(MondValue) && (method.Name.Contains(wrapperMagic) || type.Name.Contains(wrapperMagic)))
                        {
                            foundWrapper = true;
                            break;
                        }
                    }

                    // don't show a native transition for wrappers
                    if (!foundWrapper)
                        stackTraceBuilder.AppendLine("[... native ...]");
                }
                else
                {
                    stackTraceBuilder = new StringBuilder();
                }

                // first line of the stack trace is where we are running
                stackTraceBuilder.AppendLine(GetAddressDebugInfo(program, errorIp));

                // generate stack trace and reset stacks
                for (var i = Math.Min(_callStackSize - 1, CallStackCapacity - 1); i > initialCallDepth; i--)
                {
                    var returnAddress = _callStack[i];
                    stackTraceBuilder.AppendLine(GetAddressDebugInfo(returnAddress.Program, returnAddress.Address));
                }

                _callStackSize = initialCallDepth;
                for (var i = _callStackSize + 1; i < CallStackCapacity; i++)
                {
                    _callStack[i] = default;
                }

                _localStackSize = initialLocalDepth;
                for (var i = _localStackSize + 1; i < CallStackCapacity; i++)
                {
                    _localStack[i] = default;
                }

                _evalStackSize = initialEvalDepth;
                _evalStackDirtySize = initialEvalDirtyDepth;
                for (var i = _evalStackDirtySize + 1; i < EvalStackCapacity; i++)
                {
                    _evalStack[i] = default;
                }

                throw new MondRuntimeException(message, e)
                {
                    MondStackTrace = stackTraceBuilder.ToString()
                };
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static int UnpackFirstOperand(int opcode)
        {
            const int operandMask    = 0x00FFFFFF;
            const int operandSignBit = 0x00800000;

            return ((opcode & operandMask) ^ operandSignBit) - operandSignBit;
        }

        private void DoCall(int argCount, ref int[] code, ref int ip, ref MondProgram program, ref Frame args, ref Frame locals)
        {
            var unpackCount = code[ip++];

            var function = Pop();

            List<MondValue> unpackedArgs = null;

            if (unpackCount > 0)
                unpackedArgs = UnpackArgs(code, ref ip, argCount, unpackCount);

            var returnAddress = ip;

            if (function.Type == MondValueType.Object)
            {
                MondValue[] argArr;

                if (unpackedArgs == null)
                {
                    argArr = new MondValue[argCount + 1];

                    for (var i = argCount; i >= 1; i--)
                    {
                        argArr[i] = Pop();
                    }

                    argArr[0] = function;
                }
                else
                {
                    unpackedArgs.Insert(0, function);
                    argArr = unpackedArgs.ToArray();
                }

                if (function.TryDispatch("__call", out var result, argArr))
                {
                    Push(result);
                    return;
                }
            }

            if (function.Type != MondValueType.Function)
            {
                var callLength = /* opcode + argCount */ 1 + /* unpackLength */ 1 + /* unpackIndices*/ unpackCount;
                var ldFldOp = ip - callLength - 1; // get the IP value for an ldfld immediately before this call
                if (ldFldOp >= 0 && (code[ldFldOp] >> 24) == (int)InstructionType.LdFld)
                {
                    var fieldNameIdx = UnpackFirstOperand(code[ldFldOp]);

                    if (fieldNameIdx >= 0 && fieldNameIdx < program.Strings.Length)
                    {
                        var fieldName = program.Strings[fieldNameIdx];
                        throw new MondRuntimeException(RuntimeError.FieldNotCallable, (string)fieldName);
                    }
                }

                throw new MondRuntimeException(RuntimeError.ValueNotCallable, function.Type.GetName());
            }

            var closure = function.FunctionValue;

            var argFrame = function.FunctionValue.Arguments;
            var argFrameCount = unpackedArgs?.Count ?? argCount;

            if (argFrame == null)
                argFrame = new Frame(1, null, argFrameCount);
            else
                argFrame = new Frame(argFrame.Depth + 1, argFrame, argFrameCount);

            // copy arguments into frame
            if (unpackedArgs == null)
            {
                for (var i = argFrameCount - 1; i >= 0; i--)
                {
                    argFrame.Values[i] = Pop();
                }
            }
            else
            {
                for (var i = 0; i < argFrameCount; i++)
                {
                    argFrame.Values[i] = unpackedArgs[i];
                }
            }

            switch (closure.Type)
            {
                case ClosureType.Mond:
                    PushCall(new ReturnAddress(program, returnAddress, argFrame, _evalStackSize));
                    PushLocal(closure.Locals);

                    program = closure.Program;
                    code = program.Bytecode;
                    ip = closure.Address;
                    args = argFrame;
                    locals = closure.Locals;

                    DebuggerOnCall();
                    break;

                case ClosureType.Native:
                    var result = closure.NativeFunction(_state, argFrame.Values);
                    Push(result);
                    break;

                default:
                    throw new MondRuntimeException(RuntimeError.UnhandledClosureType);
            }
        }

        private List<MondValue> UnpackArgs(int[] code, ref int ip, int argCount, int unpackCount)
        {
            var unpackIndices = new List<int>(unpackCount);

            for (var i = 0; i < unpackCount; i++)
            {
                unpackIndices.Add(code[ip++]);
            }

            var unpackedArgs = new List<MondValue>(argCount + unpackCount * 16);
            var argIndex = 0;
            var unpackIndex = 0;

            for (var i = argCount - 1; i >= 0; i--)
            {
                var value = Pop();

                if (unpackIndex < unpackIndices.Count && i == unpackIndices[unpackIndex])
                {
                    unpackIndex++;

                    var start = argIndex;
                    var count = 0;

                    foreach (var unpackedValue in value.Enumerate(_state))
                    {
                        unpackedArgs.Add(unpackedValue);
                        argIndex++;
                        count++;
                    }

                    unpackedArgs.Reverse(start, count);

                    continue;
                }

                unpackedArgs.Add(value);
                argIndex++;
            }

            unpackedArgs.Reverse();
            return unpackedArgs;
        }

        private void DebuggerOnCall()
        {
#if !NO_DEBUG
            if (Debugger != null && _debugAction != MondDebugAction.Run)
            {
                _debugDepth++;
            }
#endif
        }

        private void DebuggerOnReturn()
        {
#if !NO_DEBUG
            if (Debugger != null && _debugAction != MondDebugAction.Run)
            {
                _debugDepth--;
            }
#endif
        }

#if !NO_DEBUG
        private void DebuggerBreak(MondProgram program, Frame locals, Frame args, int address, int initialCallDepth)
        {
            var context = new MondDebugContext(
                _state, program, address, locals, args, _callStack, _callStackSize, initialCallDepth);

            // so eval can work
            _debugAction = MondDebugAction.Run;

            _debugAction = Debugger.Break(context, address);
            _debugDepth = 0;
        }
#endif

        private static string GetAddressDebugInfo(MondProgram program, int address)
        {
            if (program.DebugInfo != null)
            {
                var func = program.DebugInfo.FindFunction(address);
                var position = program.DebugInfo.FindPosition(address);

                if (func.HasValue && position.HasValue)
                {
                    var prefix = "";
                    var funcName = program.Strings[func.Value.Name];
                    var fileName = program.DebugInfo.FileName ?? program.GetHashCode().ToString("X8");

                    if (!string.IsNullOrEmpty(funcName))
                        prefix = string.Format("at {0} ", funcName);

                    return string.Format("{0}in {1}: line {2}:{3}", prefix, fileName, position.Value.LineNumber, position.Value.ColumnNumber);
                }
            }

            return address.ToString("X8");
        }
    }
}
