const std = @import("std");
const c = @import("c.zig");

const core = @import("core");
const Vector2i = core.Vector2i;
const Vector2f = core.Vector2f;
const Name = core.Name;

const colors = core.colors;
const ColorRGBA8 = colors.ColorRGBA8;
const Color = colors.Color;

const LocText = @import("localization.zig").LocText;

const utils = @import("utils.zig");
const loadFileAlloc = utils.loadFileAlloc;
const BmpWriter = @import("BmpRenderer.zig").BmpWriter;

name: Name,
atlas: *FontAtlas,

pub fn setRendererHash(self: *@This(), hash: u32) void {
    self.atlas.rendererHash = hash;
}

pub const FontCreateOpts = struct {
    isMonospace: bool = false,
    isSDF: bool = true,
};

// this is going to be interesting... perhaps fonts aren't something that i need to actually make
// a cooked format for.
//
// instead I should make something like a glyph cache

pub const FontAtlas = struct {
    font: c.stbtt_fontinfo = undefined,
    allocator: std.mem.Allocator,
    fileContent: []const u8,
    fromArchive: bool = false,
    filePath: []const u8,

    rendererHash: u32 = 0, // optional field to associate this atlas with an identifier to the renderer implementation
    isEmbedded: bool = false,

    isSDF: bool = false,
    fontSize: f32,
    atlasSize: Vector2i = .{},
    glyphMax: Vector2i = .{},
    glyphStride: i32 = 0,
    scale: f32 = 0,
    lineSize: f32 = 0,
    isMonospace: bool = false,
    glyphMetrics: [256]Vector2i = undefined,
    glyphBox0: [256]Vector2i = undefined,
    glyphBox1: [256]Vector2i = undefined,
    hasGlyph: [256]bool = undefined,
    meshes: [256][4]Vector2f = undefined,
    glyphCoordinates: [256][2]Vector2f = undefined,
    atlasBuffer: ?[]u8,

    const defaultFontEmbed = @embedFile("fonts/Roboto-Regular.ttf");
    const defaultMonoEmbed = @embedFile("fonts/FiraMono-Medium.ttf");

    pub fn makeBitmapRGBA(self: @This(), allocator: std.mem.Allocator) ![]u8 {
        var buf = try allocator.alloc(u8, self.atlasBuffer.?.len * 4);

        for (0..self.atlasBuffer.?.len) |i| {
            buf[(i * 4) + 0] = self.atlasBuffer.?[i];
            buf[(i * 4) + 1] = self.atlasBuffer.?[i];
            buf[(i * 4) + 2] = self.atlasBuffer.?[i];
            buf[(i * 4) + 3] = 1.0;
        }

        return buf;
    }

    pub fn initFontCacheEmbedded(allocator: std.mem.Allocator, fontName: []const u8, bytes: []const u8, fontSize: f32, opts: FontCreateOpts) !@This() {
        const cacheFile = try std.fmt.allocPrint(allocator, ".fontcache/{s}.fontcache", .{fontName});
        defer allocator.free(cacheFile);

        std.fs.cwd().access(cacheFile, .{}) catch {
            const rv = try initEmbeddedFont(allocator, bytes, fontSize, opts);
            core.engine_log("[Fontcache] creating font cache for {s} font", .{fontName});

            var cachedFontArchive = std.ArrayList(u8).init(allocator);
            defer cachedFontArchive.deinit();
            try rv.saveToArchive(&cachedFontArchive);

            try std.fs.cwd().makePath(".fontcache");

            const file = try std.fs.cwd().createFile(cacheFile, .{});
            defer file.close();

            try file.writeAll(cachedFontArchive.items);
            return rv;
        };

        core.engine_log("[Fontcache] loading font cache for {s} font", .{fontName});
        const archiveBytes = try loadFileAlloc(cacheFile, 8, allocator);
        defer allocator.free(archiveBytes);

        var rv = try initFromArchive(allocator, archiveBytes);
        rv.fromArchive = true;
        return rv;
    }

    pub fn initDefaultBitmapFont(allocator: std.mem.Allocator, fontSize: f32) !@This() {
        return try initFontCacheEmbedded(allocator, "bitmap", defaultFontEmbed, fontSize, .{ .isSDF = false });
    }

    pub fn initMonoFont(allocator: std.mem.Allocator, fontSize: f32) !@This() {
        return try initFontCacheEmbedded(allocator, "monospace", defaultMonoEmbed, fontSize, .{ .isMonospace = true });
    }

    pub fn initDefaultFont(allocator: std.mem.Allocator, fontSize: f32) !@This() {
        return try initFontCacheEmbedded(allocator, "default", defaultFontEmbed, fontSize, .{});
    }

    pub fn initEmbeddedFont(allocator: std.mem.Allocator, fontContent: []const u8, fontSize: f32, opts: FontCreateOpts) !@This() {
        var self = @This(){
            .allocator = allocator,
            .filePath = "embedded_file",
            .fileContent = fontContent,
            .atlasBuffer = null,
            .fontSize = fontSize,
            .isSDF = opts.isSDF,
            .isEmbedded = true,
            .isMonospace = opts.isMonospace,
        };

        _ = c.stbtt_InitFont(
            &self.font,
            self.fileContent.ptr,
            c.stbtt_GetFontOffsetForIndex(self.fileContent.ptr, 0),
        );

        try self.createAtlas();

        return self;
    }

    pub fn initFromFileSDF(allocator: std.mem.Allocator, file: []const u8, fontSize: f32, opts: FontCreateOpts) !@This() {
        var self = @This(){
            .allocator = allocator,
            .filePath = file,
            .fileContent = try loadFileAlloc(file, 8, allocator),
            .atlasBuffer = null,
            .fontSize = fontSize,
            .isSDF = true,
            .isMonospace = opts.isMonospace,
        };

        _ = c.stbtt_InitFont(
            &self.font,
            self.fileContent.ptr,
            c.stbtt_GetFontOffsetForIndex(self.fileContent.ptr, 0),
        );

        try self.createAtlas();

        return self;
    }

    // destroys all bitmaps created during the setup process.
    // if the bitmaps are already uploaded to the GPU, you wouldnt need to
    // keep them mapped
    pub fn cleanUp(self: *@This()) void {
        if (self.atlasBuffer) |buffer| {
            self.atlasBuffer = null;
            self.allocator.free(buffer);
        }
    }

    // creates a font atlas from
    pub fn initFromFile(allocator: std.mem.Allocator, file: []const u8, fontSize: f32, opts: FontCreateOpts) !@This() {
        var self = @This(){
            .allocator = allocator,
            .filePath = file,
            .fileContent = try loadFileAlloc(file, 8, allocator),
            .atlasBuffer = null,
            .fontSize = fontSize,
            .isMonospace = opts.isMonospace,
        };

        _ = c.stbtt_InitFont(&self.font, self.fileContent.ptr, c.stbtt_GetFontOffsetForIndex(self.fileContent.ptr, 0));

        try self.createAtlas();

        return self;
    }

    pub fn initFromArchive(allocator: std.mem.Allocator, archive: []const u8) !@This() {
        var self = @This(){
            .allocator = allocator,
            .filePath = "from_archive",
            .fileContent = undefined,
            .fromArchive = true,
            .atlasBuffer = null,
            .fontSize = 0,
            .isMonospace = false,
        };

        try self.loadFromBytes(archive);

        return self;
    }

    pub fn saveToArchive(self: @This(), out: *std.ArrayList(u8)) !void {
        var writer = out.writer();

        try writer.writeInt(u8, @intFromBool(self.isSDF), .little); // isSDF
        try writer.writeInt(u32, @bitCast(self.fontSize), .little); // fontSize: f32 = 0,
        try writer.writeStruct(self.atlasSize); // atlasSize: Vector2i = .{},
        try writer.writeStruct(self.glyphMax); // glyphMax: Vector2i = .{},
        try writer.writeInt(i32, self.glyphStride, .little); // glyphStride: i32 = 0,
        try writer.writeInt(u32, @bitCast(self.scale), .little); // scale: f32 = 0,
        try writer.writeInt(u32, @bitCast(self.lineSize), .little); // lineSize: f32 = 0,
        try writer.writeInt(u8, @intFromBool(self.isMonospace), .little); // isMonospace: bool = false,
        for (self.glyphMetrics) |x| { // glyphMetrics: [256]Vector2i = undefined,
            try writer.writeStruct(x);
        }

        for (self.glyphBox0) |x| { // glyphBox0: [256]Vector2i = undefined,
            try writer.writeStruct(x);
        }

        for (self.glyphBox1) |x| { // glyphBox1: [256]Vector2i = undefined,
            try writer.writeStruct(x);
        }

        for (self.hasGlyph) |x| { // hasGlyph: [256]bool = undefined,
            try writer.writeInt(u8, @intFromBool(x), .little);
        }

        for (self.meshes) |x| { // meshes: [256][4]Vector2f = undefined,
            for (x) |y| {
                try writer.writeStruct(y);
            }
        }

        for (self.glyphCoordinates) |x| { // glyphCoordinates: [256][2]Vector2f = undefined,
            for (x) |y| {
                try writer.writeStruct(y);
            }
        }

        if (self.atlasBuffer) |buffer| { // atlasBuffer: ?[]u8,
            try writer.writeInt(u32, @intCast(buffer.len), .little);
            try writer.writeAll(buffer);
        }
    }

    pub fn loadFromBytes(self: *@This(), archive: []const u8) !void {
        var fbs = std.io.fixedBufferStream(archive);
        var reader = fbs.reader();

        self.isSDF = (try reader.readByte()) == 1; // isSDF: bool = false,
        self.fontSize = @bitCast(try reader.readInt(u32, .little)); // fontSize: f32,
        self.atlasSize = try reader.readStruct(Vector2i); // atlasSize: Vector2i = .{},
        self.glyphMax = try reader.readStruct(Vector2i); // glyphMax: Vector2i = .{},
        self.glyphStride = try reader.readInt(i32, .little); // glyphStride: i32 = 0,
        self.scale = @bitCast(try reader.readInt(u32, .little)); // scale: f32 = 0,
        self.lineSize = @bitCast(try reader.readInt(u32, .little)); // lineSize: f32 = 0,
        self.isMonospace = (try reader.readByte()) == 1; // isMonospace: bool = false,
        for (self.glyphMetrics, 0..) |_, i| { // glyphMetrics: [256]Vector2i = undefined,
            self.glyphMetrics[i] = try reader.readStruct(Vector2i);
        }

        for (self.glyphBox0, 0..) |_, i| { // glyphBox0: [256]Vector2i = undefined,
            self.glyphBox0[i] = try reader.readStruct(Vector2i);
        }

        for (self.glyphBox1, 0..) |_, i| { // glyphBox1: [256]Vector2i = undefined,
            self.glyphBox1[i] = try reader.readStruct(Vector2i);
        }

        for (self.hasGlyph, 0..) |_, i| { // hasGlyph: [256]bool = undefined,
            self.hasGlyph[i] = (try reader.readByte()) == 1;
        }

        // meshes: [256][4]Vector2f = undefined,
        for (self.meshes, 0..) |_, i| { // meshes: [256]Vector2i = undefined,
            self.meshes[i][0] = try reader.readStruct(Vector2f);
            self.meshes[i][1] = try reader.readStruct(Vector2f);
            self.meshes[i][2] = try reader.readStruct(Vector2f);
            self.meshes[i][3] = try reader.readStruct(Vector2f);
        }

        for (self.glyphCoordinates, 0..) |_, i| { // glyphCoordinates: [256][2]Vector2f = undefined,
            self.glyphCoordinates[i][0] = try reader.readStruct(Vector2f);
            self.glyphCoordinates[i][1] = try reader.readStruct(Vector2f);
        }

        const size = try reader.readInt(u32, .little);
        self.atlasBuffer = try reader.readAllAlloc(self.allocator, size); // atlasBuffer: ?[]u8,
    }

    fn createAtlas(self: *@This()) !void {
        const glyphCount = 256;
        var glyphs: [glyphCount][*c]u8 = undefined;
        var max: Vector2i = .{};

        var ch: u32 = 0;

        self.scale = c.stbtt_ScaleForPixelHeight(&self.font, self.fontSize);

        while (ch < glyphCount) : (ch += 1) {
            self.glyphMetrics[ch] = .{ .x = 0, .y = 0 };
            self.glyphBox1[ch] = .{ .x = 0, .y = 0 };

            if (self.isSDF) {
                glyphs[ch] = c.stbtt_GetCodepointSDF(
                    &self.font,
                    c.stbtt_ScaleForPixelHeight(&self.font, self.fontSize),
                    @as(c_int, @intCast(ch)),
                    5,
                    180,
                    36,
                    &self.glyphMetrics[ch].x,
                    &self.glyphMetrics[ch].y,
                    &self.glyphBox1[ch].x,
                    &self.glyphBox1[ch].y,
                );
            } else {
                glyphs[ch] = c.stbtt_GetCodepointBitmap(
                    &self.font,
                    0,
                    c.stbtt_ScaleForPixelHeight(&self.font, self.fontSize),
                    @as(c_int, @intCast(ch)),
                    &self.glyphMetrics[ch].x,
                    &self.glyphMetrics[ch].y,
                    &self.glyphBox1[ch].x,
                    &self.glyphBox1[ch].y,
                );
            }

            if (self.glyphMetrics[ch].x > max.x) {
                //if (self.glyphMetrics[ch].x < 128) {
                max.x = self.glyphMetrics[ch].x;
                //}
            }

            if (self.glyphMetrics[ch].y > max.y) {
                //if (self.glyphMetrics[ch].y < 128) {
                max.y = self.glyphMetrics[ch].y;
                //}
            }
        }

        self.glyphMax = max;
        // allocate the atlasBuffer, just a linear strip
        self.atlasSize = .{ .x = (max.x + 1) * glyphCount, .y = (max.y + 1) };
        self.glyphStride = max.x + 1;
        self.atlasBuffer = try self.allocator.alloc(u8, @as(usize, @intCast(self.atlasSize.x * self.atlasSize.y)));
        @memset(self.atlasBuffer.?, 0x0);

        // write bitmaps into the atlas buffer
        ch = 0;
        while (ch < glyphCount) : (ch += 1) {
            if (@intFromPtr(glyphs[ch]) == 0) {
                self.hasGlyph[ch] = false;
                continue;
            }
            self.hasGlyph[ch] = true;

            const tl = Vector2i{ .x = @as(i32, @intCast(ch)) * (max.x + 1), .y = 0 };
            const maxCol = self.glyphMetrics[ch].x;
            const maxRow = self.glyphMetrics[ch].y;
            var col: i32 = 0;
            var row: i32 = 0;

            // get floating point coordinates for rendering to opengl/vulkan
            // get top left coordinates
            self.glyphCoordinates[ch][0] = .{
                .x = @as(f32, @floatFromInt(tl.x)) / @as(f32, @floatFromInt(self.atlasSize.x)),
                .y = @as(f32, @floatFromInt(tl.y)) / @as(f32, @floatFromInt(self.atlasSize.y)),
            };

            // get bottom right coordinates
            self.glyphCoordinates[ch][1] = .{
                .x = @as(f32, @floatFromInt(tl.x + self.glyphMetrics[ch].x)) / @as(f32, @floatFromInt(self.atlasSize.x)),
                .y = @as(f32, @floatFromInt(tl.y + self.glyphMetrics[ch].y)) / @as(f32, @floatFromInt(self.atlasSize.y)),
            };

            while (row < maxRow) : (row += 1) {
                col = 0;
                while (col < maxCol) : (col += 1) {
                    const pixelOffset = @as(usize, @intCast(((row + tl.y) * self.atlasSize.x) + (col + tl.x)));
                    self.atlasBuffer.?[pixelOffset] = glyphs[ch][@as(usize, @intCast((row * maxCol) + col))];
                }
            }

            const xSize = @as(f32, @floatFromInt(self.glyphMetrics[ch].x)) * self.scale;
            const ySize = @as(f32, @floatFromInt(self.glyphMetrics[ch].y)) * self.scale;
            const xOff = @as(f32, @floatFromInt(self.glyphBox1[ch].x)) * self.scale;
            const yOff = @as(f32, @floatFromInt(self.glyphBox1[ch].y)) * self.scale;

            // create an appropriately proportioned mesh based on the scale.
            self.meshes[ch][0] = .{ .x = xOff, .y = ySize + yOff }; // TL
            self.meshes[ch][1] = .{ .x = xSize + xOff, .y = ySize + yOff }; // TR
            self.meshes[ch][2] = .{ .x = xSize + xOff, .y = 0 + yOff }; // BR
            self.meshes[ch][3] = .{ .x = xOff, .y = yOff }; // BL
        }

        ch = 0;
        while (ch < glyphCount) : (ch += 1) {
            if (glyphs[ch]) |ptr| {
                if (self.isSDF) {
                    c.stbtt_FreeSDF(ptr, null);
                } else {
                    c.stbtt_FreeBitmap(ptr, null);
                }
            }
        }

        if (self.isMonospace) {
            var i: usize = 0;

            const fixedSize = self.glyphBox1['a'].x + self.glyphMetrics['a'].x;

            while (i < self.glyphMetrics.len) : (i += 1) {
                self.glyphMetrics[i].x = fixedSize - self.glyphBox1[i].x;
            }
        }
    }

    pub fn dumpBufferToFile(self: *@This(), fileName: []const u8) !void {
        var renderer = try BmpWriter.init(std.testing.allocator, self.atlasSize);
        var row: i32 = 0;
        var col: i32 = 0;
        while (row < renderer.extent.y) : (row += 1) {
            col = 0;
            while (col < renderer.extent.x) : (col += 1) {
                const pixelOffset = @as(usize, @intCast((renderer.extent.x * (row)) + col));
                const pixelOffset2 = @as(usize, @intCast((renderer.extent.x * (self.atlasSize.y - row - 1)) + col));
                renderer.pixelBuffer[pixelOffset * 3 + 0] = self.atlasBuffer.?[pixelOffset2];
                renderer.pixelBuffer[pixelOffset * 3 + 1] = self.atlasBuffer.?[pixelOffset2];
                renderer.pixelBuffer[pixelOffset * 3 + 2] = self.atlasBuffer.?[pixelOffset2];
            }
        }

        try renderer.writeOut(fileName);
        defer renderer.deinit();
    }

    pub fn deinit(self: *@This()) void {
        if (self.atlasBuffer != null) {
            self.allocator.free(self.atlasBuffer.?);
        }
        if (!self.isEmbedded and !self.fromArchive)
            self.allocator.free(self.fileContent);
    }
};
