const std = @import("std");
const mem = std.mem;
const builtin = @import("builtin");

const bytecode = @import("bytecode.zig");
const Canvas = @import("canvas.zig").Canvas;
const render = @import("render.zig");
const Scratch = @import("scratch.zig");
const value = @import("value.zig");
const Vm = @import("vm.zig");
const log = @import("log.zig");

pub const allocator =
    if (builtin.cpu.arch == .wasm32)
        std.heap.wasm_allocator
    else
        @import("allocator.zig").hostAllocator;

pub const std_options: std.Options = .{
    .log_level = .debug,
    .logFn = log.logFn,
};

pub fn enableLogScope(scope: @TypeOf(.enum_literal)) bool {
    if (scope == .vm)
        return false
    else
        return true;
}

// Allocator

export fn haku2_alloc(size: usize, alignment: usize) ?[*]u8 {
    return allocator.rawAlloc(size, mem.Alignment.fromByteUnits(alignment), @returnAddress());
}

export fn haku2_free(alloc: [*]u8, size: usize, alignment: usize) void {
    allocator.rawFree(alloc[0..size], mem.Alignment.fromByteUnits(alignment), @returnAddress());
}

// Scratch

export fn haku2_scratch_new(max: usize) ?*Scratch {
    return Scratch.create(allocator, max) catch return null;
}

export fn haku2_scratch_destroy(scratch: *Scratch) void {
    scratch.destroy(allocator);
}

export fn haku2_scratch_reset(scratch: *Scratch) void {
    scratch.fixedBuffer = std.heap.FixedBufferAllocator.init(scratch.buffer);
}

export fn haku2_scratch_used(scratch: *const Scratch) usize {
    return scratch.fixedBuffer.end_index;
}

// Limits

export fn haku2_limits_new() ?*Vm.Limits {
    const limits = allocator.create(Vm.Limits) catch return null;
    limits.* = .{};
    return limits;
}

export fn haku2_limits_destroy(limits: *Vm.Limits) void {
    allocator.destroy(limits);
}

export fn haku2_limits_set_stack_capacity(limits: *Vm.Limits, new: usize) void {
    limits.stack_capacity = new;
}

export fn haku2_limits_set_call_stack_capacity(limits: *Vm.Limits, new: usize) void {
    limits.call_stack_capacity = new;
}

// Defs

export fn haku2_defs_parse(
    defs_string: [*]const u8,
    defs_len: usize,
    tags_string: [*]const u8,
    tags_len: usize,
) ?*bytecode.Defs {
    return bytecode.Defs.parse(
        allocator,
        defs_string[0..defs_len],
        tags_string[0..tags_len],
    ) catch null;
}

export fn haku2_defs_destroy(defs: *bytecode.Defs) void {
    defs.destroy(allocator);
}

// VM

export fn haku2_vm_new() ?*Vm {
    const vm = allocator.create(Vm) catch return null;
    vm.* = .{};
    return vm;
}

export fn haku2_vm_destroy(vm: *Vm) void {
    allocator.destroy(vm);
}

export fn haku2_vm_reset(vm: *Vm, s: *Scratch, defs: *const bytecode.Defs, limits: *const Vm.Limits, fuel: u32) bool {
    vm.* = Vm.init(s.allocator(), defs, limits) catch return false;
    vm.reset(fuel);
    return true;
}

export fn haku2_vm_run_main(
    vm: *Vm,
    scratch: *Scratch,
    code: [*]const u8,
    code_len: usize,
    local_count: u8,
) bool {
    const chunk = scratch.allocator().create(bytecode.Chunk) catch {
        vm.outOfMemory() catch {};
        return false;
    };
    chunk.* = bytecode.Chunk{
        .bytecode = code[0..code_len],
    };
    const closure = scratch.allocator().create(value.Closure) catch {
        vm.outOfMemory() catch {};
        return false;
    };
    closure.* = value.Closure{
        .name = "(brush)",
        .param_count = 0,
        .impl = .{ .bytecode = .{
            .chunk = chunk,
            .start = 0,
            .local_count = local_count,
            .captures = &[_]value.Value{},
        } },
    };
    vm.run(scratch.allocator(), closure, vm.stack_top) catch return false;
    return true;
}

export fn haku2_vm_has_cont(vm: *const Vm) bool {
    if (vm.stack.len == 0) return false;
    const top = vm.top();
    return top == .ref and top.ref.* == .reticle;
}

export fn haku2_vm_is_dotter(vm: *const Vm) bool {
    if (vm.stack.len == 0) return false;
    const top = vm.top();
    return top == .ref and top.ref.* == .reticle and top.ref.reticle == .dotter;
}

export fn haku2_vm_run_dotter(
    vm: *Vm,
    scratch: *Scratch,
    from_x: f32,
    from_y: f32,
    to_x: f32,
    to_y: f32,
    num: f32,
) bool {
    vm.runDotter(
        scratch.allocator(),
        .{ from_x, from_y, 0, 0 },
        .{ to_x, to_y, 0, 0 },
        num,
    ) catch return false;
    return true;
}

export fn haku2_vm_fuel(vm: *const Vm) usize {
    return vm.fuel;
}

export fn haku2_vm_exception_len(vm: *const Vm) usize {
    if (vm.exception) |exn| {
        return exn.len;
    } else {
        return 0;
    }
}

export fn haku2_vm_exception_render(vm: *const Vm, buffer: [*]u8) void {
    const exn = vm.exception.?;
    _ = exn.format(buffer[0..exn.len], &exn.args);
}

export fn haku2_vm_stackframe_count(vm: *const Vm) usize {
    return vm.stackFrameCount();
}

export fn haku2_vm_stackframe_is_return_marker(vm: *const Vm, index: usize) bool {
    const stack_frame = vm.stackFrame(index);
    return stack_frame.closure == null;
}

export fn haku2_vm_stackframe_is_system(vm: *const Vm, index: usize) bool {
    const stack_frame = vm.stackFrame(index);
    return stack_frame.closure.?.impl == .system;
}

export fn haku2_vm_stackframe_pc(vm: *const Vm, index: usize) i32 {
    const stack_frame = vm.stackFrame(index);
    if (stack_frame.closure.?.impl == .bytecode)
        if (stack_frame.ip) |ip|
            return stack_frame.closure.?.programCounter(ip);
    return -1; // no return address
}

export fn haku2_vm_stackframe_function_name_len(vm: *const Vm, index: usize) usize {
    const stack_frame = vm.stackFrame(index);
    return stack_frame.closure.?.name.len;
}

export fn haku2_vm_stackframe_function_name(vm: *const Vm, index: usize) [*]const u8 {
    const stack_frame = vm.stackFrame(index);
    return stack_frame.closure.?.name.ptr;
}

// Renderer

export fn haku2_render(vm: *Vm, canvas: *Canvas, max_depth: usize) bool {
    render.render(vm, canvas, max_depth) catch return false;
    return true;
}
