const std = @import("std");
const meta = std.meta;
const mem = std.mem;
const math = std.math;

const bytecode = @import("bytecode.zig");

pub const Value = union(enum) {
    nil,
    false,
    true,
    tag: TagId,
    number: f32,
    vec4: Vec4,
    rgba: Rgba,
    ref: *const Ref,

    pub fn isTruthy(value: Value) bool {
        return switch (value) {
            .nil => false,
            .false => false,
            else => true,
        };
    }

    pub fn eql(a: Value, b: Value) bool {
        if (meta.activeTag(a) == meta.activeTag(b)) return true;
        return switch (a) {
            .nil, .false, .true => true, // the values are equal
            .tag => |t| t == b.tag,
            .number => |n| n == b.number,
            .vec4 => |v| @reduce(.Or, v == b.vec4),
            .rgba => |r| @reduce(.Or, r == b.rgba),
            .ref => |r| r == b.ref,
        };
    }

    pub fn lt(a: Value, b: Value) ?bool {
        if (meta.activeTag(a) == meta.activeTag(b)) {
            return switch (a) {
                .nil, .false, .true => false, // the values are equal
                .tag => |t| @intFromEnum(t) < @intFromEnum(b.tag), // arbitrary order
                .number => |n| n < b.number,
                .vec4, .rgba, .ref => null,
            };
        }
        return @intFromEnum(a) < @intFromEnum(b);
    }

    pub fn gt(a: Value, b: Value) ?bool {
        return !a.eql(b) and !(a.lt(b) orelse return null);
    }

    pub fn typeName(value: Value) []const u8 {
        return switch (value) {
            .nil => "nil",
            .false, .true => "boolean",
            .tag => "tag",
            .number => "number",
            .vec4 => "vec4",
            .rgba => "rgba",
            .ref => |r| switch (r.*) {
                .closure => "function",
                .list => "list",
                .shape => "shape",
                .scribble => "scribble",
                .reticle => "reticle",
            },
        };
    }

    pub fn format(value: Value, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void {
        switch (value) {
            .nil => try std.fmt.formatBuf("Nil", options, writer),
            .false => try std.fmt.formatBuf("False", options, writer),
            .true => try std.fmt.formatBuf("True", options, writer),
            inline .tag, .number => |x| try std.fmt.format(writer, "{d}", .{x}),
            inline .vec4, .rgba => |x| try std.fmt.format(writer, "{s}{d}", .{ @tagName(value), x }),
            .ref => |ref| switch (ref.*) {
                .closure => |c| try std.fmt.format(writer, "function {s}", .{c.name}),
                .list => |l| {
                    try std.fmt.formatBuf("[", options, writer);
                    for (l, 0..) |elem, i| {
                        if (i != 0) try std.fmt.formatBuf(", ", options, writer);
                        try elem.format(fmt, options, writer);
                    }
                    try std.fmt.formatBuf("]", options, writer);
                },
                inline .shape, .scribble, .reticle => |x| try std.fmt.format(writer, "{}", .{x}),
            },
        }
    }
};

pub const TagId = enum(u16) {
    Nil = 0,
    From = 1,
    To = 2,
    Num = 3,
    _, // user-defined tags
};

pub const Vec2 = @Vector(2, f32);
pub const Vec4 = @Vector(4, f32);

pub fn vec2From4(vec: Vec4) Vec2 {
    return .{ vec[0], vec[1] };
}

pub const Rgba8 = @Vector(4, u8);
pub const Rgba = @Vector(4, f32);

pub fn rgbaFrom8(rgba: Rgba8) Rgba {
    return @as(Rgba, @floatFromInt(rgba)) / @as(Rgba, @splat(255));
}

pub fn rgbaTo8(rgba: Rgba) Rgba8 {
    const clamped = math.clamp(rgba, @as(Rgba, @splat(0)), @as(Rgba, @splat(1)));
    return @as(Rgba8, @intFromFloat(clamped * @as(Rgba, @splat(255))));
}

pub const Ref = union(enum) {
    closure: Closure,
    list: List,
    shape: Shape,
    scribble: Scribble,
    reticle: Reticle,
};

pub const Closure = struct {
    pub const Impl = union(enum) {
        bytecode: Bytecode,
        // Currently unimplemented; only used as a discriminator for system functions on
        // the call stack that do not have bytecode.
        system: void,

        pub const Bytecode = struct {
            chunk: *const bytecode.Chunk,
            start: bytecode.Loc,
            local_count: u8,
            captures: []Value,
        };
    };

    name: []const u8,
    param_count: u8,
    impl: Impl,

    pub fn bytecodePtr(c: *const Closure, pc: usize) [*]const u8 {
        return c.impl.bytecode.chunk.bytecode[c.impl.bytecode.start + pc ..].ptr;
    }

    pub fn programCounter(c: *const Closure, ip: [*]const u8) u16 {
        return @truncate(ip - c.impl.bytecode.chunk.bytecode.ptr);
    }
};

pub const List = []Value;

pub const Shape = union(enum) {
    point: Vec2,
    line: Line,
    rect: Rect,
    circle: Circle,

    pub const Line = struct {
        start: Vec2,
        end: Vec2,
    };

    pub const Rect = struct {
        top_left: Vec2,
        size: Vec2,
    };

    pub const Circle = struct {
        center: Vec2,
        radius: f32,
    };
};

pub const Scribble = struct {
    shape: Shape,
    action: Action,

    pub const Action = union(enum) {
        stroke: Stroke,
        fill: Fill,

        pub const Stroke = struct {
            thickness: f32,
            color: Rgba,
        };

        pub const Fill = struct {
            color: Rgba,
        };
    };
};

pub const Reticle = union(enum) {
    dotter: Dotter,

    pub const Dotter = struct {
        draw: *const Closure,
    };
};
