const std = @import("std");
const rl = @import("raylib");
const rect_utils = @import("./rect-utils.zig");
const assert = std.debug.assert;
const SourceLocation = std.builtin.SourceLocation;

// TODO: Implement Id context (I.e. ID parenting)

const UI = @This();

const max_stack_depth = 16;
const TransformFrame = struct {
    offset: rl.Vector2,
    scale: rl.Vector2,
};

hot_widget: ?Id = null,
active_widget: ?Id = null,

transform_stack: std.BoundedArray(TransformFrame, max_stack_depth),

pub fn init() UI {
    var stack = std.BoundedArray(TransformFrame, max_stack_depth).init(0) catch unreachable;
    stack.appendAssumeCapacity(TransformFrame{
        .offset = rl.Vector2{ .x = 0, .y = 0 },
        .scale  = rl.Vector2{ .x = 1, .y = 1 },
    });

    return UI{
        .transform_stack = stack
    };
}

pub fn isHot(self: *const UI, id: Id) bool {
    if (self.hot_widget) |hot_id| {
        return hot_id.eql(id);
    }
    return false;
}

pub fn isActive(self: *const UI, id: Id) bool {
    if (self.active_widget) |active_id| {
        return active_id.eql(id);
    }
    return false;
}

pub fn hashSrc(src: SourceLocation) u64 {
    var hash = std.hash.Fnv1a_64.init();
    hash.update(src.file);
    hash.update(std.mem.asBytes(&src.line));
    hash.update(std.mem.asBytes(&src.column));
    return hash.value;
}

fn getTopFrame(self: *UI) *TransformFrame {
    assert(self.transform_stack.len >= 1);
    return &self.transform_stack.buffer[self.transform_stack.len-1];
}

pub fn getMousePosition(self: *UI) rl.Vector2 {
    const frame = self.getTopFrame();
    return rl.getMousePosition().subtract(frame.offset).divide(frame.scale);
}

pub fn getMouseDelta(self: *UI) rl.Vector2 {
    const frame = self.getTopFrame();
    return rl.Vector2.multiply(rl.getMouseDelta(), frame.scale);
}

pub fn getMouseWheelMove(self: *UI) f32 {
    const frame = self.getTopFrame();
    return rl.getMouseWheelMove() * frame.scale.y;
}

pub fn isMouseInside(self: *UI, rect: rl.Rectangle) bool {
    return rect_utils.isInsideVec2(rect, self.getMousePosition());
}

pub fn transformScale(self: *UI, x: f32, y: f32) void {
    const frame = self.getTopFrame();
    frame.scale.x *= x;
    frame.scale.y *= y;

    rl.gl.rlScalef(x, y, 1);
}

pub fn transformTranslate(self: *UI, x: f32, y: f32) void {
    const frame = self.getTopFrame();
    frame.offset.x += x * frame.scale.x;
    frame.offset.y += y * frame.scale.y;

    rl.gl.rlTranslatef(x, y, 0);
}

pub fn pushTransform(self: *UI) void {
    rl.gl.rlPushMatrix();
    self.transform_stack.appendAssumeCapacity(self.getTopFrame().*);
}

pub fn popTransform(self: *UI) void {
    assert(self.transform_stack.len >= 2);
    rl.gl.rlPopMatrix();
    _ = self.transform_stack.pop();
}

pub fn beginScissorMode(self: *UI, x: f32, y: f32, width: f32, height: f32) void {
    const frame = self.getTopFrame();

    rl.beginScissorMode(
        @intFromFloat(x * frame.scale.x + frame.offset.x),
        @intFromFloat(y * frame.scale.y + frame.offset.y),
        @intFromFloat(width * frame.scale.x),
        @intFromFloat(height * frame.scale.y),
    );
}

pub fn beginScissorModeRect(self: *UI, rect: rl.Rectangle) void {
    self.beginScissorMode(rect.x, rect.y, rect.width, rect.height);
}

pub fn endScissorMode(self: *UI) void {
    _ = self;
    rl.endScissorMode();
}

pub const Id = struct {
    location: u64,
    extra: u32 = 0,

    pub fn init(comptime src: SourceLocation) Id {
        return Id{ .location = comptime hashSrc(src) };
    }

    pub fn eql(a: Id, b: Id) bool {
        return a.location == b.location and a.extra == b.extra;
    }
};

pub const Stack = struct {
    pub const Direction = enum {
        top_to_bottom,
        bottom_to_top,
        left_to_right
    };

    unused_box: rl.Rectangle,
    dir: Direction,
    gap: f32 = 0,

    pub fn init(box: rl.Rectangle, dir: Direction) Stack {
        return Stack{
            .unused_box = box,
            .dir = dir
        };
    }

    pub fn next(self: *Stack, size: f32) rl.Rectangle {
        return switch (self.dir) {
            .top_to_bottom => {
                const next_box = rl.Rectangle.init(self.unused_box.x, self.unused_box.y, self.unused_box.width, size);
                self.unused_box.y += size;
                self.unused_box.y += self.gap;
                return next_box;
            },
            .bottom_to_top => {
                const next_box = rl.Rectangle.init(self.unused_box.x, self.unused_box.y + self.unused_box.height - size, self.unused_box.width, size);
                self.unused_box.height -= size;
                self.unused_box.height -= self.gap;
                return next_box;
            },
            .left_to_right => {
                const next_box = rl.Rectangle.init(self.unused_box.x, self.unused_box.y, size, self.unused_box.height);
                self.unused_box.x += size;
                self.unused_box.x += self.gap;
                return next_box;
            },
        };
    }
};

pub const IdIterator = struct {
    id: Id,
    counter: u32,

    pub fn init(comptime src: SourceLocation) IdIterator {
        return IdIterator{
            .id = Id.init(src),
            .counter = 0
        };
    }

    pub fn next(self: *IdIterator) Id {
        var id = self.id;
        id.extra = self.counter;

        self.counter += 1;
        return id;
    }
};
