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

const Event = dvui.Event;
const Options = dvui.Options;
const Rect = dvui.Rect;
const Size = dvui.Size;
const WidgetData = dvui.WidgetData;

const LabelWidget = @This();

pub var defaults: Options = .{
    .name = "Label",
    .role = .label,
    .padding = Rect.all(6),
};

pub const InitOptions = struct {
    align_x: f32 = 0,
    align_y: f32 = 0,

    /// Replace the end of text with "..." if it would be truncated.  Ignored
    /// if text is rotated (for now).
    ellipsize: bool = true,

    sel_start: ?usize = null,
    sel_end: ?usize = null,
    sel_color: ?dvui.Color = null,

    pub fn gravityGet(self: InitOptions) Options.Gravity {
        return .{ .x = self.align_x, .y = self.align_y };
    }
};

wd: WidgetData,
label_str: []const u8,
label_size: Size,
/// An allocator to free `label_str` on `deinit`
allocator: ?std.mem.Allocator,
init_options: InitOptions,
ellipsized: bool,

/// It's expected to call this when `self` is `undefined`
pub fn init(self: *LabelWidget, src: std.builtin.SourceLocation, comptime fmt: []const u8, args: anytype, init_opts: InitOptions, opts: Options) void {
    comptime if (!std.unicode.utf8ValidateSlice(fmt)) @compileError("Format strings must be valid utf-8");

    const cw = dvui.currentWindow();
    // Validate utf8 formatting
    const str, const alloc = blk: {
        const str = std.fmt.allocPrint(cw.lifo(), fmt, args) catch |err| {
            const newid = dvui.parentGet().extendId(src, opts.idExtra());
            dvui.logError(@src(), err, "Label {x} could not print its content", .{newid});
            break :blk .{ fmt, null };
        };
        // We need to use `long_term_arena` because otherwise we
        // will not be able to free the memory of the allocPrint
        const utf8 = dvui.toUtf8(cw.arena(), str) catch |err| {
            const newid = dvui.parentGet().extendId(src, opts.idExtra());
            dvui.logError(@src(), err, "Label {x} could not allocate valid utf8 slice", .{newid});
            // We contained invalid utf8, so textSize will fail later
            break :blk .{ str, cw.lifo() };
        };
        if (str.ptr == utf8.ptr) break :blk .{ str, cw.lifo() };
        cw.lifo().free(str);
        dvui.log.debug("LabelWidget format output was invalid utf8 for {s} with '{any}'.", .{ fmt, args });
        break :blk .{ utf8, null };
    };
    return self.initNoFmtAllocator(src, str, alloc, init_opts, opts);
}

/// It's expected to call this when `self` is `undefined`
pub fn initNoFmt(self: *LabelWidget, src: std.builtin.SourceLocation, label_str: []const u8, init_opts: InitOptions, opts: Options) void {
    const arena = dvui.currentWindow().lifo();
    // If the allocation fails, the textSize will be incorrect
    // later because of invalid utf8
    const str = dvui.toUtf8(arena, label_str) catch |err| blk: {
        const newid = dvui.parentGet().extendId(src, opts.idExtra());
        dvui.logError(@src(), err, "Label {x} could not allocate valid utf8 slice", .{newid});
        break :blk label_str;
    };
    return self.initNoFmtAllocator(src, str, if (str.ptr != label_str.ptr) arena else null, init_opts, opts);
}

/// The `allocator` argument will be used to deallocator `label_str` on
/// when `deinit` is called.
///
/// Assumes the label_str is valid utf8
///
/// It's expected to call this when `self` is `undefined`
pub fn initNoFmtAllocator(self: *LabelWidget, src: std.builtin.SourceLocation, label_str: []const u8, allocator: ?std.mem.Allocator, init_opts: InitOptions, opts: Options) void {
    const options = defaults.override(opts);
    const lsize = options.fontGet().textSize(label_str);
    var size = lsize;
    if (opts.rotationGet() != 0.0) {
        var t: dvui.Triangles = .{ .vertexes = &.{}, .indices = &.{}, .bounds = .{ .w = size.w, .h = size.h } };

        t.rotate(.{}, opts.rotationGet());
        size.w = t.bounds.w;
        size.h = t.bounds.h;
    }
    size = Size.max(size, options.min_size_contentGet());
    self.* = .{
        .wd = .init(src, .{}, options.override(.{ .min_size_content = size })),
        .init_options = init_opts,
        .label_str = label_str,
        .label_size = lsize,
        .allocator = allocator,
        .ellipsized = false,
    };

    self.data().register();
    self.data().borderAndBackground(.{});

    if (self.data().accesskit_node()) |ak_node| {
        dvui.AccessKit.nodeSetValueWithLength(ak_node, self.label_str.ptr, self.label_str.len);
        if (self.data().options.label == null) {
            dvui.AccessKit.nodeSetLabelWithLength(ak_node, self.label_str.ptr, self.label_str.len);
        }
    }
}

pub fn data(self: *LabelWidget) *WidgetData {
    return self.wd.validate();
}

pub fn draw(self: *LabelWidget) void {
    const rot = self.data().options.rotationGet();
    const label_gravity = self.init_options.gravityGet();
    const rect = dvui.placeIn(self.data().contentRect(), self.data().options.min_size_contentGet(), .none, label_gravity);
    const rs = self.data().parent.screenRectScale(rect);
    const sel_start = self.init_options.sel_start;
    const sel_end = self.init_options.sel_end;
    const sel_color = self.init_options.sel_color;
    const oldclip = if (rot == 0.0) dvui.clip(rs.r) else dvui.clipGet();
    var iter = std.mem.splitScalar(u8, self.label_str, '\n');
    var line_height_adj: ?f32 = null;
    var first: bool = true;
    var r = rs.r;
    var line_start: usize = 0;
    while (iter.next()) |line_slice| {
        r.x = rs.r.x;
        if (first) {
            first = false;
        } else {
            if (line_height_adj == null) {
                line_height_adj = self.data().options.fontGet().textHeight() * (self.data().options.fontGet().line_height_factor - 1.0);
            }
            r.y += rs.s * line_height_adj.?;
        }

        var line = line_slice;
        var tsize = if (line.len == self.label_str.len) self.label_size else self.data().options.fontGet().textSize(line);

        // this is only about horizontal direction
        var lineRect = dvui.placeIn(self.data().contentRect(), tsize, .none, label_gravity);

        const ellip = "...";
        // give ourselves a fraction of a pixel extra for floating point innacurracies:
        // - a lot of times the content Rect is sized based on the text width
        if (rot == 0.0 and self.init_options.ellipsize and tsize.w > (self.data().contentRect().w + 0.001)) {
            self.ellipsized = true;
            const esize = self.data().options.fontGet().textSize(ellip);
            var endi: usize = 0;
            tsize = self.data().options.fontGet().textSizeEx(line, .{ .max_width = self.data().contentRect().w - esize.w, .end_idx = &endi });
            line = line[0..endi];
            lineRect = dvui.placeIn(self.data().contentRect(), tsize, .none, .{ .x = 0, .y = 0 });
        }

        const liners = self.data().parent.screenRectScale(lineRect);
        r.x = liners.r.x;

        var render_p = r.topLeft();
        if (rot != 0.0) {
            const cos = @cos(rot);
            const sin = @sin(rot);

            // place label size in center and rotate
            const origin = self.data().parent.screenRectScale(rect).r.center();
            //origin.stroke(.{ .after = true, .thickness = 3.0, .color = .blue });

            //const label_size_rect: Rect.Physical = .{ .x = origin.x, .y = origin.y, .w = self.label_size.w * rs.s, .h = self.label_size.h * rs.s };
            //label_size_rect.stroke(.all(0), .{ .after = true, .thickness = 1.0, .color = .green });

            var d = (dvui.Point{ .x = -self.label_size.w / 2, .y = -self.label_size.h / 2 }).scale(rs.s, dvui.Point);
            d.x += (self.label_size.w - tsize.w) * label_gravity.x * rs.s;
            d.y += r.y - rs.r.y;

            // rotate vector
            const rotated: dvui.Point.Physical = .{
                .x = d.x * cos - d.y * sin,
                .y = d.x * sin + d.y * cos,
            };

            const xy = origin.plus(rotated);
            render_p.x = xy.x;
            render_p.y = xy.y;

            //r.stroke(.all(0), .{ .after = true, .thickness = 1.0, .color = .red });
        }

        dvui.renderText(.{
            .font = self.data().options.fontGet(),
            .text = line,
            .p = render_p,
            .rs = rs,
            .color = self.data().options.color(.text),
            .rotation = rot,
            .sel_start = if (sel_start) |ss| ss -| line_start else null,
            .sel_end = if (sel_end) |se| se -| line_start else null,
            .sel_color = sel_color,
        }) catch |err| {
            dvui.logError(@src(), err, "Failed to render text: {s}", .{line});
        };

        if (self.ellipsized) {
            r.x += liners.r.w;
            dvui.renderText(.{
                .font = self.data().options.fontGet(),
                .text = ellip,
                .rs = .{ .r = r, .s = rs.s },
                .color = self.data().options.color(.text),
            }) catch |err| {
                dvui.logError(@src(), err, "Failed to render ellipses after text: {s}", .{line});
            };
        }

        r.y += rs.s * tsize.h;
        line_start += line_slice.len + 1; // account for newline
    }
    dvui.clipSet(oldclip);
}

pub fn matchEvent(self: *LabelWidget, e: *Event) bool {
    return dvui.eventMatchSimple(e, self.data());
}

pub fn deinit(self: *LabelWidget) void {
    defer if (dvui.widgetIsAllocated(self)) dvui.widgetFree(self);
    defer self.* = undefined;
    if (self.allocator) |alloc| alloc.free(self.label_str);
    self.data().minSizeSetAndRefresh();
    self.data().minSizeReportToParent();
}

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