//! A glyph cache sits on top of a Group and caches the results from it.
const GroupCache = @This();

const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;

const font = @import("main.zig");
const Face = @import("main.zig").Face;
const DeferredFace = @import("main.zig").DeferredFace;
const Library = @import("main.zig").Library;
const Glyph = @import("main.zig").Glyph;
const Style = @import("main.zig").Style;
const Group = @import("main.zig").Group;
const Presentation = @import("main.zig").Presentation;

const log = std.log.scoped(.font_groupcache);

/// Cache for codepoints to font indexes in a group.
codepoints: std.AutoHashMapUnmanaged(CodepointKey, ?Group.FontIndex) = .{},

/// Cache for glyph renders.
glyphs: std.AutoHashMapUnmanaged(GlyphKey, Glyph) = .{},

/// The underlying font group. Users are expected to use this directly
/// to setup the group or make changes. Beware some changes require a reset
/// (see reset).
group: Group,

/// The texture atlas to store renders in. The GroupCache has to store these
/// because the cached Glyph result is dependent on the Atlas.
atlas_greyscale: font.Atlas,
atlas_color: font.Atlas,

const CodepointKey = struct {
    style: Style,
    codepoint: u32,
    presentation: ?Presentation,
};

const GlyphKey = struct {
    index: Group.FontIndex,
    glyph: u32,
};

/// The GroupCache takes ownership of Group and will free it.
pub fn init(alloc: Allocator, group: Group) !GroupCache {
    var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale);
    errdefer atlas_greyscale.deinit(alloc);
    var atlas_color = try font.Atlas.init(alloc, 512, .rgba);
    errdefer atlas_color.deinit(alloc);

    var result: GroupCache = .{
        .group = group,
        .atlas_greyscale = atlas_greyscale,
        .atlas_color = atlas_color,
    };

    // We set an initial capacity that can fit a good number of characters.
    // This number was picked empirically based on my own terminal usage.
    try result.codepoints.ensureTotalCapacity(alloc, 128);
    try result.glyphs.ensureTotalCapacity(alloc, 128);

    return result;
}

pub fn deinit(self: *GroupCache, alloc: Allocator) void {
    self.codepoints.deinit(alloc);
    self.glyphs.deinit(alloc);
    self.atlas_greyscale.deinit(alloc);
    self.atlas_color.deinit(alloc);
    self.group.deinit();
}

/// Reset the cache. This should be called:
///
///   - If an Atlas was reset
///   - If a font group font size was changed
///   - If a font group font set was changed
///
pub fn reset(self: *GroupCache) void {
    self.codepoints.clearRetainingCapacity();
    self.glyphs.clearRetainingCapacity();
}

/// Resize the fonts in the group. This will clear the cache.
pub fn setSize(self: *GroupCache, size: font.face.DesiredSize) !void {
    try self.group.setSize(size);

    // Reset our internal state
    self.reset();

    // Clear our atlases
    self.atlas_greyscale.clear();
    self.atlas_color.clear();
}

/// Get the font index for a given codepoint. This is cached.
pub fn indexForCodepoint(
    self: *GroupCache,
    alloc: Allocator,
    cp: u32,
    style: Style,
    p: ?Presentation,
) !?Group.FontIndex {
    const key: CodepointKey = .{ .style = style, .codepoint = cp, .presentation = p };
    const gop = try self.codepoints.getOrPut(alloc, key);

    // If it is in the cache, use it.
    if (gop.found_existing) return gop.value_ptr.*;

    // Load a value and cache it. This even caches negative matches.
    const value = self.group.indexForCodepoint(cp, style, p);
    gop.value_ptr.* = value;
    return value;
}

/// Render a glyph. This automatically determines the correct texture
/// atlas to use and caches the result.
pub fn renderGlyph(
    self: *GroupCache,
    alloc: Allocator,
    index: Group.FontIndex,
    glyph_index: u32,
    opts: font.face.RenderOptions,
) !Glyph {
    const key: GlyphKey = .{ .index = index, .glyph = glyph_index };
    const gop = try self.glyphs.getOrPut(alloc, key);

    // If it is in the cache, use it.
    if (gop.found_existing) return gop.value_ptr.*;

    // Uncached, render it
    const atlas: *font.Atlas = switch (try self.group.presentationFromIndex(index)) {
        .text => &self.atlas_greyscale,
        .emoji => &self.atlas_color,
    };
    const glyph = self.group.renderGlyph(
        alloc,
        atlas,
        index,
        glyph_index,
        opts,
    ) catch |err| switch (err) {
        // If the atlas is full, we resize it
        error.AtlasFull => blk: {
            try atlas.grow(alloc, atlas.size * 2);
            break :blk try self.group.renderGlyph(
                alloc,
                atlas,
                index,
                glyph_index,
                opts,
            );
        },

        else => return err,
    };

    // Cache and return
    gop.value_ptr.* = glyph;
    return glyph;
}

test {
    const testing = std.testing;
    const alloc = testing.allocator;
    const testFont = @import("test.zig").fontRegular;
    // const testEmoji = @import("test.zig").fontEmoji;

    var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale);
    defer atlas_greyscale.deinit(alloc);

    var lib = try Library.init();
    defer lib.deinit();

    var cache = try init(alloc, try Group.init(
        alloc,
        lib,
        .{ .points = 12 },
    ));
    defer cache.deinit(alloc);

    // Setup group
    _ = try cache.group.addFace(
        .regular,
        .{ .loaded = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } }) },
    );
    var group = cache.group;

    // Visible ASCII. Do it twice to verify cache.
    var i: u32 = 32;
    while (i < 127) : (i += 1) {
        const idx = (try cache.indexForCodepoint(alloc, i, .regular, null)).?;
        try testing.expectEqual(Style.regular, idx.style);
        try testing.expectEqual(@as(Group.FontIndex.IndexInt, 0), idx.idx);

        // Render
        const face = try cache.group.faceFromIndex(idx);
        const glyph_index = face.glyphIndex(i).?;
        _ = try cache.renderGlyph(
            alloc,
            idx,
            glyph_index,
            .{},
        );
    }

    // Do it again, but reset the group so that we know for sure its not hitting it
    {
        cache.group = undefined;
        defer cache.group = group;

        i = 32;
        while (i < 127) : (i += 1) {
            const idx = (try cache.indexForCodepoint(alloc, i, .regular, null)).?;
            try testing.expectEqual(Style.regular, idx.style);
            try testing.expectEqual(@as(Group.FontIndex.IndexInt, 0), idx.idx);

            // Render
            const face = try group.faceFromIndex(idx);
            const glyph_index = face.glyphIndex(i).?;
            _ = try cache.renderGlyph(
                alloc,
                idx,
                glyph_index,
                .{},
            );
        }
    }
}

/// The wasm-compatible API.
pub const Wasm = struct {
    const wasm = @import("../os/wasm.zig");
    const alloc = wasm.alloc;

    export fn group_cache_new(group: *Group) ?*GroupCache {
        return group_cache_new_(group) catch null;
    }

    fn group_cache_new_(group: *Group) !*GroupCache {
        var gc = try GroupCache.init(alloc, group.*);
        errdefer gc.deinit(alloc);

        var result = try alloc.create(GroupCache);
        errdefer alloc.destroy(result);
        result.* = gc;
        return result;
    }

    export fn group_cache_free(ptr: ?*GroupCache) void {
        if (ptr) |v| {
            v.deinit(alloc);
            alloc.destroy(v);
        }
    }

    export fn group_cache_set_size(self: *GroupCache, size: u16) void {
        return self.setSize(.{ .points = size }) catch |err| {
            log.warn("error setting group cache size err={}", .{err});
            return;
        };
    }

    /// Presentation is negative for doesn't matter.
    export fn group_cache_index_for_codepoint(self: *GroupCache, cp: u32, style: u16, p: i16) i16 {
        const presentation: ?Presentation = if (p < 0) null else @enumFromInt(p);
        if (self.indexForCodepoint(
            alloc,
            cp,
            @enumFromInt(style),
            presentation,
        )) |idx| {
            return @intCast(@as(u8, @bitCast(idx orelse return -1)));
        } else |err| {
            log.warn("error getting index for codepoint from group cache size err={}", .{err});
            return -1;
        }
    }

    export fn group_cache_render_glyph(
        self: *GroupCache,
        idx: i16,
        cp: u32,
        max_height: u16,
    ) ?*Glyph {
        return group_cache_render_glyph_(self, idx, cp, max_height) catch |err| {
            log.warn("error rendering group cache glyph err={}", .{err});
            return null;
        };
    }

    fn group_cache_render_glyph_(
        self: *GroupCache,
        idx_: i16,
        cp: u32,
        max_height_: u16,
    ) !*Glyph {
        const idx = @as(Group.FontIndex, @bitCast(@as(u8, @intCast(idx_))));
        const max_height = if (max_height_ <= 0) null else max_height_;
        const glyph = try self.renderGlyph(alloc, idx, cp, .{
            .max_height = max_height,
        });

        var result = try alloc.create(Glyph);
        errdefer alloc.destroy(result);
        result.* = glyph;
        return result;
    }

    export fn group_cache_atlas_greyscale(self: *GroupCache) *font.Atlas {
        return &self.atlas_greyscale;
    }

    export fn group_cache_atlas_color(self: *GroupCache) *font.Atlas {
        return &self.atlas_color;
    }
};

test "resize" {
    const testing = std.testing;
    const alloc = testing.allocator;
    const testFont = @import("test.zig").fontRegular;
    // const testEmoji = @import("test.zig").fontEmoji;

    var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale);
    defer atlas_greyscale.deinit(alloc);

    var lib = try Library.init();
    defer lib.deinit();

    var cache = try init(alloc, try Group.init(
        alloc,
        lib,
        .{ .points = 12 },
    ));
    defer cache.deinit(alloc);

    // Setup group
    _ = try cache.group.addFace(
        .regular,
        .{ .loaded = try Face.init(
            lib,
            testFont,
            .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
        ) },
    );

    // Load a letter
    {
        const idx = (try cache.indexForCodepoint(alloc, 'A', .regular, null)).?;
        const face = try cache.group.faceFromIndex(idx);
        const glyph_index = face.glyphIndex('A').?;
        const glyph = try cache.renderGlyph(
            alloc,
            idx,
            glyph_index,
            .{},
        );

        try testing.expectEqual(@as(u32, 11), glyph.height);
    }

    // Resize
    try cache.setSize(.{ .points = 24, .xdpi = 96, .ydpi = 96 });
    {
        const idx = (try cache.indexForCodepoint(alloc, 'A', .regular, null)).?;
        const face = try cache.group.faceFromIndex(idx);
        const glyph_index = face.glyphIndex('A').?;
        const glyph = try cache.renderGlyph(
            alloc,
            idx,
            glyph_index,
            .{},
        );

        try testing.expectEqual(@as(u32, 21), glyph.height);
    }
}
