const std = @import("std");
const vaxis = @import("../main.zig");

const Allocator = std.mem.Allocator;

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

const SplitView = @This();

lhs: vxfw.Widget,
rhs: vxfw.Widget,
constrain: enum { lhs, rhs } = .lhs,
style: vaxis.Style = .{},
/// min width for the constrained side
min_width: u16 = 0,
/// max width for the constrained side
max_width: ?u16 = null,
/// Target width to draw at
width: u16,

/// Used to calculate mouse events when our constraint is rhs
last_max_width: ?u16 = null,

// State
pressed: bool = false,
mouse_set: bool = false,

pub fn widget(self: *const SplitView) vxfw.Widget {
    return .{
        .userdata = @constCast(self),
        .eventHandler = typeErasedEventHandler,
        .drawFn = typeErasedDrawFn,
    };
}

fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
    const self: *SplitView = @ptrCast(@alignCast(ptr));
    switch (event) {
        .mouse_leave => {
            self.pressed = false;
            return;
        },
        .mouse => {},
        else => return,
    }
    const mouse = event.mouse;

    const separator_col: u16 = switch (self.constrain) {
        .lhs => self.width,
        .rhs => if (self.last_max_width) |max|
            max -| self.width -| 1
        else {
            ctx.redraw = true;
            return;
        },
    };

    // If we are on the separator, we always set the mouse shape
    if (mouse.col == separator_col) {
        try ctx.setMouseShape(.@"ew-resize");
        self.mouse_set = true;
        // Set pressed state if we are a left click
        if (mouse.type == .press and mouse.button == .left) {
            self.pressed = true;
        }
    } else if (self.mouse_set) {
        // If we have set the mouse state and *aren't* over the separator, default the mouse state
        try ctx.setMouseShape(.default);
        self.mouse_set = false;
    }

    // On release, we reset state
    if (mouse.type == .release) {
        self.pressed = false;
        self.mouse_set = false;
        try ctx.setMouseShape(.default);
    }

    // If pressed, we always keep the mouse shape and we update the width
    if (self.pressed) {
        try ctx.setMouseShape(.@"ew-resize");
        switch (self.constrain) {
            .lhs => {
                self.width = @max(self.min_width, mouse.col);
                if (self.max_width) |max| {
                    self.width = @min(self.width, max);
                }
            },
            .rhs => {
                const last_max = self.last_max_width orelse return;
                const mouse_col: u16 = if (mouse.col < 0) 0 else @intCast(mouse.col);
                self.width = @min(last_max -| self.min_width, last_max -| mouse_col -| 1);
                if (self.max_width) |max| {
                    self.width = @max(self.width, max);
                }
            },
        }
        ctx.consume_event = true;
    }
}

fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
    const self: *SplitView = @ptrCast(@alignCast(ptr));
    // Fills entire space
    const max = ctx.max.size();
    // Constrain width to the max
    self.width = @min(self.width, max.width);
    self.last_max_width = max.width;

    // The constrained side is equal to the width
    const constrained_min: vxfw.Size = .{ .width = self.width, .height = max.height };
    const constrained_max = vxfw.MaxSize.fromSize(constrained_min);

    const unconstrained_min: vxfw.Size = .{ .width = max.width -| self.width -| 1, .height = max.height };
    const unconstrained_max = vxfw.MaxSize.fromSize(unconstrained_min);

    var children = try std.ArrayList(vxfw.SubSurface).initCapacity(ctx.arena, 2);

    switch (self.constrain) {
        .lhs => {
            if (constrained_max.width.? > 0 and constrained_max.height.? > 0) {
                const lhs_ctx = ctx.withConstraints(constrained_min, constrained_max);
                const lhs_surface = try self.lhs.draw(lhs_ctx);
                children.appendAssumeCapacity(.{
                    .surface = lhs_surface,
                    .origin = .{ .row = 0, .col = 0 },
                });
            }
            if (unconstrained_max.width.? > 0 and unconstrained_max.height.? > 0) {
                const rhs_ctx = ctx.withConstraints(unconstrained_min, unconstrained_max);
                const rhs_surface = try self.rhs.draw(rhs_ctx);
                children.appendAssumeCapacity(.{
                    .surface = rhs_surface,
                    .origin = .{ .row = 0, .col = self.width + 1 },
                });
            }
            var surface = try vxfw.Surface.initWithChildren(
                ctx.arena,
                self.widget(),
                max,
                children.items,
            );
            for (0..max.height) |row| {
                surface.writeCell(self.width, @intCast(row), .{
                    .char = .{ .grapheme = "│", .width = 1 },
                    .style = self.style,
                });
            }
            return surface;
        },
        .rhs => {
            if (unconstrained_max.width.? > 0 and unconstrained_max.height.? > 0) {
                const lhs_ctx = ctx.withConstraints(unconstrained_min, unconstrained_max);
                const lhs_surface = try self.lhs.draw(lhs_ctx);
                children.appendAssumeCapacity(.{
                    .surface = lhs_surface,
                    .origin = .{ .row = 0, .col = 0 },
                });
            }
            if (constrained_max.width.? > 0 and constrained_max.height.? > 0) {
                const rhs_ctx = ctx.withConstraints(constrained_min, constrained_max);
                const rhs_surface = try self.rhs.draw(rhs_ctx);
                children.appendAssumeCapacity(.{
                    .surface = rhs_surface,
                    .origin = .{ .row = 0, .col = unconstrained_max.width.? + 1 },
                });
            }
            var surface = try vxfw.Surface.initWithChildren(
                ctx.arena,
                self.widget(),
                max,
                children.items,
            );
            for (0..max.height) |row| {
                surface.writeCell(max.width -| self.width -| 1, @intCast(row), .{
                    .char = .{ .grapheme = "│", .width = 1 },
                    .style = self.style,
                });
            }
            return surface;
        },
    }
}

test SplitView {
    // Boiler plate draw context
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    vxfw.DrawContext.init(.unicode);

    const draw_ctx: vxfw.DrawContext = .{
        .arena = arena.allocator(),
        .min = .{},
        .max = .{ .width = 16, .height = 16 },
        .cell_size = .{ .width = 10, .height = 20 },
    };

    // Create LHS and RHS widgets
    const lhs: vxfw.Text = .{ .text = "Left hand side" };
    const rhs: vxfw.Text = .{ .text = "Right hand side" };

    var split_view: SplitView = .{
        .lhs = lhs.widget(),
        .rhs = rhs.widget(),
        .width = 8,
    };

    const split_widget = split_view.widget();
    {
        const surface = try split_widget.draw(draw_ctx);
        // SplitView expands to fill the space
        try std.testing.expectEqual(@as(vxfw.Size, .{ .width = 16, .height = 16 }), surface.size);
        // It has two children
        try std.testing.expectEqual(2, surface.children.len);
        // The left child should have a width = SplitView.width
        try std.testing.expectEqual(split_view.width, surface.children[0].surface.size.width);
    }

    // Send the widget a mouse press on the separator
    var mouse: vaxis.Mouse = .{
        // The separator is at width
        .col = @intCast(split_view.width),
        .row = 0,
        .type = .press,
        .button = .left,
        .mods = .{},
    };

    var ctx: vxfw.EventContext = .{
        .io = std.testing.io,
        .alloc = arena.allocator(),
        .cmds = .empty,
    };
    try split_widget.handleEvent(&ctx, .{ .mouse = mouse });
    // We should get a command to change the mouse shape
    try std.testing.expect(ctx.cmds.items[0] == .set_mouse_shape);
    try std.testing.expect(ctx.redraw);
    try std.testing.expect(split_view.pressed);

    // If we move the mouse, we should update the width
    mouse.col = 2;
    mouse.type = .drag;
    try split_widget.handleEvent(&ctx, .{ .mouse = mouse });
    try std.testing.expect(ctx.redraw);
    try std.testing.expect(split_view.pressed);
    const mouse_col: u16 = if (mouse.col < 0) 0 else @intCast(mouse.col);
    try std.testing.expectEqual(mouse_col, split_view.width);
}

test "refAllDecls" {
    std.testing.refAllDecls(@This());
}
