const std = @import("std");
const vaxis = @import("vaxis");
const vxfw = vaxis.vxfw;

const Model = struct {
    list: std.ArrayList(vxfw.Text),
    /// Memory owned by .arena
    filtered: std.ArrayList(vxfw.RichText),
    list_view: vxfw.ListView,
    text_field: vxfw.TextField,

    /// Used for filtered RichText Spans and result
    arena: std.heap.ArenaAllocator,
    result: []const u8,

    pub fn init(gpa: std.mem.Allocator) !*Model {
        const model = try gpa.create(Model);
        errdefer gpa.destroy(model);

        model.* = .{
            .list = .empty,
            .filtered = .empty,
            .list_view = .{
                .children = .{
                    .builder = .{
                        .userdata = model,
                        .buildFn = Model.widgetBuilder,
                    },
                },
            },
            .text_field = .{
                .buf = .init(gpa),
                .userdata = model,
                .onChange = Model.onChange,
                .onSubmit = Model.onSubmit,
            },
            .result = "",
            .arena = .init(gpa),
        };

        return model;
    }

    pub fn deinit(self: *Model, gpa: std.mem.Allocator) void {
        self.arena.deinit();
        self.text_field.deinit();
        self.list.deinit(gpa);
        gpa.destroy(self);
    }

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

    fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
        const self: *Model = @ptrCast(@alignCast(ptr));
        switch (event) {
            .init => {
                // Initialize the filtered list
                const arena = self.arena.allocator();
                for (self.list.items) |line| {
                    var spans: std.ArrayList(vxfw.RichText.TextSpan) = .empty;
                    const span: vxfw.RichText.TextSpan = .{ .text = line.text };
                    try spans.append(arena, span);
                    try self.filtered.append(arena, .{ .text = spans.items });
                }

                return ctx.requestFocus(self.text_field.widget());
            },
            .key_press => |key| {
                if (key.matches('c', .{ .ctrl = true })) {
                    ctx.quit = true;
                    return;
                }
                return self.list_view.handleEvent(ctx, event);
            },
            .focus_in => {
                return ctx.requestFocus(self.text_field.widget());
            },
            else => {},
        }
    }

    fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface {
        const self: *Model = @ptrCast(@alignCast(ptr));
        const max = ctx.max.size();

        const list_view: vxfw.SubSurface = .{
            .origin = .{ .row = 2, .col = 0 },
            .surface = try self.list_view.draw(ctx.withConstraints(
                ctx.min,
                .{ .width = max.width, .height = max.height - 3 },
            )),
        };

        const text_field: vxfw.SubSurface = .{
            .origin = .{ .row = 0, .col = 2 },
            .surface = try self.text_field.draw(ctx.withConstraints(
                ctx.min,
                .{ .width = max.width, .height = 1 },
            )),
        };

        const prompt: vxfw.Text = .{ .text = "", .style = .{ .fg = .{ .index = 4 } } };

        const prompt_surface: vxfw.SubSurface = .{
            .origin = .{ .row = 0, .col = 0 },
            .surface = try prompt.draw(ctx.withConstraints(ctx.min, .{ .width = 2, .height = 1 })),
        };

        const children = try ctx.arena.alloc(vxfw.SubSurface, 3);
        children[0] = list_view;
        children[1] = text_field;
        children[2] = prompt_surface;

        return .{
            .size = max,
            .widget = self.widget(),
            .buffer = &.{},
            .children = children,
        };
    }

    fn widgetBuilder(ptr: *const anyopaque, idx: usize, _: usize) ?vxfw.Widget {
        const self: *const Model = @ptrCast(@alignCast(ptr));
        if (idx >= self.filtered.items.len) return null;

        return self.filtered.items[idx].widget();
    }

    fn onChange(maybe_ptr: ?*anyopaque, _: *vxfw.EventContext, str: []const u8) anyerror!void {
        const ptr = maybe_ptr orelse return;
        const self: *Model = @ptrCast(@alignCast(ptr));
        const arena = self.arena.allocator();
        self.filtered.clearAndFree(arena);
        _ = self.arena.reset(.free_all);

        const hasUpper = for (str) |b| {
            if (std.ascii.isUpper(b)) break true;
        } else false;

        // Loop each line
        // If our input is only lowercase, we convert the line to lowercase
        // Iterate the input graphemes, looking for them _in order_ in the line
        outer: for (self.list.items) |item| {
            const tgt = if (hasUpper)
                item.text
            else
                try toLower(arena, item.text);

            var spans = std.ArrayList(vxfw.RichText.TextSpan).empty;
            var i: usize = 0;
            var iter = vaxis.unicode.graphemeIterator(str);
            while (iter.next()) |g| {
                if (std.mem.indexOfPos(u8, tgt, i, g.bytes(str))) |idx| {
                    const up_to_here: vxfw.RichText.TextSpan = .{ .text = item.text[i..idx] };
                    const match: vxfw.RichText.TextSpan = .{
                        .text = item.text[idx .. idx + g.len],
                        .style = .{ .fg = .{ .index = 4 }, .reverse = true },
                    };
                    try spans.append(arena, up_to_here);
                    try spans.append(arena, match);
                    i = idx + g.len;
                } else continue :outer;
            }
            const up_to_here: vxfw.RichText.TextSpan = .{ .text = item.text[i..] };
            try spans.append(arena, up_to_here);
            try self.filtered.append(arena, .{ .text = spans.items });
        }
        self.list_view.scroll.top = 0;
        self.list_view.scroll.offset = 0;
        self.list_view.cursor = 0;
    }

    fn onSubmit(maybe_ptr: ?*anyopaque, ctx: *vxfw.EventContext, _: []const u8) anyerror!void {
        const ptr = maybe_ptr orelse return;
        const self: *Model = @ptrCast(@alignCast(ptr));
        if (self.list_view.cursor < self.filtered.items.len) {
            const selected = self.filtered.items[self.list_view.cursor];
            const arena = self.arena.allocator();
            var result = std.ArrayList(u8).empty;
            for (selected.text) |span| {
                try result.appendSlice(arena, span.text);
            }
            self.result = result.items;
        }
        ctx.quit = true;
    }
};

fn toLower(arena: std.mem.Allocator, src: []const u8) std.mem.Allocator.Error![]const u8 {
    const lower = try arena.alloc(u8, src.len);
    for (src, 0..) |b, i| {
        lower[i] = std.ascii.toLower(b);
    }
    return lower;
}

pub fn main(init: std.process.Init) !u8 {
    const io = init.io;
    const alloc = init.gpa;

    const model = try Model.init(alloc);
    defer model.deinit(alloc);

    // Run the command
    const fd = try std.process.run(alloc, io, .{
        .argv = &.{"fd"},
    });
    defer alloc.free(fd.stdout);
    defer alloc.free(fd.stderr);

    var iter = std.mem.splitScalar(u8, fd.stdout, '\n');
    while (iter.next()) |line| {
        if (line.len == 0) continue;
        try model.list.append(alloc, .{ .text = line });
    }

    {
        var buffer: [1024]u8 = undefined;
        var app: vxfw.App = try .init(io, alloc, init.environ_map, &buffer);
        defer app.deinit();

        try app.run(model.widget(), .{});
    }

    if (model.result.len > 0) {
        var stdout_file: std.Io.File = .stdout();
        var stdout_buffer: [1024]u8 = undefined;
        var stdout_writer = stdout_file.writer(io, &stdout_buffer);
        const stdout = &stdout_writer.interface;
        try stdout.writeAll(model.result);
        try stdout.writeByte('\n');
        try stdout.flush();
        return 0;
    } else {
        return 130;
    }
}

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