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

const Allocator = std.mem.Allocator;

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

const Text = @This();

text: []const u8,
style: vaxis.Style = .{},
text_align: enum { left, center, right } = .left,
softwrap: bool = true,
overflow: enum { ellipsis, clip } = .ellipsis,
width_basis: enum { parent, longest_line } = .longest_line,

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

fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
    const self: *const Text = @ptrCast(@alignCast(ptr));
    return self.draw(ctx);
}

pub fn draw(self: *const Text, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
    if (ctx.max.width != null and ctx.max.width.? == 0) {
        return .{
            .size = ctx.min,
            .widget = self.widget(),
            .buffer = &.{},
            .children = &.{},
        };
    }
    const container_size = self.findContainerSize(ctx);

    // Create a surface of target width and max height. We'll trim the result after drawing
    const surface = try vxfw.Surface.init(
        ctx.arena,
        self.widget(),
        container_size,
    );
    const base_style: vaxis.Style = .{
        .fg = self.style.fg,
        .bg = self.style.bg,
        .reverse = self.style.reverse,
    };
    const base: vaxis.Cell = .{ .style = base_style };
    @memset(surface.buffer, base);

    var row: u16 = 0;
    if (self.softwrap) {
        var iter = SoftwrapIterator.init(self.text, ctx);
        while (iter.next()) |line| {
            if (row >= container_size.height) break;
            defer row += 1;
            var col: u16 = switch (self.text_align) {
                .left => 0,
                .center => (container_size.width - line.width) / 2,
                .right => container_size.width - line.width,
            };
            var char_iter = ctx.graphemeIterator(line.bytes);
            while (char_iter.next()) |char| {
                const grapheme = char.bytes(line.bytes);
                if (std.mem.eql(u8, grapheme, "\t")) {
                    for (0..8) |i| {
                        surface.writeCell(@intCast(col + i), row, .{
                            .char = .{ .grapheme = " ", .width = 1 },
                            .style = self.style,
                        });
                    }
                    col += 8;
                    continue;
                }
                const grapheme_width: u8 = @intCast(ctx.stringWidth(grapheme));
                surface.writeCell(col, row, .{
                    .char = .{ .grapheme = grapheme, .width = grapheme_width },
                    .style = self.style,
                });
                col += grapheme_width;
            }
        }
    } else {
        var line_iter: LineIterator = .{ .buf = self.text };
        while (line_iter.next()) |line| {
            if (row >= container_size.height) break;
            // \t is default 1 wide. We add 7x the count of tab characters to get the full width
            const line_width = ctx.stringWidth(line) + 7 * std.mem.count(u8, line, "\t");
            defer row += 1;
            const resolved_line_width = @min(container_size.width, line_width);
            var col: u16 = switch (self.text_align) {
                .left => 0,
                .center => (container_size.width - resolved_line_width) / 2,
                .right => container_size.width - resolved_line_width,
            };
            var char_iter = ctx.graphemeIterator(line);
            while (char_iter.next()) |char| {
                if (col >= container_size.width) break;
                const grapheme = char.bytes(line);
                const grapheme_width: u8 = @intCast(ctx.stringWidth(grapheme));

                if (col + grapheme_width >= container_size.width and
                    line_width > container_size.width and
                    self.overflow == .ellipsis)
                {
                    surface.writeCell(col, row, .{
                        .char = .{ .grapheme = "…", .width = 1 },
                        .style = self.style,
                    });
                    col = container_size.width;
                } else {
                    surface.writeCell(col, row, .{
                        .char = .{ .grapheme = grapheme, .width = grapheme_width },
                        .style = self.style,
                    });
                    col += @intCast(grapheme_width);
                }
            }
        }
    }
    return surface.trimHeight(@max(row, ctx.min.height));
}

/// Determines the container size by finding the widest line in the viewable area
fn findContainerSize(self: Text, ctx: vxfw.DrawContext) vxfw.Size {
    var row: u16 = 0;
    var max_width: u16 = ctx.min.width;
    if (self.softwrap) {
        var iter = SoftwrapIterator.init(self.text, ctx);
        while (iter.next()) |line| {
            if (ctx.max.outsideHeight(row))
                break;

            defer row += 1;
            max_width = @max(max_width, line.width);
        }
    } else {
        var line_iter: LineIterator = .{ .buf = self.text };
        while (line_iter.next()) |line| {
            if (ctx.max.outsideHeight(row))
                break;
            const line_width: u16 = @truncate(ctx.stringWidth(line));
            defer row += 1;
            const resolved_line_width = if (ctx.max.width) |max|
                @min(max, line_width)
            else
                line_width;
            max_width = @max(max_width, resolved_line_width);
        }
    }
    const result_width = switch (self.width_basis) {
        .longest_line => blk: {
            if (ctx.max.width) |max|
                break :blk @min(max, max_width)
            else
                break :blk max_width;
        },
        .parent => blk: {
            std.debug.assert(ctx.max.width != null);
            break :blk ctx.max.width.?;
        },
    };
    return .{ .width = result_width, .height = @max(row, ctx.min.height) };
}

/// Iterates a slice of bytes by linebreaks. Lines are split by '\r', '\n', or '\r\n'
pub const LineIterator = struct {
    buf: []const u8,
    index: usize = 0,

    fn next(self: *LineIterator) ?[]const u8 {
        if (self.index >= self.buf.len) return null;

        const start = self.index;
        const end = std.mem.indexOfAnyPos(u8, self.buf, self.index, "\r\n") orelse {
            self.index = self.buf.len;
            return self.buf[start..];
        };

        self.index = end;
        self.consumeCR();
        self.consumeLF();
        return self.buf[start..end];
    }

    // consumes a \n byte
    fn consumeLF(self: *LineIterator) void {
        if (self.index >= self.buf.len) return;
        if (self.buf[self.index] == '\n') self.index += 1;
    }

    // consumes a \r byte
    fn consumeCR(self: *LineIterator) void {
        if (self.index >= self.buf.len) return;
        if (self.buf[self.index] == '\r') self.index += 1;
    }
};

pub const SoftwrapIterator = struct {
    ctx: vxfw.DrawContext,
    line: []const u8 = "",
    index: usize = 0,
    hard_iter: LineIterator,

    pub const Line = struct {
        width: u16,
        bytes: []const u8,
    };

    const soft_breaks = " \t";

    fn init(buf: []const u8, ctx: vxfw.DrawContext) SoftwrapIterator {
        return .{
            .ctx = ctx,
            .hard_iter = .{ .buf = buf },
        };
    }

    fn next(self: *SoftwrapIterator) ?Line {
        // Advance the hard iterator
        if (self.index == self.line.len) {
            self.line = self.hard_iter.next() orelse return null;
            self.line = std.mem.trimEnd(u8, self.line, " \t");
            self.index = 0;
        }

        const start = self.index;
        var cur_width: u16 = 0;
        while (self.index < self.line.len) {
            const idx = self.nextWrap();
            const word = self.line[self.index..idx];
            const next_width = self.ctx.stringWidth(word);

            if (self.ctx.max.width) |max| {
                if (cur_width + next_width > max) {
                    // Trim the word to see if it can fit on a line by itself
                    const trimmed = std.mem.trimEnd(u8, word, " \t");
                    const trimmed_bytes = word.len - trimmed.len;
                    // The number of bytes we trimmed is equal to the reduction in length
                    const trimmed_width = next_width - trimmed_bytes;
                    if (trimmed_width > max) {
                        // Won't fit on line by itself, so fit as much on this line as we can
                        var iter = self.ctx.graphemeIterator(word);
                        while (iter.next()) |item| {
                            const grapheme = item.bytes(word);
                            const w = self.ctx.stringWidth(grapheme);
                            if (cur_width + w > max) {
                                const end = self.index;
                                return .{ .width = cur_width, .bytes = self.line[start..end] };
                            }
                            cur_width += @intCast(w);
                            self.index += grapheme.len;
                        }
                    }
                    // We are softwrapping, advance index to the start of the next word
                    const end = self.index;
                    self.index = std.mem.indexOfNonePos(u8, self.line, self.index, soft_breaks) orelse self.line.len;
                    return .{ .width = cur_width, .bytes = self.line[start..end] };
                }
            }

            self.index = idx;
            cur_width += @intCast(next_width);
        }
        return .{ .width = cur_width, .bytes = self.line[start..] };
    }

    /// Determines the index of the end of the next word
    fn nextWrap(self: *SoftwrapIterator) usize {
        // Find the first linear whitespace char
        const start_pos = std.mem.indexOfNonePos(u8, self.line, self.index, soft_breaks) orelse
            return self.line.len;
        if (std.mem.indexOfAnyPos(u8, self.line, start_pos, soft_breaks)) |idx| {
            return idx;
        }
        return self.line.len;
    }

    // consumes a \n byte
    fn consumeLF(self: *SoftwrapIterator) void {
        if (self.index >= self.buf.len) return;
        if (self.buf[self.index] == '\n') self.index += 1;
    }

    // consumes a \r byte
    fn consumeCR(self: *SoftwrapIterator) void {
        if (self.index >= self.buf.len) return;
        if (self.buf[self.index] == '\r') self.index += 1;
    }
};

test "SoftwrapIterator: LF breaks" {
    vxfw.DrawContext.init(.unicode);
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();

    const ctx: vxfw.DrawContext = .{
        .min = .{ .width = 0, .height = 0 },
        .max = .{ .width = 20, .height = 10 },
        .arena = arena.allocator(),
        .cell_size = .{ .width = 10, .height = 20 },
    };
    var iter = SoftwrapIterator.init("Hello, \n world", ctx);
    const first = iter.next();
    try std.testing.expect(first != null);
    try std.testing.expectEqualStrings("Hello,", first.?.bytes);
    try std.testing.expectEqual(6, first.?.width);

    const second = iter.next();
    try std.testing.expect(second != null);
    try std.testing.expectEqualStrings(" world", second.?.bytes);
    try std.testing.expectEqual(6, second.?.width);

    const end = iter.next();
    try std.testing.expect(end == null);
}

test "SoftwrapIterator: soft breaks that fit" {
    vxfw.DrawContext.init(.unicode);
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();

    const ctx: vxfw.DrawContext = .{
        .min = .{ .width = 0, .height = 0 },
        .max = .{ .width = 6, .height = 10 },
        .arena = arena.allocator(),
        .cell_size = .{ .width = 10, .height = 20 },
    };
    var iter = SoftwrapIterator.init("Hello, \nworld", ctx);
    const first = iter.next();
    try std.testing.expect(first != null);
    try std.testing.expectEqualStrings("Hello,", first.?.bytes);
    try std.testing.expectEqual(6, first.?.width);

    const second = iter.next();
    try std.testing.expect(second != null);
    try std.testing.expectEqualStrings("world", second.?.bytes);
    try std.testing.expectEqual(5, second.?.width);

    const end = iter.next();
    try std.testing.expect(end == null);
}

test "SoftwrapIterator: soft breaks that are longer than width" {
    vxfw.DrawContext.init(.unicode);
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();

    const ctx: vxfw.DrawContext = .{
        .min = .{ .width = 0, .height = 0 },
        .max = .{ .width = 6, .height = 10 },
        .arena = arena.allocator(),
        .cell_size = .{ .width = 10, .height = 20 },
    };
    var iter = SoftwrapIterator.init("very-long-word \nworld", ctx);
    const first = iter.next();
    try std.testing.expect(first != null);
    try std.testing.expectEqualStrings("very-l", first.?.bytes);
    try std.testing.expectEqual(6, first.?.width);

    const second = iter.next();
    try std.testing.expect(second != null);
    try std.testing.expectEqualStrings("ong-wo", second.?.bytes);
    try std.testing.expectEqual(6, second.?.width);

    const third = iter.next();
    try std.testing.expect(third != null);
    try std.testing.expectEqualStrings("rd", third.?.bytes);
    try std.testing.expectEqual(2, third.?.width);

    const fourth = iter.next();
    try std.testing.expect(fourth != null);
    try std.testing.expectEqualStrings("world", fourth.?.bytes);
    try std.testing.expectEqual(5, fourth.?.width);

    const end = iter.next();
    try std.testing.expect(end == null);
}

test "SoftwrapIterator: soft breaks with leading spaces" {
    vxfw.DrawContext.init(.unicode);
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();

    const ctx: vxfw.DrawContext = .{
        .min = .{ .width = 0, .height = 0 },
        .max = .{ .width = 6, .height = 10 },
        .arena = arena.allocator(),
        .cell_size = .{ .width = 10, .height = 20 },
    };
    var iter = SoftwrapIterator.init("Hello,        \n world", ctx);
    const first = iter.next();
    try std.testing.expect(first != null);
    try std.testing.expectEqualStrings("Hello,", first.?.bytes);
    try std.testing.expectEqual(6, first.?.width);

    const second = iter.next();
    try std.testing.expect(second != null);
    try std.testing.expectEqualStrings(" world", second.?.bytes);
    try std.testing.expectEqual(6, second.?.width);

    const end = iter.next();
    try std.testing.expect(end == null);
}

test "LineIterator: LF breaks" {
    const input = "Hello, \n world";
    var iter: LineIterator = .{ .buf = input };
    const first = iter.next();
    try std.testing.expect(first != null);
    try std.testing.expectEqualStrings("Hello, ", first.?);

    const second = iter.next();
    try std.testing.expect(second != null);
    try std.testing.expectEqualStrings(" world", second.?);

    const end = iter.next();
    try std.testing.expect(end == null);
}

test "LineIterator: CR breaks" {
    const input = "Hello, \r world";
    var iter: LineIterator = .{ .buf = input };
    const first = iter.next();
    try std.testing.expect(first != null);
    try std.testing.expectEqualStrings("Hello, ", first.?);

    const second = iter.next();
    try std.testing.expect(second != null);
    try std.testing.expectEqualStrings(" world", second.?);

    const end = iter.next();
    try std.testing.expect(end == null);
}

test "LineIterator: CRLF breaks" {
    const input = "Hello, \r\n world";
    var iter: LineIterator = .{ .buf = input };
    const first = iter.next();
    try std.testing.expect(first != null);
    try std.testing.expectEqualStrings("Hello, ", first.?);

    const second = iter.next();
    try std.testing.expect(second != null);
    try std.testing.expectEqualStrings(" world", second.?);

    const end = iter.next();
    try std.testing.expect(end == null);
}

test "LineIterator: CRLF breaks with empty line" {
    const input = "Hello, \r\n\r\n world";
    var iter: LineIterator = .{ .buf = input };
    const first = iter.next();
    try std.testing.expect(first != null);
    try std.testing.expectEqualStrings("Hello, ", first.?);

    const second = iter.next();
    try std.testing.expect(second != null);
    try std.testing.expectEqualStrings("", second.?);

    const third = iter.next();
    try std.testing.expect(third != null);
    try std.testing.expectEqualStrings(" world", third.?);

    const end = iter.next();
    try std.testing.expect(end == null);
}

test Text {
    var text: Text = .{ .text = "Hello, world" };
    const text_widget = text.widget();

    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    vxfw.DrawContext.init(.unicode);

    // Center expands to the max size. It must therefore have non-null max width and max height.
    // These values are asserted in draw
    const ctx: vxfw.DrawContext = .{
        .arena = arena.allocator(),
        .min = .{},
        .max = .{ .width = 7, .height = 2 },
        .cell_size = .{ .width = 10, .height = 20 },
    };

    {
        // Text softwraps by default
        const surface = try text_widget.draw(ctx);
        try std.testing.expectEqual(@as(vxfw.Size, .{ .width = 6, .height = 2 }), surface.size);
    }

    {
        text.softwrap = false;
        text.overflow = .ellipsis;
        const surface = try text_widget.draw(ctx);
        try std.testing.expectEqual(@as(vxfw.Size, .{ .width = 7, .height = 1 }), surface.size);
        // The last character will be an ellipsis
        try std.testing.expectEqualStrings("…", surface.buffer[surface.buffer.len - 1].char.grapheme);
    }
}

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