const std    = @import("std");
const rl     = @import("c.zig").rl;
const kb     = @import("kb");
const ft     = @import("freetype");
const rp     = @import("stb_rect_pack");
const tracy = @import("tracy");

pub var ft_lib : ft.Library = undefined;
pub var shader : rl.Shader = undefined;

const ATLAS_SIZE = 4096;

const Glyph = struct {
    index : u32,

    x : i16,
    y : i16,
    width      : i32,
    height     : i32,
    dpi_width  : f32,
    dpi_height : f32,

    bearing_x : i32,
    bearing_y : i32,
    dpi_bearing_x : f32,
    dpi_bearing_y : f32,

    descent : i32,
    ascent : i32,
    dpi_descent : f32,
    dpi_ascent : f32,

    st0 : rl.Vector2,
    st1 : rl.Vector2,
};

pub const Font = struct {
    face : ft.Face,
    glyphs : std.AutoHashMap(u32, Glyph),
    kbts_font : kb.kbts_font,
    texture : rl.Texture2D,
    kb_file_data : []u8,

    pub fn init(filename : [:0]const u8, size : i32, allocator: std.mem.Allocator) !Font {

        const file_data = try std.fs.cwd().readFileAlloc(
            allocator,
            filename,
            10 * 1024 * 1024
        );
        defer allocator.free(file_data);

        const kb_file_data = try allocator.dupe(u8, file_data);
        errdefer allocator.free(kb_file_data);

        var face = try ft_lib.createFaceMemory(file_data, 0);
        var glyphs = std.AutoHashMap(u32, Glyph).init(allocator);
        try face.setCharSize(0, size * 64, 0, 96 * @as(u16, @intFromFloat(rl.GetWindowScaleDPI().y)));
        try face.selectCharmap(.unicode);

        var rects = try std.ArrayList(rp.stbrp_rect).initCapacity(allocator, 1024);
        defer rects.deinit(allocator);

        var nodes : [ATLAS_SIZE]rp.stbrp_node = std.mem.zeroes([ATLAS_SIZE]rp.stbrp_node);

        var stbrpcontext : rp.stbrp_context = .{};
        rp.stbrp_init_target(&stbrpcontext, ATLAS_SIZE, ATLAS_SIZE, &nodes, nodes.len);

        const _atlas : []u8 = try allocator.alloc(u8, ATLAS_SIZE * ATLAS_SIZE);
        defer allocator.free(_atlas);
        @memset(_atlas, 0);

        const atlas : rl.Image = .{
            .data = _atlas.ptr,
            .width = ATLAS_SIZE,
            .height = ATLAS_SIZE,
            .mipmaps = 1,
            .format = rl.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE,
        };

        for (0..face.numGlyphs()) |index| {
            try face.loadGlyph(@intCast(index), .{});
            const bmp = face.glyph().bitmap();

            try rects.append(allocator, .{
                .id = @intCast(index),
                .w = @max(1, @as(c_int, @intCast(bmp.width()))),
                .h = @max(1, @as(c_int, @intCast(bmp.rows()))),
                .x = 0,
                .y = 0,
                .was_packed = 0,
            });
        }

        _ = rp.stbrp_pack_rects(&stbrpcontext, rects.items.ptr, @intCast(rects.items.len));

        for (rects.items) |rect| {
            const index : u32 = @intCast(rect.id);
            if (rect.was_packed == 0) continue;
            try face.loadGlyph(index, .{ .render = true });

            const bmp = face.glyph().bitmap();

            const pitch: usize = @intCast(bmp.pitch());
            const w: usize = @intCast(bmp.width());
            const h: usize = @intCast(bmp.rows());

            const dst_x: usize = @intCast(rect.x);
            const dst_y: usize = @intCast(rect.y);

            if (bmp.buffer()) |buf| {
                for (0..h) |y| {
                    const src = buf[y * pitch .. y * pitch + w];

                    const dst_index = (dst_y + y) * ATLAS_SIZE + dst_x;
                    const dst = _atlas[dst_index .. dst_index + w];

                    @memcpy(dst, src);
                }
            }

            const width         : i32 = @intCast(bmp.width());
            const height        : i32 = @intCast(bmp.rows());
            const dpi_width     : f32 = @as(f32, @floatFromInt(width)) / rl.GetWindowScaleDPI().x;
            const dpi_height    : f32 = @as(f32, @floatFromInt(height)) / rl.GetWindowScaleDPI().y;
            const bearing_x     : i32 = face.glyph().bitmapLeft();
            const bearing_y     : i32 = face.glyph().bitmapTop();
            const dpi_bearing_x : f32 = @as(f32, @floatFromInt(bearing_x)) / rl.GetWindowScaleDPI().x;
            const dpi_bearing_y : f32 = @as(f32, @floatFromInt(bearing_y)) / rl.GetWindowScaleDPI().y;
            const descent       : i32 = height - bearing_y;
            const ascent        : i32 = height - descent;
            const dpi_descent   : f32 = dpi_height - dpi_bearing_y;
            const dpi_ascent    : f32 = dpi_height - dpi_descent;

            const x : i16 = @intCast(rect.x);
            const y : i16 = @as(i16, @intCast(rect.y)) + @as(i16, @intCast(height));

            const fx = @as(f32, @floatFromInt(x));
            const fy = @as(f32, @floatFromInt(y));
            const fw = @as(f32, @floatFromInt(width));
            const fh = @as(f32, @floatFromInt(height));
            const fs = @as(f32, ATLAS_SIZE);

            const glyph = Glyph{
                .x = x,
                .y = y,
                .index = index,
                .bearing_x = bearing_x,
                .bearing_y = bearing_y,
                .dpi_bearing_x = dpi_bearing_x,
                .dpi_bearing_y = dpi_bearing_y,
                .width = width,
                .height = height,
                .dpi_width = dpi_width,
                .dpi_height = dpi_height,
                .descent = descent,
                .ascent = ascent,
                .dpi_descent = dpi_descent,
                .dpi_ascent = dpi_ascent,
                .st0 = .{.x = (fx + 0.5) / fs, .y = (fy - fh + 0.5) / fs},
                .st1 = .{.x = (fx + fw - 0.5) / fs,  .y = (fy - 0.5) / fs},
            };

            try glyphs.put(glyph.index, glyph);
        }

        const texture = rl.LoadTextureFromImage(atlas);

        var kbts_font = kb.kbts_FontFromMemory(kb_file_data.ptr, @intCast(kb_file_data.len), 0, null, null);

        if (kb.kbts_FontIsValid(&kbts_font) == 0) {
            std.log.info("[Error] [kb_text_shape] Failed to load the font.", .{});
        }

        return .{
            .face = face,
            .glyphs = glyphs,
            .kbts_font = kbts_font,
            .texture = texture,
            .kb_file_data = kb_file_data,
        };
    }

    pub fn deinit(self: *Font, allocator : std.mem.Allocator) void {
        self.glyphs.deinit();
        self.face.deinit();
        kb.kbts_FreeFont(&self.kbts_font);
        rl.UnloadTexture(self.texture);
        allocator.free(self.kb_file_data);
    }

    pub fn render_text(
        self: *Font,
        text: []const u8,
        pos: rl.Vector2,
        window_space: bool,
        colour: rl.Color,
        background: rl.Color,
        nice_background: bool,
        count_descent: bool
    ) void {
        if (text.len == 0)
            return;

        const zone = tracy.ZoneNC(@src(), "render_text", 0x00_ff_00_00);
        defer zone.End();

        rl.BeginShaderMode(shader);
        rl.rlSetTexture(self.texture.id);
        rl.rlBegin(rl.RL_QUADS);

        if (nice_background) {
            self.render_text(
                text,
                rl.Vector2Add(pos, .{.x = 3.0, .y = -3.0}),
                window_space,
                rl.Color{ .r = 0, .g = 0, .b = 0, .a = 255 },
                rl.Color{ .r = 0, .g = 0, .b = 0, .a = 0 },
                false,
                false
            );
        }

        var render_pos = pos;
        _ = count_descent;
        _ = background;

        const font_ascent : f32 = @floatFromInt(self.face.size().metrics().ascender >> 6);
        const dpi_font_ascent = font_ascent / rl.GetWindowScaleDPI().y;

        render_pos.y += dpi_font_ascent;

        const Context = kb.kbts_CreateShapeContext(null, null);
        defer kb.kbts_DestroyShapeContext(Context);
        const kb_font = kb.kbts_ShapePushFont(Context, &self.kbts_font);
        if (kb_font == null) {
            std.log.info("Could not open font!", .{});
            return;
        }

        const zone2 = tracy.ZoneNC(@src(), "ShapeUtf8", 0x00_ff_00_00);
        kb.kbts_ShapeBegin(Context, kb.KBTS_DIRECTION_DONT_KNOW, kb.KBTS_LANGUAGE_DONT_KNOW);
        kb.kbts_ShapeUtf8(Context, text.ptr, @intCast(text.len), kb.KBTS_USER_ID_GENERATION_MODE_CODEPOINT_INDEX);
        kb.kbts_ShapeEnd(Context);
        zone2.End();

        var cursor_x : f32 = 0;
        var cursor_y : f32 = 0;

        var Run = kb.kbts_run{};
        while (my_kbts_ShapeRun(Context, &Run) != 0) {
            const zone3 = tracy.ZoneNC(@src(), "shape run", 0x00_ff_00_00);
            defer zone3.End();
            if ((Run.Flags & kb.KBTS_BREAK_FLAG_LINE_HARD) != 0) {
                const font_linegap : f32 = @floatFromInt(self.face.size().metrics().height >> 6);
                const dpi_font_linegap = font_linegap / rl.GetWindowScaleDPI().y;

                cursor_y += dpi_font_linegap;
                cursor_x = 0;
            }

            var g : [*c]kb.kbts_glyph = null;
            while (kb.kbts_GlyphIteratorNext(&Run.Glyphs, &g) != 0) {
                const zone4 = tracy.ZoneNC(@src(), "glyph iterator", 0x00_ff_00_00);
                defer zone4.End();
                const RunGlyph : *kb.kbts_glyph = g.?;
                const CodepointIndex : i32 = RunGlyph.UserIdOrCodepointIndex;
                var ShapeCodepoint : kb.kbts_shape_codepoint = undefined;
                if (kb.kbts_ShapeGetShapeCodepoint(Context, CodepointIndex, &ShapeCodepoint) == 0) {
                    std.log.info("aah it's wrong!! idk how to handle the error rn!!", .{});
                }

                const advance_x : f32 = @floatFromInt(ft.mulFix(RunGlyph.AdvanceX, @intCast(self.face.size().metrics().x_scale)) >> 6);
                const advance_y : f32 = @floatFromInt(ft.mulFix(RunGlyph.AdvanceY, @intCast(self.face.size().metrics().y_scale)) >> 6);
                const dpi_advance_x = advance_x / rl.GetWindowScaleDPI().x;
                const dpi_advance_y = advance_y / rl.GetWindowScaleDPI().y;

                const offset_x : f32 = @floatFromInt(ft.mulFix(RunGlyph.OffsetX, @intCast(self.face.size().metrics().x_scale)) >> 6);
                const offset_y : f32 = @floatFromInt(ft.mulFix(RunGlyph.OffsetY, @intCast(self.face.size().metrics().y_scale)) >> 6);
                const dpi_offset_x = offset_x / rl.GetWindowScaleDPI().x;
                const dpi_offset_y = offset_y / rl.GetWindowScaleDPI().y;

                if (self.glyphs.getPtr(RunGlyph.Id)) |glyph| {

                    var v0 = rl.Vector2{};
                    var v1 = rl.Vector2{};
                    
                    v0 = rl.Vector2Add(render_pos, .{ .x = cursor_x + glyph.dpi_bearing_x + dpi_offset_x,
                                           .y = cursor_y - glyph.dpi_bearing_y - dpi_offset_y });

                    v1 = rl.Vector2Add(v0, rl.Vector2{ .x = glyph.dpi_width, .y = glyph.dpi_height });
                    const p0 : rl.Vector4 = .{ .x = v0.x, .y = v0.y, .z = 0.0, .w = 1.0 };
                    const p1 : rl.Vector4 = .{ .x = v1.x, .y = v1.y, .z = 0.0, .w = 1.0 };
                    
                    cursor_x += dpi_advance_x;
                    cursor_y += dpi_advance_y;

                    const st0 = glyph.st0;
                    const st1 = glyph.st1;

                    rl.rlColor4ub(colour.r, colour.g, colour.b, colour.a);
                    rl.rlNormal3f(0.0, 0.0, 1.0);

                    rl.rlTexCoord2f(st0.x, st0.y); rl.rlVertex2f(p0.x, p0.y);
                    rl.rlTexCoord2f(st0.x, st1.y); rl.rlVertex2f(p0.x, p1.y);
                    rl.rlTexCoord2f(st1.x, st1.y); rl.rlVertex2f(p1.x, p1.y);
                    rl.rlTexCoord2f(st1.x, st0.y); rl.rlVertex2f(p1.x, p0.y);

                } else {
                    std.log.warn("kb_text_shape found the glyph, but we didn't load it from the font. index {}", .{RunGlyph.Id});
                    std.log.warn("advance_x {}", .{advance_x});
                    cursor_x += dpi_advance_x;
                    cursor_y += dpi_advance_y;
                }
            }
        }

        rl.rlEnd();
        rl.rlSetTexture(0);
        rl.EndShaderMode();
    }

    pub fn size_row(self: *Font, str: []const u8, n: i32, max_width: f32) struct {rl.Vector2, f32, i32} {
        _ = max_width;
        _ = n;
        const Context = kb.kbts_CreateShapeContext(null, null);
        const kb_font = kb.kbts_ShapePushFont(Context, &self.kb);
        if (kb_font == null) {
            std.log.info("Could not open font!", .{});
            return .{ rl.Vector2.zero(), 0, 0 };
        }

        kb.kbts_ShapeBegin(Context, kb.KBTS_DIRECTION_DONT_KNOW, kb.KBTS_LANGUAGE_DONT_KNOW);
        kb.kbts_ShapeUtf8(Context, str.ptr, @intCast(str.len), kb.KBTS_USER_ID_GENERATION_MODE_CODEPOINT_INDEX);
        kb.kbts_ShapeEnd(Context);

        var size = rl.Vector2.zero();
        var max_descent : f32 = 0;

        var Run = kb.kbts_run{};
        while(kb.kbts_ShapeRun(Context, &Run) != 0) {
            if ((Run.Flags & kb.KBTS_BREAK_FLAG_LINE_HARD) != 0) {

            }

            var g : [*c]kb.kbts_glyph = null;
            while (kb.kbts_GlyphIteratorNext(&Run.Glyphs, &g) != 0) {
                const RunGlyph : *kb.kbts_glyph = g.?;
                const CodepointIndex : i32 = RunGlyph.UserIdOrCodepointIndex;
                var ShapeCodepoint : kb.kbts_shape_codepoint = undefined;
                if (kb.kbts_ShapeGetShapeCodepoint(Context, CodepointIndex, &ShapeCodepoint) == 0) {
                    std.log.info("aah it's wrong!! idk how to handle the error rn!!", .{});
                }

                if (self.glyphs.getPtr(@intCast(RunGlyph.Id))) |glyph| {
                    const advance_x = @as(f32, @floatFromInt(ft.mulFix(RunGlyph.AdvanceX, @intCast(self.face.size().metrics().x_scale)) >> 6));
                    const dpi_advance_x = advance_x / rl.getWindowScaleDPI().x;

                    size.y = @max(size.y, glyph.dpi_height);
                    size.x += dpi_advance_x;
                    max_descent = @max(max_descent, glyph.dpi_descent);
                }
            }
        }

        return .{ size, max_descent,  0};
    }
};

fn my_kbts_ShapeRun(Context: ?*kb.kbts_shape_context, Run: [*c]kb.kbts_run) c_int {
    const zone = tracy.ZoneNC(@src(), "my_kbts_ShapeRun", 0x00_ff_00_00);
    defer zone.End();
    return kb.kbts_ShapeRun(Context, Run);
}
