describe('Font Behavior', () => {
    let container;

    const assetLoadingPromises = [];
    let notoSerifFontBuffer = null;
    // This font is known to support kerning
    assetLoadingPromises.push(fetch('/assets/NotoSerif-Regular.ttf').then(
        (response) => response.arrayBuffer()).then(
        (buffer) => {
            notoSerifFontBuffer = buffer;
        }));

    let bungeeFontBuffer = null;
    // This font has tofu for incorrect null terminators
    // see https://bugs.chromium.org/p/skia/issues/detail?id=9314
    assetLoadingPromises.push(fetch('/assets/Bungee-Regular.ttf').then(
        (response) => response.arrayBuffer()).then(
        (buffer) => {
            bungeeFontBuffer = buffer;
        }));

    let colrv1FontBuffer = null;
    // This font has glyphs for COLRv1. Also used in gms/colrv1.cpp
    assetLoadingPromises.push(fetch('/assets/test_glyphs-glyf_colr_1.ttf').then(
        (response) => response.arrayBuffer()).then(
        (buffer) => {
            colrv1FontBuffer = buffer;
        }));

    beforeEach(async () => {
        await EverythingLoaded;
        await Promise.all(assetLoadingPromises);
        container = document.createElement('div');
        container.innerHTML = `
            <canvas width=600 height=600 id=test></canvas>
            <canvas width=600 height=600 id=report></canvas>`;
        document.body.appendChild(container);
    });

    afterEach(() => {
        document.body.removeChild(container);
    });

    gm('monospace_text_on_path', (canvas) => {
        const paint = new CanvasKit.Paint();
        paint.setAntiAlias(true);
        paint.setStyle(CanvasKit.PaintStyle.Stroke);

        const font = new CanvasKit.Font(null, 24);
        const fontPaint = new CanvasKit.Paint();
        fontPaint.setAntiAlias(true);
        fontPaint.setStyle(CanvasKit.PaintStyle.Fill);


        const arc = new CanvasKit.Path();
        arc.arcToOval(CanvasKit.LTRBRect(20, 40, 280, 300), -160, 140, true);
        arc.lineTo(210, 140);
        arc.arcToOval(CanvasKit.LTRBRect(20, 0, 280, 260), 160, -140, true);

        // Only 1 dot should show up in the image, because we run out of path.
        const str = 'This téxt should follow the curve across contours...';
        const textBlob = CanvasKit.TextBlob.MakeOnPath(str, arc, font);

        canvas.drawPath(arc, paint);
        canvas.drawTextBlob(textBlob, 0, 0, fontPaint);

        textBlob.delete();
        arc.delete();
        paint.delete();
        font.delete();
        fontPaint.delete();
    });

    gm('serif_text_on_path', (canvas) => {
        const notoSerif = CanvasKit.Typeface.MakeFreeTypeFaceFromData(notoSerifFontBuffer);

        const paint = new CanvasKit.Paint();
        paint.setAntiAlias(true);
        paint.setStyle(CanvasKit.PaintStyle.Stroke);

        const font = new CanvasKit.Font(notoSerif, 24);
        const fontPaint = new CanvasKit.Paint();
        fontPaint.setAntiAlias(true);
        fontPaint.setStyle(CanvasKit.PaintStyle.Fill);

        const arc = new CanvasKit.Path();
        arc.arcToOval(CanvasKit.LTRBRect(20, 40, 280, 300), -160, 140, true);
        arc.lineTo(210, 140);
        arc.arcToOval(CanvasKit.LTRBRect(20, 0, 280, 260), 160, -140, true);

        const str = 'This téxt should follow the curve across contours...';
        const textBlob = CanvasKit.TextBlob.MakeOnPath(str, arc, font, 60.5);

        canvas.drawPath(arc, paint);
        canvas.drawTextBlob(textBlob, 0, 0, fontPaint);

        textBlob.delete();
        arc.delete();
        paint.delete();
        notoSerif.delete();
        font.delete();
        fontPaint.delete();
    });

    // https://bugs.chromium.org/p/skia/issues/detail?id=9314
    gm('nullterminators_skbug_9314', (canvas) => {
        const bungee = CanvasKit.Typeface.MakeFreeTypeFaceFromData(bungeeFontBuffer);

        // yellow, to make sure tofu is plainly visible
        canvas.clear(CanvasKit.Color(255, 255, 0, 1));

        const font = new CanvasKit.Font(bungee, 24);
        const fontPaint = new CanvasKit.Paint();
        fontPaint.setAntiAlias(true);
        fontPaint.setStyle(CanvasKit.PaintStyle.Fill);


        const str = 'This is téxt';
        const textBlob = CanvasKit.TextBlob.MakeFromText(str + ' text blob', font);

        canvas.drawTextBlob(textBlob, 10, 50, fontPaint);

        canvas.drawText(str + ' normal', 10, 100, fontPaint, font);

        canvas.drawText('null terminator ->\u0000<- on purpose', 10, 150, fontPaint, font);

        textBlob.delete();
        bungee.delete();
        font.delete();
        fontPaint.delete();
    });

    gm('textblobs_with_glyphs', (canvas) => {
        const notoSerif = CanvasKit.Typeface.MakeFreeTypeFaceFromData(notoSerifFontBuffer);

        const font = new CanvasKit.Font(notoSerif, 24);
        const bluePaint = new CanvasKit.Paint();
        bluePaint.setColor(CanvasKit.parseColorString('#04083f')); // arbitrary deep blue
        bluePaint.setAntiAlias(true);
        bluePaint.setStyle(CanvasKit.PaintStyle.Fill);

        const redPaint = new CanvasKit.Paint();
        redPaint.setColor(CanvasKit.parseColorString('#770b1e')); // arbitrary deep red

        const ids = notoSerif.getGlyphIDs('AEGIS ægis');
        expect(ids.length).toEqual(10); // one glyph id per glyph
        expect(ids[0]).toEqual(36); // spot check this, should be consistent as long as the font is.

        const bounds = font.getGlyphBounds(ids, bluePaint);
        expect(bounds.length).toEqual(40); // 4 measurements per glyph
        expect(bounds[0]).toEqual(0); // again, spot check the measurements for the first glyph.
        expect(bounds[1]).toEqual(-17);
        expect(bounds[2]).toEqual(17);
        expect(bounds[3]).toEqual(0);

        const widths = font.getGlyphWidths(ids, bluePaint);
        expect(widths.length).toEqual(10); // 1 width per glyph
        expect(widths[0]).toEqual(17);

        const topBlob = CanvasKit.TextBlob.MakeFromGlyphs(ids, font);
        canvas.drawTextBlob(topBlob, 5, 30, bluePaint);
        canvas.drawTextBlob(topBlob, 5, 60, redPaint);
        topBlob.delete();

        const mIDs = CanvasKit.MallocGlyphIDs(ids.length);
        const mArr = mIDs.toTypedArray();
        mArr.set(ids);

        const mXforms = CanvasKit.Malloc(Float32Array, ids.length * 4);
        const mXformsArr = mXforms.toTypedArray();
        // Draw each glyph rotated slightly and slightly lower than the glyph before it.
        let currX = 0;
        for (let i = 0; i < ids.length; i++) {
            mXformsArr[i * 4] = Math.cos(-Math.PI / 16); // scos
            mXformsArr[i * 4 + 1] = Math.sin(-Math.PI / 16); // ssin
            mXformsArr[i * 4 + 2] = currX; // tx
            mXformsArr[i * 4 + 3] = i*2; // ty
            currX += widths[i];
        }

        const bottomBlob = CanvasKit.TextBlob.MakeFromRSXformGlyphs(mIDs, mXforms, font);
        canvas.drawTextBlob(bottomBlob, 5, 110, bluePaint);
        canvas.drawTextBlob(bottomBlob, 5, 140, redPaint);
        bottomBlob.delete();

        CanvasKit.Free(mIDs);
        CanvasKit.Free(mXforms);
        bluePaint.delete();
        redPaint.delete();
        notoSerif.delete();
        font.delete();
    });

    it('can make a font mgr with passed in fonts', () => {
        // CanvasKit.FontMgr.FromData([bungeeFontBuffer, notoSerifFontBuffer]) also works
        const fontMgr = CanvasKit.FontMgr.FromData(bungeeFontBuffer, notoSerifFontBuffer);
        expect(fontMgr).toBeTruthy();
        expect(fontMgr.countFamilies()).toBe(2);
        // in debug mode, let's list them.
        if (fontMgr.dumpFamilies) {
            fontMgr.dumpFamilies();
        }
        fontMgr.delete();
    });

    it('can make a font provider with passed in fonts and aliases', () => {
        const fontProvider = CanvasKit.TypefaceFontProvider.Make();
        fontProvider.registerFont(bungeeFontBuffer, "My Bungee Alias");
        fontProvider.registerFont(notoSerifFontBuffer, "My Noto Serif Alias");
        expect(fontProvider).toBeTruthy();
        expect(fontProvider.countFamilies()).toBe(2);
        // in debug mode, let's list them.
        if (fontProvider.dumpFamilies) {
            fontProvider.dumpFamilies();
        }
        fontProvider.delete();
    });

    gm('various_font_formats', (canvas, fetchedByteBuffers) => {
        const fontPaint = new CanvasKit.Paint();
        fontPaint.setAntiAlias(true);
        fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
        const inputs = [{
            type: '.ttf font',
            buffer: bungeeFontBuffer,
            y: 60,
        },{
            type: '.otf font',
            buffer: fetchedByteBuffers[0],
            y: 90,
        },{
            type: '.woff font',
            buffer: fetchedByteBuffers[1],
            y: 120,
        },{
            type: '.woff2 font',
            buffer: fetchedByteBuffers[2],
            y: 150,
        }];

        const defaultFont = new CanvasKit.Font(null, 24);
        canvas.drawText(`The following should be ${inputs.length + 1} lines of text:`, 5, 30, fontPaint, defaultFont);

        for (const fontType of inputs) {
            // smoke test that the font bytes loaded.
            expect(fontType.buffer).toBeTruthy(fontType.type + ' did not load');

            const typeface = CanvasKit.Typeface.MakeFreeTypeFaceFromData(fontType.buffer);
            const font = new CanvasKit.Font(typeface, 24);

            if (font && typeface) {
                canvas.drawText(fontType.type + ' loaded', 5, fontType.y, fontPaint, font);
            } else {
                canvas.drawText(fontType.type + ' *not* loaded', 5, fontType.y, fontPaint, defaultFont);
            }
            font && font.delete();
            typeface && typeface.delete();
        }

        // The only ttc font I could find was 14 MB big, so I'm using the smaller test font,
        // which doesn't have very many glyphs in it, so we just check that we got a non-zero
        // typeface for it. I was able to load NotoSansCJK-Regular.ttc just fine in a
        // manual test.
        const typeface = CanvasKit.Typeface.MakeFreeTypeFaceFromData(fetchedByteBuffers[3]);
        expect(typeface).toBeTruthy('.ttc font');
        if (typeface) {
            canvas.drawText('.ttc loaded', 5, 180, fontPaint, defaultFont);
            typeface.delete();
        } else {
            canvas.drawText('.ttc *not* loaded', 5, 180, fontPaint, defaultFont);
        }

        defaultFont.delete();
        fontPaint.delete();
    }, '/assets/Roboto-Regular.otf', '/assets/Roboto-Regular.woff', '/assets/Roboto-Regular.woff2', '/assets/test.ttc');

    it('can measure text very precisely with proper settings', () => {
        const typeface = CanvasKit.Typeface.MakeFreeTypeFaceFromData(notoSerifFontBuffer);
        const fontSizes = [257, 100, 11];
        // The point of these values is to let us know 1) we can measure to sub-pixel levels
        // and 2) that measurements don't drastically change. If these change a little bit,
        // just update them with the new values. For super-accurate readings, one could
        // run a C++ snippet of code and compare the values, but that is likely unnecessary
        // unless we suspect a bug with the bindings.
        const expectedSizes = [241.06299, 93.79883, 10.31787];
        for (const idx in fontSizes) {
            const font = new CanvasKit.Font(typeface, fontSizes[idx]);
            font.setHinting(CanvasKit.FontHinting.None);
            font.setLinearMetrics(true);
            font.setSubpixel(true);

            const ids = font.getGlyphIDs('M');
            const widths = font.getGlyphWidths(ids);
            expect(widths[0]).toBeCloseTo(expectedSizes[idx], 5);
            font.delete();
        }

        typeface.delete();
    });

    gm('font_edging', (canvas) => {
        // Draw a small font scaled up to see the aliasing artifacts.
        canvas.scale(8, 8);
        const notoSerif = CanvasKit.Typeface.MakeFreeTypeFaceFromData(notoSerifFontBuffer);

        const textPaint = new CanvasKit.Paint();
        const annotationFont = new CanvasKit.Font(notoSerif, 6);

        canvas.drawText('Default', 5, 5, textPaint, annotationFont);
        canvas.drawText('Alias', 5, 25, textPaint, annotationFont);
        canvas.drawText('AntiAlias', 5, 45, textPaint, annotationFont);
        canvas.drawText('Subpixel', 5, 65, textPaint, annotationFont);

        const testFont = new CanvasKit.Font(notoSerif, 20);

        canvas.drawText('SEA', 35, 15, textPaint, testFont);
        testFont.setEdging(CanvasKit.FontEdging.Alias);
        canvas.drawText('SEA', 35, 35, textPaint, testFont);
        testFont.setEdging(CanvasKit.FontEdging.AntiAlias);
        canvas.drawText('SEA', 35, 55, textPaint, testFont);
        testFont.setEdging(CanvasKit.FontEdging.SubpixelAntiAlias);
        canvas.drawText('SEA', 35, 75, textPaint, testFont);

        textPaint.delete();
        annotationFont.delete();
        testFont.delete();
        notoSerif.delete();
    });

    it('can get the intercepts of glyphs', () => {
        const font = new CanvasKit.Font(null, 100);
        const ids = font.getGlyphIDs('I');
        expect(ids.length).toEqual(1);

        // aim for the middle of the I at 100 point, expecting a hit
        let sects = font.getGlyphIntercepts(ids, [0, 0], -60, -40);
        expect(sects.length).toEqual(2, "expected one pair of intercepts");
        expect(sects[0]).toBeCloseTo(25.39063, 5);
        expect(sects[1]).toBeCloseTo(34.52148, 5);

        // aim below the baseline where we expect no intercepts
        sects = font.getGlyphIntercepts(ids, [0, 0], 20, 30);
        expect(sects.length).toEqual(0, "expected no intercepts");
        font.delete();
    });

    it('can use mallocd and normal arrays', () => {
        const font = new CanvasKit.Font(null, 100);
        const ids = font.getGlyphIDs('I');
        expect(ids.length).toEqual(1);
        const glyphID = ids[0];

        // aim for the middle of the I at 100 point, expecting a hit
        const sects = font.getGlyphIntercepts(Array.of(glyphID), Float32Array.of(0, 0), -60, -40);
        expect(sects.length).toEqual(2);
        expect(sects[0]).toBeLessThan(sects[1]);
        // these values were recorded from the first time it was run
        expect(sects[0]).toBeCloseTo(25.39063, 5);
        expect(sects[1]).toBeCloseTo(34.52148, 5);

        const free_list = [];   // will free CanvasKit.Malloc objects at the end

        // Want to exercise 4 different ways we can receive an array:
        //  1. normal array
        //  2. typed-array
        //  3. CanvasKit.Malloc typeed-array
        //  4. CavnasKit.Malloc (raw)

        const id_makers = [
            (id) => [ id ],
            (id) => new Uint16Array([ id ]),
            (id) => {
                const a = CanvasKit.Malloc(Uint16Array, 1);
                free_list.push(a);
                const ta = a.toTypedArray();
                ta[0] = id;
                return ta;  // return typed-array
            },
            (id) => {
                const a = CanvasKit.Malloc(Uint16Array, 1);
                free_list.push(a);
                a.toTypedArray()[0] = id;
                return a;   // return raw obj
            },
        ];
        const pos_makers = [
            (x, y) => [ x, y ],
            (x, y) => new Float32Array([ x, y ]),
            (x, y) => {
                const a = CanvasKit.Malloc(Float32Array, 2);
                free_list.push(a);
                const ta = a.toTypedArray();
                ta[0] = x;
                ta[1] = y;
                return ta;  // return typed-array
            },
            (x, y) => {
                const a = CanvasKit.Malloc(Float32Array, 2);
                free_list.push(a);
                const ta = a.toTypedArray();
                ta[0] = x;
                ta[1] = y;
                return a;   // return raw obj
            },
        ];

        for (const idm of id_makers) {
            for (const posm of pos_makers) {
                const s = font.getGlyphIntercepts(idm(glyphID), posm(0, 0), -60, -40);
                expect(s.length).toEqual(sects.length);
                for (let i = 0; i < s.length; ++i) {
                    expect(s[i]).toEqual(sects[i]);
                }
            }

        }

        free_list.forEach(obj => CanvasKit.Free(obj));
        font.delete();
    });

    gm('colrv1_gradients', (canvas) => {
        // Inspired by gm/colrv1.cpp, specifically the kColorFontsRepoGradients one.
        const colrFace = CanvasKit.Typeface.MakeFreeTypeFaceFromData(colrv1FontBuffer);

        const textPaint = new CanvasKit.Paint();
        const annotationFont = new CanvasKit.Font(null, 20);

        canvas.drawText('You should see 4 lines of gradient glyphs below',
            5, 25, textPaint, annotationFont);

        // These glyphs show off gradients in the COLRv1 font.
        // See https://github.com/googlefonts/color-fonts/blob/main/glyph_descriptions.md for
        // the list of available test glyphs and their codepoints.
        const testCodepoints = "\u{F0200} \u{F0100} \u{F0101} \u{F0102} \u{F0103} \u{F0D00}";
        const testFont = new CanvasKit.Font(colrFace);
        const sizes = [12, 18, 30, 100];
        let y = 30;
        for (let i = 0; i < sizes.length; i++) {
            const size = sizes[i];
            testFont.setSize(size);
            const metrics = testFont.getMetrics();
            y -= metrics.ascent;
            canvas.drawText(testCodepoints, 5, y, textPaint, testFont);
            y += metrics.descent + metrics.leading;
        }

        textPaint.delete();
        annotationFont.delete();
        testFont.delete();
        colrFace.delete();
    });
});
