//! Face represents a single font face. A single font face has a single set
//! of properties associated with it such as style, weight, etc.
//!
//! A Face isn't typically meant to be used directly. It is usually used
//! via a Family in order to store it in an Atlas.

const std = @import("std");
const builtin = @import("builtin");
const freetype = @import("freetype");
const harfbuzz = @import("harfbuzz");
const stb = @import("../../stb/main.zig");
const assert = @import("../../quirks.zig").inlineAssert;
const testing = std.testing;
const Allocator = std.mem.Allocator;
const font = @import("../main.zig");
const Glyph = font.Glyph;
const Library = font.Library;
const opentype = @import("../opentype.zig");
const quirks = @import("../../quirks.zig");
const config = @import("../../config.zig");

const F26Dot6 = opentype.sfnt.F26Dot6;

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

pub const Face = struct {
    comptime {
        // If we have the freetype backend, we should have load flags.
        assert(font.face.FreetypeLoadFlags != void);
    }

    /// Our Library
    lib: Library,

    /// Our font face.
    face: freetype.Face,

    /// This mutex MUST be held while doing anything with the
    /// glyph slot on the freetype face, because this struct
    /// may be shared across multiple surfaces.
    ///
    /// This means that anywhere where `self.face.loadGlyph`
    /// is called, this mutex must be held.
    ft_mutex: *std.Thread.Mutex,

    /// Harfbuzz font corresponding to this face.
    hb_font: harfbuzz.Font,

    /// Freetype load flags for this font face.
    load_flags: font.face.FreetypeLoadFlags,

    /// Set quirks.disableDefaultFontFeatures
    quirks_disable_default_font_features: bool = false,

    /// Set to true to apply a synthetic italic to the face.
    synthetic: packed struct {
        italic: bool = false,
        bold: bool = false,
    } = .{},

    /// The current size this font is set to.
    size: font.face.DesiredSize,

    /// Initialize a new font face with the given source in-memory.
    pub fn initFile(
        lib: Library,
        path: [:0]const u8,
        index: i32,
        opts: font.face.Options,
    ) !Face {
        lib.mutex.lock();
        defer lib.mutex.unlock();
        const face = try lib.lib.initFace(path, index);
        errdefer face.deinit();
        return try initFace(lib, face, opts);
    }

    /// Initialize a new font face with the given source in-memory.
    pub fn init(
        lib: Library,
        source: [:0]const u8,
        opts: font.face.Options,
    ) !Face {
        lib.mutex.lock();
        defer lib.mutex.unlock();
        const face = try lib.lib.initMemoryFace(source, 0);
        errdefer face.deinit();
        return try initFace(lib, face, opts);
    }

    fn initFace(
        lib: Library,
        face: freetype.Face,
        opts: font.face.Options,
    ) !Face {
        try face.selectCharmap(.unicode);
        try setSize_(face, opts.size);

        var hb_font = try harfbuzz.freetype.createFont(face.handle);
        errdefer hb_font.destroy();

        const ft_mutex = try lib.alloc.create(std.Thread.Mutex);
        errdefer lib.alloc.destroy(ft_mutex);
        ft_mutex.* = .{};

        var result: Face = .{
            .lib = lib,
            .face = face,
            .hb_font = hb_font,
            .ft_mutex = ft_mutex,
            .load_flags = opts.freetype_load_flags,
            .size = opts.size,
        };
        result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);

        // In debug mode, we output information about available variation axes,
        // if they exist.
        if (comptime builtin.mode == .Debug) mm: {
            if (!face.hasMultipleMasters()) break :mm;
            var buf: [1024]u8 = undefined;
            log.debug("variation axes font={s}", .{try result.name(&buf)});

            const mm = try face.getMMVar();
            defer lib.lib.doneMMVar(mm);
            for (0..mm.num_axis) |i| {
                const axis = mm.axis[i];
                const id_raw = std.math.cast(c_int, axis.tag) orelse continue;
                const id: font.face.Variation.Id = @bitCast(id_raw);
                log.debug("variation axis: name={s} id={s} min={} max={} def={}", .{
                    std.mem.sliceTo(axis.name, 0),
                    id.str(),
                    axis.minimum >> 16,
                    axis.maximum >> 16,
                    axis.def >> 16,
                });
            }
        }

        return result;
    }

    pub fn deinit(self: *Face) void {
        self.lib.alloc.destroy(self.ft_mutex);
        {
            self.lib.mutex.lock();
            defer self.lib.mutex.unlock();

            self.face.deinit();
        }
        self.hb_font.destroy();
        self.* = undefined;
    }

    /// Returns the font name. If allocation is required, buf will be used,
    /// but sometimes allocation isn't required and a static string is
    /// returned.
    pub fn name(self: *const Face, buf: []u8) Allocator.Error![]const u8 {
        const count = self.face.getSfntNameCount();

        // We look for the font family entry.
        for (0..count) |i| {
            const entry = self.face.getSfntName(i) catch continue;
            if (entry.name_id == freetype.c.TT_NAME_ID_FONT_FAMILY) {
                const string = entry.string[0..entry.string_len];
                // There are other encodings that are something other than UTF-8
                // but this is one we've seen "in the wild" so far.
                if (entry.platform_id == freetype.c.TT_PLATFORM_MICROSOFT and entry.encoding_id == freetype.c.TT_MS_ID_UNICODE_CS) skip: {
                    if (string.len % 2 != 0) break :skip;
                    if (string.len > 1024) break :skip;
                    var tmp: [512]u16 = undefined;
                    const max = string.len / 2;
                    for (@as([]const u16, @ptrCast(@alignCast(string))), 0..) |c, j| tmp[j] = @byteSwap(c);
                    const len = std.unicode.utf16LeToUtf8(buf, tmp[0..max]) catch return string;
                    return buf[0..len];
                }
                return string;
            }
        }

        return "";
    }

    test "face name" {
        const embedded = @import("../embedded.zig");

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

        {
            var face: Face = try .init(lib, embedded.variable, .{ .size = .{ .points = 14 } });
            defer face.deinit();

            var buf: [1024]u8 = undefined;
            const actual = try face.name(&buf);

            try testing.expectEqualStrings("JetBrains Mono", actual);
        }

        {
            var face: Face = try .init(lib, embedded.inconsolata, .{ .size = .{ .points = 14 } });
            defer face.deinit();

            var buf: [1024]u8 = undefined;
            const actual = try face.name(&buf);

            try testing.expectEqualStrings("Inconsolata", actual);
        }
    }

    /// Return a new face that is the same as this but also has synthetic
    /// bold applied.
    pub fn syntheticBold(self: *const Face, opts: font.face.Options) !Face {
        // Increase face ref count
        self.face.ref();
        errdefer self.face.deinit();

        var f = try initFace(self.lib, self.face, opts);
        errdefer f.deinit();
        f.synthetic = self.synthetic;
        f.synthetic.bold = true;

        return f;
    }

    /// Return a new face that is the same as this but has a transformation
    /// matrix applied to italicize it.
    pub fn syntheticItalic(self: *const Face, opts: font.face.Options) !Face {
        // Increase face ref count
        self.face.ref();
        errdefer self.face.deinit();

        var f = try initFace(self.lib, self.face, opts);
        errdefer f.deinit();
        f.synthetic = self.synthetic;
        f.synthetic.italic = true;

        return f;
    }

    /// Resize the font in-place. If this succeeds, the caller is responsible
    /// for clearing any glyph caches, font atlas data, etc.
    pub fn setSize(self: *Face, opts: font.face.Options) !void {
        try setSize_(self.face, opts.size);
        self.size = opts.size;
    }

    fn setSize_(face: freetype.Face, size: font.face.DesiredSize) !void {
        // If we have fixed sizes, we just have to try to pick the one closest
        // to what the user requested. Otherwise, we can choose an arbitrary
        // pixel size.
        if (face.isScalable()) {
            const size_26dot6: i32 = @intFromFloat(@round(size.points * 64));
            try face.setCharSize(0, size_26dot6, size.xdpi, size.ydpi);
        } else try selectSizeNearest(face, @intFromFloat(@round(size.pixels())));
    }

    /// Selects the fixed size in the loaded face that is closest to the
    /// requested pixel size.
    fn selectSizeNearest(face: freetype.Face, size: u32) !void {
        var i: i32 = 0;
        var best_i: i32 = 0;
        var best_diff: i32 = 0;
        while (i < face.handle.*.num_fixed_sizes) : (i += 1) {
            const width = face.handle.*.available_sizes[@intCast(i)].width;
            const diff = @as(i32, @intCast(size)) - @as(i32, @intCast(width));
            if (i == 0 or diff < best_diff) {
                best_diff = diff;
                best_i = i;
            }
        }

        try face.selectSize(best_i);
    }

    /// Set the variation axes for this font. This will modify this font
    /// in-place.
    pub fn setVariations(
        self: *Face,
        vs: []const font.face.Variation,
        opts: font.face.Options,
    ) !void {
        _ = opts;

        // If this font doesn't support variations, we can't do anything.
        if (!self.face.hasMultipleMasters() or vs.len == 0) return;

        // Freetype requires that we send ALL coordinates in at once so the
        // first thing we have to do is get all the vars and put them into
        // an array.
        const mm = try self.face.getMMVar();
        defer self.lib.lib.doneMMVar(mm);

        // To avoid allocations, we cap the number of variation axes we can
        // support. This is arbitrary but Firefox caps this at 16 so I
        // feel like that's probably safe... and we do double cause its
        // cheap.
        var coords_buf: [32]freetype.c.FT_Fixed = undefined;
        var coords = coords_buf[0..@min(coords_buf.len, mm.num_axis)];
        try self.face.getVarDesignCoordinates(coords);

        // Now we go through each axis and see if its set. This is slow
        // but there usually aren't many axes and usually not many set
        // variations, either.
        for (0..mm.num_axis) |i| {
            const axis = mm.axis[i];
            const id = std.math.cast(u32, axis.tag) orelse continue;
            for (vs) |v| {
                if (id == @as(u32, @bitCast(v.id))) {
                    coords[i] = @intFromFloat(v.value * 65536);
                    break;
                }
            }
        }

        // Set them!
        try self.face.setVarDesignCoordinates(coords);
    }

    /// Returns the glyph index for the given Unicode code point. If this
    /// face doesn't support this glyph, null is returned.
    pub fn glyphIndex(self: Face, cp: u32) ?u32 {
        return self.face.getCharIndex(cp);
    }

    /// Returns true if this font is colored. This can be used by callers to
    /// determine what kind of atlas to pass in.
    pub fn hasColor(self: Face) bool {
        return self.face.hasColor();
    }

    /// Returns true if the given glyph ID is colorized.
    pub fn isColorGlyph(self: *const Face, glyph_id: u32) bool {
        self.ft_mutex.lock();
        defer self.ft_mutex.unlock();

        // Load the glyph and see what pixel mode it renders with.
        // All modes other than BGRA are non-color.
        // If the glyph fails to load, just return false.
        self.face.loadGlyph(glyph_id, .{
            .render = true,
            .color = self.face.hasColor(),
            // NO_SVG set to true because we don't currently support rendering
            // SVG glyphs under FreeType, since that requires bundling another
            // dependency to handle rendering the SVG.
            .no_svg = true,
        }) catch return false;

        const glyph = self.face.handle.*.glyph;

        return glyph.*.bitmap.pixel_mode == freetype.c.FT_PIXEL_MODE_BGRA;
    }

    /// Set the load flags to use when loading a glyph for measurement or
    /// rendering.
    fn glyphLoadFlags(self: Face, constrained: bool) freetype.LoadFlags {
        // Hinting should only be enabled if the configured load flags specify
        // it and the provided constraint doesn't actually do anything, since
        // if it does, then it'll mess up the hinting anyway when it moves or
        // resizes the glyph.
        const do_hinting = self.load_flags.hinting and !constrained;

        return .{
            // If our glyph has color, we want to render the color
            .color = self.face.hasColor(),

            // We don't render, because we'll invoke the render
            // manually after applying constraints further down.
            .render = false,

            // use options from config
            .no_hinting = !do_hinting,
            .force_autohint = self.load_flags.@"force-autohint",
            .no_autohint = !self.load_flags.autohint,

            // If we're gonna be rendering this glyph in monochrome,
            // then we should use the monochrome hinter as well, or
            // else it won't look very good at all.
            //
            // Otherwise if the user asked for light hinting we
            // use that, otherwise we just use the normal target.
            .target = if (self.load_flags.monochrome)
                .mono
            else if (self.load_flags.light)
                .light
            else
                .normal,

            // NO_SVG set to true because we don't currently support rendering
            // SVG glyphs under FreeType, since that requires bundling another
            // dependency to handle rendering the SVG.
            .no_svg = true,
        };
    }

    /// Get a rect that represents the position and size of the loaded glyph.
    fn getGlyphSize(glyph: freetype.c.FT_GlyphSlot) font.Glyph.Size {
        // If we're dealing with an outline glyph then we get the
        // outline's bounding box instead of using the built-in
        // metrics, since that's more precise and allows better
        // cell-fitting.
        if (glyph.*.format == freetype.c.FT_GLYPH_FORMAT_OUTLINE) {
            // Get the glyph's bounding box before we transform it at all.
            // We use this rather than the metrics, since it's more precise.
            var bbox: freetype.c.FT_BBox = undefined;
            _ = freetype.c.FT_Outline_Get_BBox(&glyph.*.outline, &bbox);

            return .{
                .x = f26dot6ToF64(bbox.xMin),
                .y = f26dot6ToF64(bbox.yMin),
                .width = f26dot6ToF64(bbox.xMax - bbox.xMin),
                .height = f26dot6ToF64(bbox.yMax - bbox.yMin),
            };
        }

        return .{
            .x = f26dot6ToF64(glyph.*.metrics.horiBearingX),
            .y = f26dot6ToF64(glyph.*.metrics.horiBearingY - glyph.*.metrics.height),
            .width = f26dot6ToF64(glyph.*.metrics.width),
            .height = f26dot6ToF64(glyph.*.metrics.height),
        };
    }

    /// Render a glyph using the glyph index. The rendered glyph is stored in the
    /// given texture atlas.
    pub fn renderGlyph(
        self: Face,
        alloc: Allocator,
        atlas: *font.Atlas,
        glyph_index: u32,
        opts: font.Glyph.RenderOptions,
    ) !Glyph {
        self.ft_mutex.lock();
        defer self.ft_mutex.unlock();

        // Load the glyph.
        try self.face.loadGlyph(glyph_index, self.glyphLoadFlags(opts.constraint.doesAnything()));
        const glyph = self.face.handle.*.glyph;

        // For synthetic bold, we embolden the glyph.
        if (self.synthetic.bold) {
            // We need to scale the embolden amount based on the font size.
            // This is a heuristic I found worked well across a variety of
            // founts: 1 pixel per 64 units of height.
            const font_height: f64 = @floatFromInt(self.face.handle.*.size.*.metrics.height);
            const ratio: f64 = 64.0 / 2048.0;
            const amount = @ceil(font_height * ratio);
            _ = freetype.c.FT_Outline_Embolden(&glyph.*.outline, @intFromFloat(amount));
        }

        // We get a rect that represents the position
        // and size of the glyph before constraints.
        const rect = getGlyphSize(glyph);

        // If our glyph is smaller than a quarter pixel in either axis
        // then it has no outlines or they're too small to render.
        //
        // In this case we just return 0-sized glyph struct.
        if (rect.width < 0.25 or rect.height < 0.25)
            return font.Glyph{
                .width = 0,
                .height = 0,
                .offset_x = 0,
                .offset_y = 0,
                .atlas_x = 0,
                .atlas_y = 0,
            };

        const metrics = opts.grid_metrics;
        const cell_width: f64 = @floatFromInt(metrics.cell_width);
        const cell_height: f64 = @floatFromInt(metrics.cell_height);

        // Next we apply any constraints to get the final size of the glyph.
        const constraint = opts.constraint;

        // We need to add the baseline position before passing to the constrain
        // function since it operates on cell-relative positions, not baseline.
        const cell_baseline: f64 = @floatFromInt(metrics.cell_baseline);

        const glyph_size = constraint.constrain(
            .{
                .width = rect.width,
                .height = rect.height,
                .x = rect.x,
                .y = rect.y + cell_baseline,
            },
            metrics,
            opts.constraint_width,
        );

        var width = glyph_size.width;
        var height = glyph_size.height;
        var x = glyph_size.x;
        var y = glyph_size.y;

        // We center all glyphs within the pixel-rounded and adjusted
        // cell width if it's larger than the face width, so that they
        // aren't weirdly off to the left.
        //
        // We don't do this if the glyph has a stretch constraint,
        // since in that case the position was already calculated with the
        // new cell width in mind.
        if ((constraint.size != .stretch) and (metrics.face_width < cell_width)) {
            // We add half the difference to re-center.
            //
            // NOTE: We round this to a whole-pixel amount because under
            //       FreeType, the outlines will be hinted, which isn't
            //       the case under CoreText. If we move the outlines by
            //       a non-whole-pixel amount, it completely ruins the
            //       hinting.
            x += @round((cell_width - metrics.face_width) / 2);
        }

        // If this is a bitmap glyph, it will always render as full pixels,
        // not fractional pixels, so we need to quantize its position and
        // size accordingly to align to full pixels so we get good results.
        if (glyph.*.format == freetype.c.FT_GLYPH_FORMAT_BITMAP) {
            width = cell_width - @round(cell_width - width - x) - @round(x);
            height = cell_height - @round(cell_height - height - y) - @round(y);
            x = @round(x);
            y = @round(y);
        }

        // Now we can render the glyph.
        var bitmap: freetype.c.FT_Bitmap = undefined;
        _ = freetype.c.FT_Bitmap_Init(&bitmap);
        defer _ = freetype.c.FT_Bitmap_Done(self.lib.lib.handle, &bitmap);
        switch (glyph.*.format) {
            freetype.c.FT_GLYPH_FORMAT_OUTLINE => {
                // Manually adjust the glyph outline with this transform.
                //
                // This offers better precision than using the freetype transform
                // matrix, since that has 16.16 coefficients, and also I was having
                // weird issues that I can only assume where due to freetype doing
                // some bad caching or something when I did this using the matrix.
                const scale_x = width / rect.width;
                const scale_y = height / rect.height;
                const skew: f64 =
                    if (self.synthetic.italic)
                        // We skew by 12 degrees to synthesize italics.
                        @tan(std.math.degreesToRadians(12))
                    else
                        0.0;

                const outline = &glyph.*.outline;
                for (outline.points[0..@intCast(outline.n_points)]) |*p| {
                    // Convert to f64 for processing
                    var px = f26dot6ToF64(p.x);
                    var py = f26dot6ToF64(p.y);

                    // Subtract original bearings
                    px -= rect.x;
                    py -= rect.y;

                    // Scale
                    px *= scale_x;
                    py *= scale_y;

                    // Add new bearings
                    px += x;
                    py += y - cell_baseline;

                    // Skew
                    px += py * skew;

                    // Convert back and store
                    p.x = @as(i32, @bitCast(F26Dot6.from(px)));
                    p.y = @as(i32, @bitCast(F26Dot6.from(py)));
                }

                try self.face.renderGlyph(
                    if (self.load_flags.monochrome)
                        .mono
                    else
                        .normal,
                );

                // Copy the glyph's bitmap, making sure
                // that it's 8bpp and densely packed.
                if (freetype.c.FT_Bitmap_Convert(
                    self.lib.lib.handle,
                    &glyph.*.bitmap,
                    &bitmap,
                    1,
                ) != 0) {
                    return error.BitmapHandlingError;
                }
            },

            freetype.c.FT_GLYPH_FORMAT_BITMAP => {
                // If our glyph has a non-color bitmap, we need
                // to convert it to dense 8bpp so that the scale
                // operation works correctly.
                switch (glyph.*.bitmap.pixel_mode) {
                    freetype.c.FT_PIXEL_MODE_BGRA,
                    freetype.c.FT_PIXEL_MODE_GRAY,
                    => {},
                    else => {
                        // Make sure the slot owns its bitmap,
                        // since we'll be modifying it here.
                        if (freetype.c.FT_GlyphSlot_Own_Bitmap(glyph) != 0) {
                            return error.BitmapHandlingError;
                        }

                        var converted: freetype.c.FT_Bitmap = undefined;
                        freetype.c.FT_Bitmap_Init(&converted);
                        if (freetype.c.FT_Bitmap_Convert(
                            self.lib.lib.handle,
                            &glyph.*.bitmap,
                            &converted,
                            1,
                        ) != 0) {
                            return error.BitmapHandlingError;
                        }
                        // Free the existing glyph bitmap and
                        // replace it with the converted one.
                        _ = freetype.c.FT_Bitmap_Done(
                            self.lib.lib.handle,
                            &glyph.*.bitmap,
                        );
                        glyph.*.bitmap = converted;
                    },
                }

                const glyph_bitmap = glyph.*.bitmap;

                // Round our target width and height
                // as the size for our scaled bitmap.
                const w: u32 = @intFromFloat(@round(width));
                const h: u32 = @intFromFloat(@round(height));
                const pitch = w * atlas.format.depth();

                // Allocate a buffer for our scaled bitmap.
                //
                // We'll copy this to the original bitmap once we're
                // done so we can free it at the end of this scope.
                const buf = try alloc.alloc(u8, pitch * h);
                defer alloc.free(buf);

                // Resize
                if (stb.stbir_resize_uint8(
                    glyph_bitmap.buffer,
                    @intCast(glyph_bitmap.width),
                    @intCast(glyph_bitmap.rows),
                    glyph_bitmap.pitch,
                    buf.ptr,
                    @intCast(w),
                    @intCast(h),
                    @intCast(pitch),
                    atlas.format.depth(),
                ) == 0) {
                    // This should never fail because this is a
                    // fairly straightforward in-memory operation...
                    return error.GlyphResizeFailed;
                }

                const scaled_bitmap: freetype.c.FT_Bitmap = .{
                    .buffer = buf.ptr,
                    .width = @intCast(w),
                    .rows = @intCast(h),
                    .pitch = @intCast(pitch),
                    .pixel_mode = glyph_bitmap.pixel_mode,
                    .num_grays = glyph_bitmap.num_grays,
                };

                // Replace the bitmap's buffer and size info.
                if (freetype.c.FT_Bitmap_Copy(
                    self.lib.lib.handle,
                    &scaled_bitmap,
                    &bitmap,
                ) != 0) {
                    return error.BitmapHandlingError;
                }

                // Update the bearings to account for the new positioning.
                glyph.*.bitmap_top = @intFromFloat(@floor(y - cell_baseline + height));
                glyph.*.bitmap_left = @intFromFloat(@floor(x));
            },

            else => |f| {
                // Glyph formats are tags, so we can
                // output a semi-readable error here.
                // Use @bitCast to u32 because MSVC translates C enums
                // as signed int, while GCC/Clang uses unsigned int.
                const tag: u32 = @bitCast(f);
                log.err(
                    "Can't render glyph with unsupported glyph format \"{s}\"",
                    .{[4]u8{
                        @truncate(tag >> 24),
                        @truncate(tag >> 16),
                        @truncate(tag >> 8),
                        @truncate(tag >> 0),
                    }},
                );
                return error.UnsupportedGlyphFormat;
            },
        }

        // If this is a color glyph but we're trying to render it to the
        // grayscale atlas, or vice versa, then we throw and error. Maybe
        // in the future we could convert, but for now it should be fine.
        switch (bitmap.pixel_mode) {
            freetype.c.FT_PIXEL_MODE_GRAY => if (atlas.format != .grayscale) {
                return error.WrongAtlas;
            },
            freetype.c.FT_PIXEL_MODE_BGRA => if (atlas.format != .bgra) {
                return error.WrongAtlas;
            },
            else => {
                log.warn("glyph={} pixel mode={}", .{ glyph_index, bitmap.pixel_mode });
                @panic("unsupported pixel mode");
            },
        }

        // Our whole-pixel bearings for the final glyph.
        // The fractional portion will be included in the rasterized position.
        //
        // For the Y position, FreeType's `bitmap_top` is the distance from the
        // baseline to the top of the glyph, but we need the distance from the
        // bottom of the cell to the bottom of the glyph, so first we add the
        // baseline to get the distance from the bottom of the cell to the top
        // of the glyph, then we subtract the height of the glyph to get the
        // bottom.
        const px_x: i32 = glyph.*.bitmap_left;
        const px_y: i32 = glyph.*.bitmap_top +
            @as(i32, @intCast(metrics.cell_baseline)) -
            @as(i32, @intCast(bitmap.rows));

        const px_width = bitmap.width;
        const px_height = bitmap.rows;
        const len: usize = @intCast(
            @as(c_uint, @intCast(@abs(bitmap.pitch))) * bitmap.rows,
        );

        // If our bitmap is grayscale, make sure to multiply all pixel
        // values by the right factor to bring `num_grays` up to 256.
        //
        // This is necessary because FT_Bitmap_Convert doesn't do this,
        // it just sets num_grays to the correct number and uses the
        // original smaller pixel values.
        if (bitmap.pixel_mode == freetype.c.FT_PIXEL_MODE_GRAY and
            bitmap.num_grays < 256)
        {
            const factor: u8 = @intCast(255 / (bitmap.num_grays - 1));
            for (bitmap.buffer[0..len]) |*p| {
                p.* *= factor;
            }
            bitmap.num_grays = 256;
        }

        // Must have non-empty bitmap because we return earlier if zero.
        // We assume the rest of this that it is non-zero so this is important.
        assert(px_width > 0 and px_height > 0);

        // If this doesn't match then something is wrong.
        assert(px_width * atlas.format.depth() == bitmap.pitch);

        // Allocate our texture atlas region and copy our bitmap in to it.
        const region = try atlas.reserve(alloc, px_width, px_height);
        atlas.set(region, bitmap.buffer[0..len]);

        // This should be the distance from the bottom of
        // the cell to the top of the glyph's bounding box.
        const offset_y: i32 = px_y + @as(i32, @intCast(px_height));

        // This should be the distance from the left of
        // the cell to the left of the glyph's bounding box.
        const offset_x: i32 = px_x;

        return Glyph{
            .width = px_width,
            .height = px_height,
            .offset_x = offset_x,
            .offset_y = offset_y,
            .atlas_x = region.x,
            .atlas_y = region.y,
        };
    }

    /// Convert 16.6 pixel format to pixels based on the scale factor of the
    /// current font size.
    fn unitsToPxY(self: Face, units: i32) i32 {
        return @intCast(freetype.mulFix(
            units,
            @intCast(self.face.handle.*.size.*.metrics.y_scale),
        ) >> 6);
    }

    /// Convert 26.6 pixel format to f32
    fn f26dot6ToFloat(v: freetype.c.FT_F26Dot6) f32 {
        return @floatFromInt(v >> 6);
    }

    fn f26dot6ToF64(v: freetype.c.FT_F26Dot6) f64 {
        return @as(F26Dot6, @bitCast(@as(i32, @intCast(v)))).to(f64);
    }

    /// Get the `FaceMetrics` for this face.
    pub fn getMetrics(self: *Face) font.Metrics.FaceMetrics {
        const face = self.face;

        const size_metrics = face.handle.*.size.*.metrics;

        // This code relies on this assumption, and it should always be
        // true since we don't do any non-uniform scaling on the font ever.
        assert(size_metrics.x_ppem == size_metrics.y_ppem);

        // Read the 'head' table out of the font data.
        const head_ = face.getSfntTable(.head);

        // Read the 'post' table out of the font data.
        const post_ = face.getSfntTable(.post);

        // Read the 'OS/2' table out of the font data.
        const os2_: ?*freetype.c.TT_OS2 = os2: {
            const os2 = face.getSfntTable(.os2) orelse break :os2 null;
            if (os2.version == 0xFFFF) break :os2 null;
            break :os2 os2;
        };

        // Read the 'hhea' table out of the font data.
        const hhea_ = face.getSfntTable(.hhea);

        // Whether the font is in a scalable format. We need to know this
        // because many of the metrics provided by FreeType are invalid for
        // non-scalable fonts.
        const is_scalable = face.handle.*.face_flags & freetype.c.FT_FACE_FLAG_SCALABLE != 0;

        // We get the UPM from the head table.
        //
        // If we have no head, but it is a scalable face, take the UPM from
        // FreeType's units_per_EM, otherwise we'll assume that UPM == PPEM.
        const units_per_em: freetype.c.FT_UShort =
            if (head_) |head|
                head.Units_Per_EM
            else if (is_scalable)
                face.handle.*.units_per_EM
            else
                size_metrics.y_ppem;
        const px_per_em: f64 = @floatFromInt(size_metrics.y_ppem);
        const px_per_unit = px_per_em / @as(f64, @floatFromInt(units_per_em));

        const ascent: f64, const descent: f64, const line_gap: f64 = vertical_metrics: {
            const hhea = hhea_ orelse {
                // If we couldn't get the hhea table, rely on metrics from FreeType.
                const ascender = f26dot6ToF64(size_metrics.ascender);
                const descender = f26dot6ToF64(size_metrics.descender);
                const height = f26dot6ToF64(size_metrics.height);
                break :vertical_metrics .{
                    ascender,
                    descender,
                    // We compute the line gap by adding the (negative) descender
                    // and subtracting the (positive) ascender from the line height
                    // to get the remaining gap size.
                    //
                    // NOTE: This might always be 0... but it doesn't hurt to do.
                    height + descender - ascender,
                };
            };

            const hhea_ascent: f64 = @floatFromInt(hhea.Ascender);
            const hhea_descent: f64 = @floatFromInt(hhea.Descender);
            const hhea_line_gap: f64 = @floatFromInt(hhea.Line_Gap);

            // If our font has no OS/2 table, then we just
            // blindly use the metrics from the hhea table.
            const os2 = os2_ orelse break :vertical_metrics .{
                hhea_ascent * px_per_unit,
                hhea_descent * px_per_unit,
                hhea_line_gap * px_per_unit,
            };

            const os2_ascent: f64 = @floatFromInt(os2.sTypoAscender);
            const os2_descent: f64 = @floatFromInt(os2.sTypoDescender);
            const os2_line_gap: f64 = @floatFromInt(os2.sTypoLineGap);

            // If the font says to use typo metrics, trust it.
            // (The USE_TYPO_METRICS bit is bit 7)
            if (os2.fsSelection & (1 << 7) != 0) {
                break :vertical_metrics .{
                    os2_ascent * px_per_unit,
                    os2_descent * px_per_unit,
                    os2_line_gap * px_per_unit,
                };
            }

            // Otherwise we prefer the height metrics from 'hhea' if they
            // are available, or else OS/2 sTypo* metrics, and if all else
            // fails then we use OS/2 usWin* metrics.
            //
            // This is not "standard" behavior, but it's our best bet to
            // account for fonts being... just weird. It's pretty much what
            // FreeType does to get its generic ascent and descent metrics.

            if (hhea.Ascender != 0 or hhea.Descender != 0) {
                break :vertical_metrics .{
                    hhea_ascent * px_per_unit,
                    hhea_descent * px_per_unit,
                    hhea_line_gap * px_per_unit,
                };
            }

            if (os2_ascent != 0 or os2_descent != 0) {
                break :vertical_metrics .{
                    os2_ascent * px_per_unit,
                    os2_descent * px_per_unit,
                    os2_line_gap * px_per_unit,
                };
            }

            const win_ascent: f64 = @floatFromInt(os2.usWinAscent);
            const win_descent: f64 = @floatFromInt(os2.usWinDescent);
            break :vertical_metrics .{
                win_ascent * px_per_unit,
                // usWinDescent is *positive* -> down unlike sTypoDescender
                // and hhea.Descender, so we flip its sign to fix this.
                -win_descent * px_per_unit,
                0.0,
            };
        };

        const underline_position: ?f64, const underline_thickness: ?f64 = ul: {
            const post = post_ orelse break :ul .{ null, null };

            // Some fonts have degenerate 'post' tables where the underline
            // thickness (and often position) are 0. We consider them null
            // if this is the case and use our own fallbacks when we calculate.
            const has_broken_underline = post.underlineThickness == 0;

            // If the underline position isn't 0 then we do use it,
            // even if the thickness is't properly specified.
            const pos: ?f64 = if (has_broken_underline and post.underlinePosition == 0)
                null
            else
                @as(f64, @floatFromInt(post.underlinePosition)) * px_per_unit;

            const thick: ?f64 = if (has_broken_underline)
                null
            else
                @as(f64, @floatFromInt(post.underlineThickness)) * px_per_unit;

            break :ul .{ pos, thick };
        };

        // Similar logic to the underline above.
        const strikethrough_position, const strikethrough_thickness = st: {
            const os2 = os2_ orelse break :st .{ null, null };

            const has_broken_strikethrough = os2.yStrikeoutSize == 0;

            const pos: ?f64 = if (has_broken_strikethrough and os2.yStrikeoutPosition == 0)
                null
            else
                @as(f64, @floatFromInt(os2.yStrikeoutPosition)) * px_per_unit;

            const thick: ?f64 = if (has_broken_strikethrough)
                null
            else
                @as(f64, @floatFromInt(os2.yStrikeoutSize)) * px_per_unit;

            break :st .{ pos, thick };
        };

        // Cell width is calculated by calculating the widest width of the
        // visible ASCII characters. Usually 'M' is widest but we just take
        // whatever is widest.
        //
        // ASCII height is calculated as the height of the overall bounding
        // box of the same characters.
        //
        // If we fail to load any visible ASCII we just use max_advance from
        // the metrics provided by FreeType, and set ascii_height to null as
        // it's optional.
        const cell_width: f64, const ascii_height: ?f64 = measurements: {
            self.ft_mutex.lock();
            defer self.ft_mutex.unlock();

            var max: f64 = 0.0;
            var top: f64 = 0.0;
            var bottom: f64 = 0.0;
            var c: u8 = ' ';
            while (c < 127) : (c += 1) {
                if (face.getCharIndex(c)) |glyph_index| {
                    if (face.loadGlyph(glyph_index, self.glyphLoadFlags(false))) {
                        const glyph = face.handle.*.glyph;
                        max = @max(
                            f26dot6ToF64(glyph.*.advance.x),
                            max,
                        );
                        const rect = getGlyphSize(glyph);
                        top = @max(rect.y + rect.height, top);
                        bottom = @min(rect.y, bottom);
                    } else |_| {}
                }
            }

            // If we couldn't get valid measurements, just use
            // FreeType's max_advance and null, respectively.
            if (max == 0.0) {
                max = f26dot6ToF64(size_metrics.max_advance);
            }
            const rect_height: ?f64 = rect_height: {
                const estimate = top - bottom;
                if (estimate <= 0.0) {
                    break :rect_height null;
                }
                break :rect_height estimate;
            };

            break :measurements .{ max, rect_height };
        };

        // We use the cap and ex heights specified by the font if they're
        // available, otherwise we try to measure the `H` and `x` glyphs.
        const cap_height: ?f64, const ex_height: ?f64 = heights: {
            if (os2_) |os2| {
                // The OS/2 table does not include these metrics in version 1.
                if (os2.version >= 2) {
                    break :heights .{
                        @as(f64, @floatFromInt(os2.sCapHeight)) * px_per_unit,
                        @as(f64, @floatFromInt(os2.sxHeight)) * px_per_unit,
                    };
                }
            }

            break :heights .{
                cap: {
                    self.ft_mutex.lock();
                    defer self.ft_mutex.unlock();
                    if (face.getCharIndex('H')) |glyph_index| {
                        if (face.loadGlyph(glyph_index, self.glyphLoadFlags(false))) {
                            break :cap getGlyphSize(face.handle.*.glyph).height;
                        } else |_| {}
                    }
                    break :cap null;
                },
                ex: {
                    self.ft_mutex.lock();
                    defer self.ft_mutex.unlock();
                    if (face.getCharIndex('x')) |glyph_index| {
                        if (face.loadGlyph(glyph_index, self.glyphLoadFlags(false))) {
                            break :ex getGlyphSize(face.handle.*.glyph).height;
                        } else |_| {}
                    }
                    break :ex null;
                },
            };
        };

        // Measure "水" (CJK water ideograph, U+6C34) for our ic width.
        const ic_width: ?f64 = ic_width: {
            self.ft_mutex.lock();
            defer self.ft_mutex.unlock();

            const glyph = face.getCharIndex('水') orelse break :ic_width null;

            face.loadGlyph(glyph, self.glyphLoadFlags(false)) catch break :ic_width null;

            const ft_glyph = face.handle.*.glyph;

            // If the advance of the glyph is less than the width of the actual
            // glyph then we just treat it as invalid since it's probably wrong
            // and using it for size normalization will instead make the font
            // way too big.
            //
            // This can sometimes happen if there's a CJK font that has been
            // patched with the nerd fonts patcher and it butchers the advance
            // values so the advance ends up half the width of the actual glyph.
            const ft_glyph_width = getGlyphSize(ft_glyph).width;
            const advance = f26dot6ToF64(ft_glyph.*.advance.x);
            if (ft_glyph_width > advance) {
                var buf: [1024]u8 = undefined;
                const font_name = self.name(&buf) catch "<Error getting font name>";
                log.warn(
                    "(getMetrics) Width of glyph '水' for font \"{s}\" is greater than its advance ({d} > {d}), discarding ic_width metric.",
                    .{ font_name, ft_glyph_width, advance },
                );
                break :ic_width null;
            }

            break :ic_width advance;
        };

        return .{
            .px_per_em = px_per_em,

            .cell_width = cell_width,

            .ascent = ascent,
            .descent = descent,
            .line_gap = line_gap,

            .underline_position = underline_position,
            .underline_thickness = underline_thickness,

            .strikethrough_position = strikethrough_position,
            .strikethrough_thickness = strikethrough_thickness,

            .cap_height = cap_height,
            .ex_height = ex_height,
            .ascii_height = ascii_height,
            .ic_width = ic_width,
        };
    }

    /// Copy the font table data for the given tag.
    pub fn copyTable(self: Face, alloc: Allocator, tag: *const [4]u8) !?[]u8 {
        return try self.face.loadSfntTable(alloc, freetype.Tag.init(tag));
    }
};

test {
    const testFont = font.embedded.inconsolata;
    const alloc = testing.allocator;

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

    var atlas = try font.Atlas.init(alloc, 512, .grayscale);
    defer atlas.deinit(alloc);

    var ft_font = try Face.init(
        lib,
        testFont,
        .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
    );
    defer ft_font.deinit();

    // Generate all visible ASCII
    var i: u8 = 32;
    while (i < 127) : (i += 1) {
        _ = try ft_font.renderGlyph(
            alloc,
            &atlas,
            ft_font.glyphIndex(i).?,
            .{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) },
        );
    }

    // Test resizing
    {
        const g1 = try ft_font.renderGlyph(
            alloc,
            &atlas,
            ft_font.glyphIndex('A').?,
            .{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) },
        );
        try testing.expectEqual(@as(u32, 11), g1.height);

        try ft_font.setSize(.{ .size = .{ .points = 24, .xdpi = 96, .ydpi = 96 } });
        const g2 = try ft_font.renderGlyph(
            alloc,
            &atlas,
            ft_font.glyphIndex('A').?,
            .{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) },
        );
        try testing.expectEqual(@as(u32, 21), g2.height);
    }
}

test "color emoji" {
    const alloc = testing.allocator;
    const testFont = font.embedded.emoji;

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

    var atlas = try font.Atlas.init(alloc, 512, .bgra);
    defer atlas.deinit(alloc);

    var ft_font = try Face.init(
        lib,
        testFont,
        .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
    );
    defer ft_font.deinit();

    _ = try ft_font.renderGlyph(
        alloc,
        &atlas,
        ft_font.glyphIndex('🥸').?,
        .{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) },
    );

    // Make sure this glyph has color
    {
        try testing.expect(ft_font.hasColor());
        const glyph_id = ft_font.glyphIndex('🥸').?;
        try testing.expect(ft_font.isColorGlyph(glyph_id));
    }
}

test "mono to bgra" {
    const alloc = testing.allocator;
    const testFont = font.embedded.emoji;

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

    var atlas = try font.Atlas.init(alloc, 512, .bgra);
    defer atlas.deinit(alloc);

    var ft_font = try Face.init(lib, testFont, .{ .size = .{ .points = 12, .xdpi = 72, .ydpi = 72 } });
    defer ft_font.deinit();

    // glyph 3 is mono in Noto
    _ = try ft_font.renderGlyph(
        alloc,
        &atlas,
        3,
        .{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) },
    );
}

test "svg font table" {
    const alloc = testing.allocator;
    const testFont = font.embedded.julia_mono;

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

    var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12, .xdpi = 72, .ydpi = 72 } });
    defer face.deinit();

    const table = (try face.copyTable(alloc, "SVG ")).?;
    defer alloc.free(table);

    try testing.expectEqual(430, table.len);
}

const terminus_i =
    \\........
    \\........
    \\...#....
    \\...#....
    \\........
    \\..##....
    \\...#....
    \\...#....
    \\...#....
    \\...#....
    \\...#....
    \\..###...
    \\........
    \\........
    \\........
    \\........
;
// Including the newline
const terminus_i_pitch = 9;

test "bitmap glyph" {
    const alloc = testing.allocator;
    const testFont = font.embedded.terminus_ttf;

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

    var atlas = try font.Atlas.init(alloc, 512, .grayscale);
    defer atlas.deinit(alloc);

    // Any glyph at 12pt @ 96 DPI is a bitmap
    var ft_font = try Face.init(lib, testFont, .{ .size = .{
        .points = 12,
        .xdpi = 96,
        .ydpi = 96,
    } });
    defer ft_font.deinit();

    // glyph 77 = 'i'
    const glyph = try ft_font.renderGlyph(
        alloc,
        &atlas,
        77,
        .{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) },
    );

    // should render crisp
    try testing.expectEqual(8, glyph.width);
    try testing.expectEqual(16, glyph.height);
    for (0..glyph.height) |y| {
        for (0..glyph.width) |x| {
            const pixel = terminus_i[y * terminus_i_pitch + x];
            try testing.expectEqual(
                @as(u8, if (pixel == '#') 255 else 0),
                atlas.data[(glyph.atlas_y + y) * atlas.size + (glyph.atlas_x + x)],
            );
        }
    }
}

// Expected pixel pattern for Spleen 8x16 'A' (glyph index from char 'A')
// Derived from BDF BITMAP data: 00,00,7C,C6,C6,C6,FE,C6,C6,C6,C6,C6,00,00,00,00
const spleen_A =
    \\........
    \\........
    \\.#####..
    \\##...##.
    \\##...##.
    \\##...##.
    \\#######.
    \\##...##.
    \\##...##.
    \\##...##.
    \\##...##.
    \\##...##.
    \\........
    \\........
    \\........
    \\........
;
// Including the newline
const spleen_A_pitch = 9;
// Test parameters for bitmap font tests
const spleen_test_point_size = 12;
const spleen_test_dpi = 96;

test "bitmap glyph BDF" {
    const alloc = testing.allocator;
    const testFont = font.embedded.spleen_bdf;

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

    var atlas = try font.Atlas.init(alloc, 512, .grayscale);
    defer atlas.deinit(alloc);

    // Spleen 8x16 is a pure bitmap font at 16px height
    var ft_font = try Face.init(lib, testFont, .{ .size = .{
        .points = spleen_test_point_size,
        .xdpi = spleen_test_dpi,
        .ydpi = spleen_test_dpi,
    } });
    defer ft_font.deinit();

    // Get glyph index for 'A'
    const glyph_index = ft_font.glyphIndex('A') orelse return error.GlyphNotFound;

    const glyph = try ft_font.renderGlyph(
        alloc,
        &atlas,
        glyph_index,
        .{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) },
    );

    // Verify dimensions match Spleen 8x16
    try testing.expectEqual(8, glyph.width);
    try testing.expectEqual(16, glyph.height);

    // Verify pixel-perfect rendering
    for (0..glyph.height) |y| {
        for (0..glyph.width) |x| {
            const pixel = spleen_A[y * spleen_A_pitch + x];
            try testing.expectEqual(
                @as(u8, if (pixel == '#') 255 else 0),
                atlas.data[(glyph.atlas_y + y) * atlas.size + (glyph.atlas_x + x)],
            );
        }
    }
}

test "bitmap glyph PCF" {
    const alloc = testing.allocator;
    const testFont = font.embedded.spleen_pcf;

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

    var atlas = try font.Atlas.init(alloc, 512, .grayscale);
    defer atlas.deinit(alloc);

    var ft_font = try Face.init(lib, testFont, .{ .size = .{
        .points = spleen_test_point_size,
        .xdpi = spleen_test_dpi,
        .ydpi = spleen_test_dpi,
    } });
    defer ft_font.deinit();

    const glyph_index = ft_font.glyphIndex('A') orelse return error.GlyphNotFound;

    const glyph = try ft_font.renderGlyph(
        alloc,
        &atlas,
        glyph_index,
        .{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) },
    );

    try testing.expectEqual(8, glyph.width);
    try testing.expectEqual(16, glyph.height);

    for (0..glyph.height) |y| {
        for (0..glyph.width) |x| {
            const pixel = spleen_A[y * spleen_A_pitch + x];
            try testing.expectEqual(
                @as(u8, if (pixel == '#') 255 else 0),
                atlas.data[(glyph.atlas_y + y) * atlas.size + (glyph.atlas_x + x)],
            );
        }
    }
}

test "bitmap glyph OTB" {
    const alloc = testing.allocator;
    const testFont = font.embedded.spleen_otb;

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

    var atlas = try font.Atlas.init(alloc, 512, .grayscale);
    defer atlas.deinit(alloc);

    var ft_font = try Face.init(lib, testFont, .{ .size = .{
        .points = spleen_test_point_size,
        .xdpi = spleen_test_dpi,
        .ydpi = spleen_test_dpi,
    } });
    defer ft_font.deinit();

    const glyph_index = ft_font.glyphIndex('A') orelse return error.GlyphNotFound;

    const glyph = try ft_font.renderGlyph(
        alloc,
        &atlas,
        glyph_index,
        .{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) },
    );

    try testing.expectEqual(8, glyph.width);
    try testing.expectEqual(16, glyph.height);

    for (0..glyph.height) |y| {
        for (0..glyph.width) |x| {
            const pixel = spleen_A[y * spleen_A_pitch + x];
            try testing.expectEqual(
                @as(u8, if (pixel == '#') 255 else 0),
                atlas.data[(glyph.atlas_y + y) * atlas.size + (glyph.atlas_x + x)],
            );
        }
    }
}
