const std = @import("std");
const mem = std.mem;
const debug = std.debug;
const log = std.log.scoped(.vm);
const testAllocator = std.testing.allocator;

const bytecode = @import("bytecode.zig");
const Canvas = @import("canvas.zig");
const system = @import("system.zig");
const value = @import("value.zig");
const Value = value.Value;

const Vm = @This();

stack: []Value = &.{},
stack_top: u32 = 0,
call_stack: []CallFrame = &.{},
call_stack_top: u32 = 0,
defs: []Value = &.{},
fuel: u32 = 0, // NOTE: VM must be refueled via reset() before running code
exception: ?Exception = null,
// closure, ip are for exception reporting. These variables are read-only, system functions
// cannot affect control flow using them.
closure: ?*const value.Closure = null,
ip: ?[*]const u8 = null,

pub const Limits = struct {
    stack_capacity: usize = 256,
    call_stack_capacity: usize = 256,
};

pub const CallFrame = struct {
    closure: ?*const value.Closure,
    return_addr: ?[*]const u8,
    bottom: u32,
};

pub const Exception = struct {
    len: usize,
    format: *const fn (buf: []u8, args: *align(64) const anyopaque) []u8,
    args: [40]u8 align(64), // increase the size if we ever throw a larger exception
};

/// All errors coming from inside the VM get turned into a single Exception type, which signals
/// any kind of error that prevented the machine from running any further.
/// Details about the exception can be found in the VM's `exception` field.
pub const Error = error{Exception};

// NOTE: A VM is only ever initialized. There is no function for deallocating its resources.
// The intent is to use it with a freshly-reset Scratch, and deallocate its resources through that.
pub fn init(a: mem.Allocator, defs: *const bytecode.Defs, limits: *const Limits) !Vm {
    return .{
        .stack = try a.alloc(Value, limits.stack_capacity),
        .call_stack = try a.alloc(CallFrame, limits.call_stack_capacity),
        .defs = try a.alloc(Value, defs.num_defs),
    };
}

pub fn reset(vm: *Vm, fuel: u32) void {
    vm.stack_top = 0;
    vm.call_stack_top = 0;
    vm.fuel = fuel;
    vm.exception = null;
}

// NOTE: When the VM throws an exception, it must be reset. There's no resuming from an exception.
pub fn throw(vm: *Vm, comptime fmt: []const u8, args: anytype) Error {
    log.info("throw: fmt={s}", .{fmt});
    log.debug("implicit stack frame: closure={?any} ip={?*}", .{ vm.closure, vm.ip });

    const Args = @TypeOf(args);
    const max_args_size = @sizeOf(@TypeOf(vm.exception.?.args));
    if (@sizeOf(Args) > max_args_size) {
        @compileError(std.fmt.comptimePrint(
            "format arguments are too large; size={}, max={}",
            .{ @sizeOf(Args), max_args_size },
        ));
    }

    const Formatter = struct {
        fn format(buf: []u8, erased_args: *align(64) const anyopaque) []u8 {
            return std.fmt.bufPrint(buf, fmt, @as(*const Args, @ptrCast(erased_args)).*) catch unreachable;
        }
    };

    var exn = Exception{
        .len = @truncate(std.fmt.count(fmt, args)),
        .format = Formatter.format,
        .args = undefined,
    };
    @memcpy(exn.args[0..@sizeOf(Args)], mem.asBytes(&args));
    vm.exception = exn;

    return error.Exception;
}

pub fn outOfMemory(vm: *Vm) Error {
    return vm.throw("out of memory", .{});
}

// NOTE: Different from CallFrame, this is a helper type for obtaining stack traces.
pub const StackFrame = struct {
    closure: ?*const value.Closure,
    ip: ?[*]const u8,
};

pub fn stackFrameCount(vm: *const Vm) usize {
    if (vm.closure != null) {
        return vm.call_stack_top + 1;
    } else {
        return vm.call_stack_top;
    }
}

pub fn stackFrame(vm: *const Vm, index: usize) StackFrame {
    if (vm.closure) |closure| {
        if (index == 0) {
            return .{
                .closure = closure,
                // Span info hairiness:
                // Because call frames place the instruction pointer past the call instruction,
                // the frontend subtracts 1 from it to get a position within the chunk that
                // has the correct line info.
                // This subtracting by one doesn't work for the implicit stack frame though, because
                // its ip gets updated at the _start_ of the instruction---meaning that if you
                // subtract one from it, it'll point to the _previous_ instruction, while the actual
                // error occurred in the current one.
                // To alleviate this, add one here to point to either the middle or the end of the
                // current instruction.
                .ip = if (vm.ip) |ip| ip + 1 else null,
            };
        } else {
            // NOTE: Minus two, because remember index == 0 is occupied by the current stack frame.
            const call_frame_index = vm.call_stack_top - 1 - (index - 1);
            const call_frame = vm.call_stack[call_frame_index];
            return .{
                .closure = call_frame.closure,
                .ip = call_frame.return_addr,
            };
        }
    } else {
        const call_frame = vm.call_stack[vm.call_stack_top - 1 - index];
        return .{
            .closure = call_frame.closure,
            .ip = call_frame.return_addr,
        };
    }
}

/// Debug assertion for bytecode validity.
/// In future versions, this may become disabled in release builds.
fn validateBytecode(vm: *Vm, ok: bool, comptime fmt: []const u8, args: anytype) Error!void {
    if (!ok) {
        return vm.throw("corrupted bytecode: " ++ fmt, args);
    }
}

pub fn consumeFuel(vm: *Vm, fuel: *u32, amount: u32) Error!void {
    const new_fuel, const overflow = @subWithOverflow(fuel.*, amount);
    if (overflow > 0) {
        return vm.throw("code ran for too long (out of fuel!)", .{});
    }
    fuel.* = new_fuel;
}

pub fn push(vm: *Vm, val: Value) Error!void {
    if (vm.stack_top >= vm.stack.len) {
        return vm.throw("too many live temporary values (local variables and expression operands)", .{});
    }
    log.debug("PUSH {any} <- {}", .{ vm.stack[0..vm.stack_top], val });
    vm.stack[vm.stack_top] = val;
    vm.stack_top += 1;
}

pub fn pop(vm: *Vm) Error!Value {
    try vm.validateBytecode(vm.stack_top > 0, "value stack underflow", .{});
    vm.stack_top -= 1;
    const result = vm.stack[vm.stack_top];
    log.debug("POP  {any} -> {}", .{ vm.stack[0..vm.stack_top], result });
    return vm.stack[vm.stack_top];
}

pub fn top(vm: *const Vm) Value {
    if (vm.stack_top > 0) {
        return vm.stack[vm.stack_top - 1];
    } else {
        return .nil;
    }
}

pub fn pushCall(vm: *Vm, frame: CallFrame) Error!void {
    if (vm.call_stack_top >= vm.call_stack.len) {
        return vm.throw("too much recursion", .{});
    }
    log.debug("PUSH CALL {}", .{frame});
    vm.call_stack[vm.call_stack_top] = frame;
    vm.call_stack_top += 1;
}

pub fn popCall(vm: *Vm) Error!CallFrame {
    try vm.validateBytecode(vm.call_stack_top > 0, "call stack underflow", .{});
    log.debug("POP CALL", .{});
    vm.call_stack_top -= 1;
    return vm.call_stack[vm.call_stack_top];
}

pub fn local(vm: *Vm, bottom: u32, offset: u8) Error!*Value {
    const index = bottom + offset;
    try vm.validateBytecode(index < vm.stack_top, "stack index out of bounds", .{});
    return &vm.stack[bottom + offset];
}

pub fn def(vm: *Vm, index: u16) Error!*Value {
    try vm.validateBytecode(index < vm.defs.len, "def index out of bounds", .{});
    return &vm.defs[index];
}

// Utility struct for storing and restoring the VM's state variables across FFI boundaries.
const Context = struct {
    ip: ?[*]const u8,
    closure: ?*const value.Closure,
    fuel: u32,
};

fn restoreContext(vm: *Vm) Context {
    return .{
        .ip = vm.ip,
        .closure = vm.closure,
        .fuel = vm.fuel,
    };
}

fn storeContext(vm: *Vm, context: Context) void {
    vm.ip = context.ip;
    vm.closure = context.closure;
    vm.fuel = context.fuel;
}

inline fn read(comptime T: type, ip: *[*]const u8) T {
    const result = mem.readInt(T, ip.*[0..@sizeOf(T)], std.builtin.Endian.little);
    ip.* = ip.*[@sizeOf(T)..];
    return result;
}

inline fn readOpcode(vm: *Vm, ip: *[*]const u8) bytecode.Opcode {
    vm.ip = ip.*;
    const opcode: bytecode.Opcode = @enumFromInt(read(u8, ip));
    log.debug("OP {*} {}", .{ ip.*, opcode });
    return opcode;
}

/// Before calling this, vm.stack_top should be saved and the appropriate amount of parameters must
/// be pushed onto the stack to execute the provided closure.
/// The saved vm.stack_top must be passed to init_bottom.
///
/// allocator should be a scratch buffer; there is no way to free the memory allocated by a VM.
pub fn run(
    vm: *Vm,
    allocator: mem.Allocator,
    init_closure: *const value.Closure,
    init_bottom: u32,
) Error!void {
    log.debug("BEGIN RUN {}", .{init_closure});

    // NOTE: This will need swapping out once we implement first-class system functions, because
    // system functions should be able to call themselves (like: map (range 1 10) sin)
    debug.assert(init_closure.impl == .bytecode);

    var closure = init_closure;
    var ip: [*]const u8 = closure.bytecodePtr(0);
    var bottom = init_bottom;
    var fuel = vm.fuel;

    vm.closure = closure;
    vm.ip = ip;

    for (0..closure.impl.bytecode.local_count) |_| {
        try vm.push(.nil);
    }

    try vm.pushCall(.{
        .closure = null,
        .return_addr = null, // nowhere to return to
        .bottom = bottom,
    });

    next: switch (vm.readOpcode(&ip)) {
        .nil => {
            try vm.consumeFuel(&fuel, 1);
            try vm.push(.nil);
            continue :next vm.readOpcode(&ip);
        },

        .false => {
            try vm.consumeFuel(&fuel, 1);
            try vm.push(.false);
            continue :next vm.readOpcode(&ip);
        },

        .true => {
            try vm.consumeFuel(&fuel, 1);
            try vm.push(.true);
            continue :next vm.readOpcode(&ip);
        },

        .tag => {
            try vm.consumeFuel(&fuel, 1);
            const tag_id: value.TagId = @enumFromInt(read(u16, &ip));
            try vm.push(.{ .tag = tag_id });
            continue :next vm.readOpcode(&ip);
        },

        .number => {
            try vm.consumeFuel(&fuel, 1);
            const number: f32 = @bitCast(read(u32, &ip));
            try vm.push(.{ .number = number });
            continue :next vm.readOpcode(&ip);
        },

        .rgba => {
            try vm.consumeFuel(&fuel, 1);
            const r = read(u8, &ip);
            const g = read(u8, &ip);
            const b = read(u8, &ip);
            const a = read(u8, &ip);
            try vm.push(.{ .rgba = value.rgbaFrom8(value.Rgba8{ r, g, b, a }) });
            continue :next vm.readOpcode(&ip);
        },

        .local => {
            try vm.consumeFuel(&fuel, 1);
            const index = read(u8, &ip);
            const l = try vm.local(bottom, index);
            try vm.push(l.*);
            continue :next vm.readOpcode(&ip);
        },

        .set_local => {
            try vm.consumeFuel(&fuel, 1);
            const index = read(u8, &ip);
            const new = try vm.pop();
            const l = try vm.local(bottom, index);
            l.* = new;
            continue :next vm.readOpcode(&ip);
        },

        .capture => {
            try vm.consumeFuel(&fuel, 1);
            const index = read(u8, &ip);
            const captures = closure.impl.bytecode.captures;
            try vm.validateBytecode(index < captures.len, "capture index out of bounds", .{});
            const capture = captures[index];
            try vm.push(capture);
            continue :next vm.readOpcode(&ip);
        },

        .def => {
            try vm.consumeFuel(&fuel, 1);
            const index = read(u16, &ip);
            const d = try vm.def(index);
            try vm.push(d.*);
            continue :next vm.readOpcode(&ip);
        },

        .set_def => {
            try vm.consumeFuel(&fuel, 1);
            const index = read(u16, &ip);
            const new = try vm.pop();
            const d = try vm.def(index);
            d.* = new;
            continue :next vm.readOpcode(&ip);
        },

        .list => {
            const len = read(u16, &ip);
            try vm.consumeFuel(&fuel, len);
            const list_end = vm.stack_top;
            vm.stack_top -= len;
            const elements = vm.stack[vm.stack_top..list_end];
            const list = allocator.dupe(Value, elements) catch return vm.outOfMemory();
            const ref = allocator.create(value.Ref) catch return vm.outOfMemory();
            ref.* = .{ .list = list };
            try vm.push(.{ .ref = ref });
            continue :next vm.readOpcode(&ip);
        },

        .function => {
            const param_count = read(u8, &ip);
            const then = read(u16, &ip);
            const name_len = read(u8, &ip);
            const name = ip[0..name_len];
            ip += name_len;
            const body = ip;
            ip = closure.bytecodePtr(then);

            const local_count = read(u8, &ip);
            const capture_count = read(u8, &ip);

            try vm.consumeFuel(&fuel, 1 + capture_count);
            const captures = allocator.alloc(Value, capture_count) catch return vm.outOfMemory();
            for (captures) |*capture| {
                const capture_kind = read(u8, &ip);
                const index = read(u8, &ip);
                vm.ip = ip;
                capture.* = switch (capture_kind) {
                    bytecode.capture_local => (try vm.local(bottom, index)).*,
                    bytecode.capture_capture => blk: {
                        const closureCaptures = closure.impl.bytecode.captures;
                        try vm.validateBytecode(index < closureCaptures.len, "capture index out of bounds", .{});
                        break :blk closureCaptures[index];
                    },
                    else => .nil,
                };
            }

            const new_closure = value.Closure{
                .name = name,
                .param_count = param_count,
                .impl = .{ .bytecode = .{
                    .chunk = closure.impl.bytecode.chunk,
                    .start = @truncate(body - closure.bytecodePtr(0)),
                    .local_count = local_count,
                    .captures = captures,
                } },
            };
            const ref = allocator.create(value.Ref) catch return vm.outOfMemory();
            ref.* = .{ .closure = new_closure };
            try vm.push(.{ .ref = ref });

            continue :next vm.readOpcode(&ip);
        },

        .jump => {
            const offset = read(u16, &ip);
            try vm.consumeFuel(&fuel, 1);
            ip = closure.bytecodePtr(offset);
            continue :next vm.readOpcode(&ip);
        },

        .jump_if_not => {
            const offset = read(u16, &ip);
            try vm.consumeFuel(&fuel, 1);
            const condition = try vm.pop();
            if (!condition.isTruthy()) {
                ip = closure.bytecodePtr(offset);
            }
            continue :next vm.readOpcode(&ip);
        },

        .field => {
            try vm.consumeFuel(&fuel, 1);

            const tag_count = read(u8, &ip);
            const field_tag = try vm.pop();
            if (field_tag != .tag) {
                return vm.throw("name of data field to look up must be a tag (starting with an uppercase letter)", .{});
            }
            const field_tag_id = field_tag.tag;

            var found_index: ?usize = null;
            for (0..tag_count) |i| {
                const tag_id: value.TagId = @enumFromInt(read(u16, &ip));
                if (tag_id == field_tag_id) {
                    found_index = i;
                }
            }

            if (found_index) |index| {
                const fields = closure.impl.bytecode.captures;
                try vm.validateBytecode(index < fields.len, "field index out of bounds", .{});
                const field = fields[index];
                try vm.push(field);
            } else {
                return vm.throw("field with this name does not exist", .{});
            }

            continue :next vm.readOpcode(&ip);
        },

        .call => {
            const arg_count = read(u8, &ip);

            try vm.consumeFuel(&fuel, 1);

            const function_value = try vm.pop();
            if (function_value != .ref or function_value.ref.* != .closure) {
                return vm.throw("attempt to call a value that is not a function", .{});
            }
            const called_closure = &function_value.ref.closure;

            // NOTE: Will need replacing for first-class system functions.
            debug.assert(called_closure.impl == .bytecode);

            if (arg_count != called_closure.param_count) {
                return vm.throw(
                    "function expects {} arguments, but it received {}",
                    .{ called_closure.param_count, arg_count },
                );
            }

            const call_frame = CallFrame{
                .closure = closure,
                .return_addr = ip,
                .bottom = bottom,
            };

            const new_bottom, const overflow = @subWithOverflow(vm.stack_top, arg_count);
            try vm.validateBytecode(overflow == 0, "not enough values on the stack for arguments", .{});

            closure = called_closure;
            ip = closure.bytecodePtr(0);
            bottom = new_bottom;

            // pushCall before setting vm.closure and vm.ip, such that stack overflow errors are
            // reported at the call site rather than within the called function.
            try vm.pushCall(call_frame);

            // Remember to update the closure used for exception reporting, since readOpcode does
            // not do that as it does with ip.
            // ip also needs updating, because the topmost stack frame will assume that vm.ip is
            // based in the new closure.
            // We want errors for no stack space being left for locals to appear at the start of the
            // function in source code, not earlier or later. Hence we do this before local slots
            // are pushed onto the stack.
            vm.closure = closure;
            vm.ip = ip;

            for (0..closure.impl.bytecode.local_count) |_| {
                try vm.push(.nil);
            }

            continue :next vm.readOpcode(&ip);
        },

        .system => {
            const index = read(u8, &ip);
            const arg_count = read(u8, &ip);
            const system_fn = &system.fns[index];

            try vm.consumeFuel(&fuel, 1);

            log.debug("system index={} arg_count={} system_fn={s}", .{ index, arg_count, system_fn.closure.name });

            // Push the current call frame onto the stack, such that the stack trace displays it
            // in addition to the ongoing system call.
            try vm.pushCall(.{
                .closure = closure,
                .return_addr = ip,
                .bottom = bottom,
            });

            // Also now, push the system function onto the stack, because nulling out vm.closure
            // and vm.ip removes the extra "current function" stack entry.
            try vm.pushCall(.{
                .closure = &system_fn.closure,
                .return_addr = null,
                .bottom = vm.stack_top - arg_count,
            });

            vm.storeContext(.{
                .fuel = fuel,
                .closure = null,
                .ip = null,
            });
            const result = try system_fn.impl(.{
                .vm = vm,
                .allocator = allocator,
                .args = vm.stack[vm.stack_top - arg_count .. vm.stack_top],
            });
            const context = vm.restoreContext();
            fuel = context.fuel;

            vm.stack_top -= arg_count;
            try vm.push(result);

            // Restore the exception handling ip and closure to what it was.
            log.debug("system restore", .{});
            vm.ip = ip;
            vm.closure = closure;

            // Remove the temporary call frames.
            _ = try vm.popCall(); // systemClosure
            _ = try vm.popCall(); // closure

            continue :next vm.readOpcode(&ip);
        },

        .ret => {
            try vm.consumeFuel(&fuel, 1);

            const result = try vm.pop();
            const call_frame = try vm.popCall();

            try vm.validateBytecode(
                bottom <= vm.stack_top,
                "called function popped too many values. bottom={} stack_top={}",
                .{ bottom, vm.stack_top },
            );

            vm.stack_top = bottom;
            try vm.push(result);

            if (call_frame.return_addr) |addr| {
                closure = call_frame.closure.?;
                ip = addr;
                bottom = call_frame.bottom;

                vm.closure = closure;
                vm.ip = ip;
            } else {
                // If there's nowhere to return to, stop interpreting.
                vm.storeContext(Context{
                    .closure = closure,
                    .ip = ip,
                    .fuel = fuel,
                });
                break :next;
            }

            continue :next vm.readOpcode(&ip);
        },

        _ => |invalid_opcode| {
            try vm.consumeFuel(&fuel, 1);
            // NOTE: Not a validateBytecode call because this is zero-cost on the happy path,
            // so we don't need to disable it on release builds.
            return vm.throw("corrupted bytecode: invalid opcode {}", .{invalid_opcode});
        },
    }

    log.debug("END RUN", .{});
}

pub const Dotter = struct {
    from: value.Vec4,
    to: value.Vec4,
    num: f32,
};

/// NOTE: Assumes the value at the top is a dotter reticle.
pub fn runDotter(
    vm: *Vm,
    allocator: mem.Allocator,
    from: value.Vec4,
    to: value.Vec4,
    num: f32,
) Error!void {
    const reticle = try vm.pop();
    const draw = reticle.ref.reticle.dotter.draw; // parameter count checked on construction

    const data = allocator.dupe(Value, &[_]Value{
        .{ .vec4 = from },
        .{ .vec4 = to },
        .{ .number = num },
    }) catch return vm.outOfMemory();
    const ref = allocator.create(value.Ref) catch return vm.outOfMemory();
    ref.* = value.Ref{ .closure = .{
        .name = "dotter",
        .param_count = 1,
        .impl = .{ .bytecode = .{
            .chunk = &system.record_dotter,
            .start = 0,
            .local_count = 0,
            .captures = data,
        } },
    } };

    const bottom = vm.stack_top;
    try vm.push(.{ .ref = ref });
    try vm.run(allocator, draw, bottom);
}
