const std = @import("std");
const assert = @import("../../quirks.zig").inlineAssert;
const testing = std.testing;
const Allocator = std.mem.Allocator;
const js = @import("zig-js");
const font = @import("../main.zig");

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

pub const Face = struct {
    /// See graphemes field for more details.
    const grapheme_start: u32 = 0x10FFFF + 1;
    const grapheme_end: u32 = std.math.maxInt(u32);

    /// The web canvas face makes use of an allocator when interacting
    /// with the JS environment.
    alloc: Allocator,

    /// The CSS "font" attribute, excluding size.
    font_str: []const u8,

    /// The size we currently have set.
    size: font.face.DesiredSize,

    /// The presentation for this font.
    presentation: font.Presentation,

    /// Metrics for this font face. These are useful for renderers.
    metrics: font.Metrics,

    /// The canvas element that we will reuse to render glyphs
    canvas: js.Object,

    /// The map to store multi-codepoint grapheme clusters that are rendered.
    /// We use 1 above the maximum unicode codepoint up to the max 32-bit
    /// unsigned integer to store the "glyph index" for graphemes.
    grapheme_to_glyph: std.StringHashMapUnmanaged(u32) = .{},
    glyph_to_grapheme: std.AutoHashMapUnmanaged(u32, []u8) = .{},
    grapheme_next: u32 = grapheme_start,

    /// Initialize a web canvas font with a "raw" value. The "raw" value can
    /// be any valid value for a CSS "font" property EXCLUDING the size. The
    /// size is always added via the `size` parameter.
    ///
    /// The raw value is copied so the caller can free it after it is gone.
    ///
    /// The presentation is given here directly because the browser gives
    /// us no easy way to determine the presentation we want for this font.
    /// Callers should just tell us what to expect.
    pub fn initNamed(
        alloc: Allocator,
        raw: []const u8,
        size: font.face.DesiredSize,
        presentation: font.Presentation,
    ) !Face {
        // Copy our font string because we're going to have to reuse it.
        const font_str = try alloc.dupe(u8, raw);
        errdefer alloc.free(font_str);

        // Create our canvas that we're going to continue to reuse.
        const doc = try js.global.get(js.Object, "document");
        defer doc.deinit();
        const canvas = try doc.call(js.Object, "createElement", .{js.string("canvas")});
        errdefer canvas.deinit();

        var result = Face{
            .alloc = alloc,
            .font_str = font_str,
            .size = size,
            .presentation = presentation,

            .canvas = canvas,

            // We're going to calculate these right after initialization.
            .metrics = undefined,
        };
        try result.calcMetrics();

        log.debug("face initialized: {s}", .{raw});
        return result;
    }

    pub fn deinit(self: *Face) void {
        self.alloc.free(self.font_str);
        self.grapheme_to_glyph.deinit(self.alloc);
        {
            var it = self.glyph_to_grapheme.valueIterator();
            while (it.next()) |value| self.alloc.free(value.*);
            self.glyph_to_grapheme.deinit(self.alloc);
        }
        self.canvas.deinit();
        self.* = undefined;
    }

    /// 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, size: font.face.DesiredSize) !void {
        const old = self.size;
        self.size = size;
        errdefer self.size = old;
        try self.calcMetrics();
    }

    /// Returns the glyph index for the given Unicode code point. For canvas,
    /// we support every glyph and the ID is just the codepoint since we don't
    /// have access to the underlying tables anyways. We let the browser deal
    /// with bad codepoints.
    pub fn glyphIndex(self: Face, cp: u32) ?u32 {
        // If this is a multi-codepoint grapheme then we only check if
        // we actually know about it.
        if (cp >= grapheme_start) {
            if (!self.glyph_to_grapheme.contains(cp)) return null;
        }

        // Render the glyph to determine if it is colored or not. We
        // have to do this because the browser will always try to render
        // whatever we give it and we have no API to determine color.
        //
        // We don't want to say yes to the wrong presentation because
        // it will go into the wrong Atlas.
        const p: font.Presentation = if (cp <= 255) .text else p: {
            break :p self.glyphPresentation(cp) catch {
                // In this case, we assume we are unable to render
                // this glyph and therefore just say we don't support it.
                return null;
            };
        };
        if (p != self.presentation) return null;

        return cp;
    }

    /// This determines the presentation of the glyph by literally
    /// inspecting the image data to look for any color. This isn't
    /// super performant but we don't have a better choice given the
    /// canvas APIs.
    fn glyphPresentation(
        self: Face,
        cp: u32,
    ) !font.Presentation {
        // Render the glyph
        var render = try self.renderGlyphInternal(self.alloc, cp);
        defer render.deinit();

        // Inspect the image data for any non-zeros in the RGB value.
        // NOTE(perf): this is an easy candidate for SIMD.
        var i: usize = 0;
        while (i < render.bitmap.len) : (i += 4) {
            if (render.bitmap[i] > 0 or
                render.bitmap[i + 1] > 0 or
                render.bitmap[i + 2] > 0) return .emoji;
        }

        return .text;
    }

    /// Returns the glyph index for the given grapheme cluster. The same
    /// cluster will always map to the same glyph index. This does not render
    /// the grapheme at this time, only reserves the index.
    pub fn graphemeGlyphIndex(self: *Face, cluster: []const u8) error{OutOfMemory}!u32 {
        // If we already have this stored then return it
        const gop = try self.grapheme_to_glyph.getOrPut(self.alloc, cluster);
        if (gop.found_existing) return gop.value_ptr.*;
        errdefer _ = self.grapheme_to_glyph.remove(cluster);

        // We don't have it stored. Ensure we have space to store. The
        // next will be "0" if we're out of space due to unsigned int wrapping.
        if (self.grapheme_next == 0) return error.OutOfMemory;

        // Copy the cluster for our reverse mapping
        const copy = try self.alloc.dupe(u8, cluster);
        errdefer self.alloc.free(copy);

        // Grow space for the reverse mapping
        try self.glyph_to_grapheme.ensureUnusedCapacity(self.alloc, 1);

        // Store it
        gop.value_ptr.* = self.grapheme_next;
        self.glyph_to_grapheme.putAssumeCapacity(self.grapheme_next, copy);

        self.grapheme_next +%= 1;
        return gop.value_ptr.*;
    }

    /// 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.face.RenderOptions,
    ) !font.Glyph {
        _ = opts;

        var render = try self.renderGlyphInternal(alloc, glyph_index);
        defer render.deinit();

        // Convert the format of the bitmap if necessary
        const bitmap_formatted: []u8 = switch (atlas.format) {
            // Bitmap is already in RGBA
            .rgba => render.bitmap,

            // Convert down to A8
            .grayscale => a8: {
                assert(@mod(render.bitmap.len, 4) == 0);
                var bitmap_a8 = try alloc.alloc(u8, render.bitmap.len / 4);
                errdefer alloc.free(bitmap_a8);
                var i: usize = 0;
                while (i < bitmap_a8.len) : (i += 1) {
                    bitmap_a8[i] = render.bitmap[(i * 4) + 3];
                }

                break :a8 bitmap_a8;
            },

            else => return error.UnsupportedAtlasFormat,
        };
        defer if (bitmap_formatted.ptr != render.bitmap.ptr) {
            alloc.free(bitmap_formatted);
        };

        // Put it in our atlas
        const region = try atlas.reserve(alloc, render.width, render.height);
        if (region.width > 0 and region.height > 0) {
            atlas.set(region, bitmap_formatted);
        }

        return font.Glyph{
            .width = render.width,
            .height = render.height,
            // TODO: this can't be right
            .offset_x = 0,
            .offset_y = 0,
            .atlas_x = region.x,
            .atlas_y = region.y,
        };
    }

    /// Calculate the metrics associated with a given face.
    fn calcMetrics(self: *Face) !void {
        const ctx = try self.context();
        defer ctx.deinit();

        // Cell width is the width of our M text
        const cell_width: f32 = cell_width: {
            const metrics = try ctx.call(js.Object, "measureText", .{js.string("M")});
            defer metrics.deinit();

            // We prefer the bounding box since it is tighter but certain
            // text such as emoji do not have a bounding box set so we use
            // the full run width instead.
            const bounding_right = try metrics.get(f32, "actualBoundingBoxRight");
            if (bounding_right > 0) break :cell_width bounding_right;
            break :cell_width try metrics.get(f32, "width");
        };

        // To get the cell height we render a high and low character and get
        // the total of the ascent and descent. This should equal our
        // pixel height but this is a more surefire way to get it.
        const height_metrics = try ctx.call(js.Object, "measureText", .{js.string("M_")});
        defer height_metrics.deinit();
        const asc = try height_metrics.get(f32, "actualBoundingBoxAscent");
        const desc = try height_metrics.get(f32, "actualBoundingBoxDescent");
        const cell_height = asc + desc;
        const cell_baseline = desc;

        // There isn't a declared underline position for canvas measurements
        // so we just go 1 under the cell height to match freetype logic
        // at this time (our freetype logic).
        const underline_position = cell_height - 1;
        const underline_thickness: f32 = 1;

        const result = font.Metrics{
            .cell_width = @intFromFloat(cell_width),
            .cell_height = @intFromFloat(cell_height),
            .cell_baseline = @intFromFloat(cell_baseline),
            .underline_position = @intFromFloat(underline_position),
            .underline_thickness = @intFromFloat(underline_thickness),
            .strikethrough_position = @intFromFloat(underline_position),
            .strikethrough_thickness = @intFromFloat(underline_thickness),
        };

        self.metrics = result;
        log.debug("metrics font={s} value={}", .{ self.font_str, self.metrics });
    }

    /// Returns the 2d context configured for drawing
    fn context(self: Face) !js.Object {
        // This will return the same context on subsequent calls so it
        // is important to reset it.
        const ctx = try self.canvas.call(js.Object, "getContext", .{js.string("2d")});
        errdefer ctx.deinit();

        // Clear the canvas
        {
            const width = try self.canvas.get(f64, "width");
            const height = try self.canvas.get(f64, "height");
            try ctx.call(void, "clearRect", .{ 0, 0, width, height });
        }

        // Set our context font
        const font_val = try std.fmt.allocPrint(
            self.alloc,
            "{d}px {s}",
            .{ self.size.points, self.font_str },
        );
        defer self.alloc.free(font_val);
        try ctx.set("font", js.string(font_val));

        // If the font property didn't change, then the font set didn't work.
        // We do this check because it is very easy to put an invalid font
        // in and this at least makes it show up in the logs.
        const check = try ctx.getAlloc(js.String, self.alloc, "font");
        defer self.alloc.free(check);
        if (!std.mem.eql(u8, font_val, check)) {
            log.warn("canvas font didn't set, fonts may be broken, expected={s} got={s}", .{
                font_val,
                check,
            });
        }

        return ctx;
    }

    /// An internal (web-canvas-only) format for rendered glyphs
    /// since we do render passes in multiple different situations.
    const RenderedGlyph = struct {
        alloc: Allocator,
        metrics: js.Object,
        width: u32,
        height: u32,
        bitmap: []u8,

        pub fn deinit(self: *RenderedGlyph) void {
            self.metrics.deinit();
            self.alloc.free(self.bitmap);
            self.* = undefined;
        }
    };

    /// Shared logic for rendering a glyph.
    fn renderGlyphInternal(
        self: Face,
        alloc: Allocator,
        glyph_index: u32,
    ) !RenderedGlyph {
        // Encode our glyph to UTF-8 so we can build a JS string out of it.
        var utf8: [4]u8 = undefined;
        const glyph_str = glyph_str: {
            // If we are a normal glyph then we are a single codepoint and
            // we just UTF8 encode it as-is.
            if (glyph_index < grapheme_start) {
                const utf8_len = try std.unicode.utf8Encode(@intCast(glyph_index), &utf8);
                break :glyph_str js.string(utf8[0..utf8_len]);
            }

            // We are a multi-codepoint glyph so we have to read the glyph
            // from the map and it is already utf8 encoded.
            const slice = self.glyph_to_grapheme.get(glyph_index) orelse
                return error.UnknownGraphemeCluster;
            break :glyph_str js.string(slice);
        };

        // Get our drawing context
        const measure_ctx = try self.context();
        defer measure_ctx.deinit();

        // Get the width and height of the render
        const metrics = try measure_ctx.call(js.Object, "measureText", .{glyph_str});
        errdefer metrics.deinit();
        const width: u32 = @as(u32, @intFromFloat(@ceil(width: {
            // We prefer the bounding box since it is tighter but certain
            // text such as emoji do not have a bounding box set so we use
            // the full run width instead.
            const bounding_right = try metrics.get(f32, "actualBoundingBoxRight");
            if (bounding_right > 0) break :width bounding_right;
            break :width try metrics.get(f32, "width");
        }))) + 1;

        const left = try metrics.get(f32, "actualBoundingBoxLeft");
        const asc = try metrics.get(f32, "actualBoundingBoxAscent");
        const desc = try metrics.get(f32, "actualBoundingBoxDescent");

        // On Firefox on Linux, the bounding box is broken in some cases for
        // ideographic glyphs (such as emoji). We detect this and behave
        // differently.
        const broken_bbox = asc + desc < 0.001;

        // Height is our ascender + descender for this char
        const height = if (!broken_bbox) @as(u32, @intFromFloat(@ceil(asc + desc))) + 1 else width;

        // Note: width and height both get "+ 1" added to them above. This
        // is important so that there is a 1px border around the glyph to avoid
        // any clipping in the atlas.

        // Resize canvas to match the glyph size exactly
        {
            try self.canvas.set("width", width);
            try self.canvas.set("height", height);

            const width_str = try std.fmt.allocPrint(alloc, "{d}px", .{width});
            defer alloc.free(width_str);
            const height_str = try std.fmt.allocPrint(alloc, "{d}px", .{height});
            defer alloc.free(height_str);

            const style = try self.canvas.get(js.Object, "style");
            defer style.deinit();
            try style.set("width", js.string(width_str));
            try style.set("height", js.string(height_str));
        }

        // Reload our context since we resized the canvas
        const ctx = try self.context();
        defer ctx.deinit();

        // For the broken bounding box case we render ideographic baselines
        // so that we just render the glyph fully in the box with no offsets.
        if (broken_bbox) {
            try ctx.set("textBaseline", js.string("ideographic"));
        }

        // Draw background
        try ctx.set("fillStyle", js.string("transparent"));
        try ctx.call(void, "fillRect", .{
            0,
            0,
            width,
            height,
        });

        // Draw glyph
        try ctx.set("fillStyle", js.string("black"));
        try ctx.call(void, "fillText", .{
            glyph_str,
            left + 1,
            if (!broken_bbox) asc + 1 else @as(f32, @floatFromInt(height)),
        });

        // Read the image data and get it into a []u8 on our side
        const bitmap: []u8 = bitmap: {
            // Read the raw bitmap data and get the "data" value which is a
            // Uint8ClampedArray.
            const data = try ctx.call(js.Object, "getImageData", .{ 0, 0, width, height });
            defer data.deinit();
            const src_array = try data.get(js.Object, "data");
            defer src_array.deinit();

            // Allocate our local memory to copy the data to.
            const len = try src_array.get(u32, "length");
            const bitmap = try alloc.alloc(u8, @intCast(len));
            errdefer alloc.free(bitmap);

            // Create our target Uint8Array that we can use to copy from src.
            const mem_array = mem_array: {
                // Get our runtime memory
                const mem = try js.runtime.get(js.Object, "memory");
                defer mem.deinit();
                const buf = try mem.get(js.Object, "buffer");
                defer buf.deinit();

                // Construct our array to peer into our memory
                const Uint8Array = try js.global.get(js.Object, "Uint8Array");
                defer Uint8Array.deinit();
                const mem_array = try Uint8Array.new(.{ buf, bitmap.ptr });
                errdefer mem_array.deinit();

                break :mem_array mem_array;
            };
            defer mem_array.deinit();

            // Copy
            try mem_array.call(void, "set", .{src_array});

            break :bitmap bitmap;
        };
        errdefer alloc.free(bitmap);

        return RenderedGlyph{
            .alloc = alloc,
            .metrics = metrics,
            .width = width,
            .height = height,
            .bitmap = bitmap,
        };
    }
};

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

    export fn face_new(ptr: [*]const u8, len: usize, pts: u16, p: u16) ?*Face {
        return face_new_(ptr, len, pts, p) catch null;
    }

    fn face_new_(ptr: [*]const u8, len: usize, pts: f32, presentation: u16) !*Face {
        var face = try Face.initNamed(
            alloc,
            ptr[0..len],
            .{ .points = pts },
            @enumFromInt(presentation),
        );
        errdefer face.deinit();

        const result = try alloc.create(Face);
        errdefer alloc.destroy(result);
        result.* = face;
        return result;
    }

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

    /// Resulting pointer must be freed using the global "free".
    export fn face_render_glyph(
        face: *Face,
        atlas: *font.Atlas,
        codepoint: u32,
    ) ?*font.Glyph {
        return face_render_glyph_(face, atlas, codepoint) catch |err| {
            log.warn("error rendering glyph err={}", .{err});
            return null;
        };
    }

    export fn face_debug_canvas(face: *Face) void {
        face_debug_canvas_(face) catch |err| {
            log.warn("error adding debug canvas err={}", .{err});
        };
    }

    fn face_debug_canvas_(face: *Face) !void {
        const doc = try js.global.get(js.Object, "document");
        defer doc.deinit();

        const elem = try doc.call(
            ?js.Object,
            "getElementById",
            .{js.string("face-canvas")},
        ) orelse return error.CanvasContainerNotFound;
        defer elem.deinit();

        try elem.call(void, "append", .{face.canvas});
    }

    fn face_render_glyph_(face: *Face, atlas: *font.Atlas, codepoint: u32) !*font.Glyph {
        const glyph = try face.renderGlyph(alloc, atlas, codepoint, .{});

        const result = try alloc.create(font.Glyph);
        errdefer alloc.destroy(result);
        _ = try wasm.toHostOwned(result);
        result.* = glyph;
        return result;
    }
};
