/* Copyright 2022-2024 Jaakko Keränen <jaakko.keranen@iki.fi>

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

#include "render.h"
#include "canvas.h"
#include "video.h"
#include "events.h"
#include "video.h"
#include "SDL_keyboard.h"

#include <the_Foundation/math.h>

static SDL_Renderer *renderer_;

void SDL_ShowCursor(void); /* keyboard.c */

#define NUM_VGA_COLORS      8
#define FIRST_COLOR         NUM_VGA_COLORS
#define FIRST_COLOR_PAIR    1

iDeclareType(ColorPair)

struct Impl_ColorPair {
    uint8_t col[2];
};

struct SDL_Renderer_s {
    SDL_Window   *window;
    SDL_Texture  *defaultTarget;
    SDL_Texture  *target;
    SDL_Color     drawColor;
    SDL_ColorId   drawColorId;
    SDL_Color     textColor;
    SDL_ColorId   textColorId;
    SDL_Color     textFillColor;
    SDL_ColorId   textFillColorId;
    int           textAttribs;
    SDL_BlendMode blend;
    SDL_Color     palette[256];
    uint16_t      numPalette;
    iColorPair    colorPairs[256];
    uint16_t      numColorPairs;
};

#define COLOR_RESET_THRESHOLD   255

static void SDL_ResetColors(SDL_Renderer *d) {
    d->numPalette    = FIRST_COLOR;
    d->numColorPairs = FIRST_COLOR_PAIR;
    SDL_PushEvent(&(SDL_Event){ .type = SDL_RENDER_DEVICE_RESET });
}

iDeclareType(HSLColor)

struct Impl_HSLColor {
    float hue;
    float sat;
    float lum;
    float alpha;
};

iLocalDef iFloat4 normalize_(SDL_Color d) {
    return divf_F4(init_F4(d.r, d.g, d.b, d.a), 255.0f);
}

static iHSLColor rgbToHSL_(SDL_Color color) {
    float rgb[4];
    store_F4(normalize_(color), rgb);
    int compMax, compMin;
    if (rgb[0] >= rgb[1] && rgb[0] >= rgb[2]) {
        compMax = 0;
    }
    else if (rgb[1] >= rgb[0] && rgb[1] >= rgb[2]) {
        compMax = 1;
    }
    else {
        compMax = 2;
    }
    if (rgb[0] <= rgb[1] && rgb[0] <= rgb[2]) {
        compMin = 0;
    }
    else if (rgb[1] <= rgb[0] && rgb[1] <= rgb[2]) {
        compMin = 1;
    }
    else {
        compMin = 2;
    }
    const float rgbMax = rgb[compMax];
    const float rgbMin = rgb[compMin];
    const float lum = (rgbMax + rgbMin) / 2.0f;
    float hue = 0.0f;
    float sat = 0.0f;
    if (fabsf(rgbMax - rgbMin) > 0.00001f) {
        float chr = rgbMax - rgbMin;
        sat = chr / (1.0f - fabsf(2.0f * lum - 1.0f));
        if (compMax == 0) {
            hue = (rgb[1] - rgb[2]) / chr + (rgb[1] < rgb[2] ? 6 : 0);
        }
        else if (compMax == 1) {
            hue = (rgb[2] - rgb[0]) / chr + 2;
        }
        else {
            hue = (rgb[0] - rgb[1]) / chr + 4;
        }
    }
    return (iHSLColor){ hue * 60, sat, lum, rgb[3] }; /* hue in degrees */
}

static const SDL_Color vgaPalette_[] = {
    { 0,   0,   0,   255 },
    { 170, 0,   0,   255 },
    { 0,   170, 0,   255 },
    { 170, 85,  0,   255 },
    { 0,   0,   170, 255 },
    { 170, 0,   170, 255 },
    { 0,   170, 170, 255 },
    { 170, 170, 170, 255 },
    /* High-intensity colors are activated via A_BOLD */
    { 85,  85,  85,  255 },
    { 255, 85,  85,  255 },
    { 85,  255, 85,  255 },
    { 255, 255, 85,  255 },
    { 85,  85,  255, 255 },
    { 255, 85,  255, 255 },
    { 85,  255, 255, 255 },
    { 255, 255, 255, 255 },
};

static const iHSLColor vgaPaletteHsl_[] = {
    {   0.000000f, 0.000000f, 0.000000f },
    {   0.000000f, 1.000000f, 0.333333f },
    { 120.000000f, 1.000000f, 0.333333f },
    {  30.000000f, 1.000000f, 0.333333f },
    { 240.000000f, 1.000000f, 0.333333f },
    { 300.000000f, 1.000000f, 0.333333f },
    { 180.000000f, 1.000000f, 0.333333f },
    {   0.000000f, 0.000000f, 0.666667f },
    {   0.000000f, 0.000000f, 0.333333f },
    {   0.000000f, 1.000000f, 0.666667f },
    { 120.000000f, 1.000000f, 0.666667f },
    {  60.000000f, 1.000000f, 0.666667f },
    { 240.000000f, 1.000000f, 0.666667f },
    { 300.000000f, 1.000000f, 0.666667f },
    { 180.000000f, 1.000000f, 0.666667f },
    {   0.000000f, 0.000000f, 1.000000f },
};

static SDL_ColorId SDL_GetColorId(SDL_Renderer *d, SDL_Color rgb) {
    if (rgb.a != 255) {
        return transparent_VisChar;
    }
    if (isFixedVGAPalette_Video || !isColored_Video) {
        /* Must find the nearest match for the requested color. */
//        FILE *f = fopen("log.txt", "at");
        float minDelta = 1.0e9;
        SDL_ColorId id = 0;
        iHSLColor hsl = rgbToHSL_(rgb);
//        fprintf(f, "RGB:%d,%d,%d HSL:%f,%f,%f\n", rgb.r, rgb.g, rgb.b, hsl.hue, hsl.sat, hsl.lum);
        for (unsigned int i = 0; i < iElemCount(vgaPalette_); i++) {
            //const SDL_Color *pal = &vgaPalette_[i];
//            fprintf(f, "{ %10ff, %10ff, %10ff },\n", hsl.hue, hsl.lum, hsl.sat);
            const iHSLColor *pal = &vgaPaletteHsl_[i];
            float diff[3] = { pal->hue - hsl.hue,
                              iAbs(pal->sat - hsl.sat),
                              iAbs(pal->lum - hsl.lum) };
            if (diff[0] > 180) {
                diff[0] -= 360;
            }
            else if (diff[0] < -180) {
                diff[0] += 360;
            }
            diff[0] = iAbs(diff[0]) / 180.0f;
//            float delta;
//            diff[1] *= 2;
//            float lumCoeff = (hsl.lum < 0.5f ? 2 * hsl.lum : ((2 - 2 * hsl.lum)));
            float delta = diff[0] * diff[0] + diff[1] * diff[1] + diff[2] * diff[2];
//            fprintf(f, "  %i: (%f,%f,%f) %f\n", i, diff[0], diff[1], diff[2], delta);
            if (delta < minDelta) {
                minDelta = delta;
                id = i;
            }
        }
//        fprintf(f, "=> chose %d\n", id);
//        fclose(f);
        return id;
    }
    for (unsigned int i = FIRST_COLOR; i < d->numPalette; i++) {
        if (!memcmp(&rgb, &d->palette[i], 3)) {
            return i;
        }
    }
    if (d->numPalette == COLOR_RESET_THRESHOLD) {
        /* Out of colors, need to reset. The app needs to handle the reset event. */
        SDL_ResetColors(d);
        return FIRST_COLOR;
    }
    SDL_ColorId id = d->numPalette++;
    d->palette[id] = rgb;
    init_color(id, rgb.r * 1000 / 255, rgb.g * 1000 / 255, rgb.b * 1000 / 255);
    return d->numPalette - 1;
}

uint32_t SDL_GetColorPair(SDL_ColorId fg, SDL_ColorId bg) {
    iBool isBold = iFalse;
    if (isFixedVGAPalette_Video || !isColored_Video) {
        /* Ensure that the color pair is readable: either the fg or bg should be black. */
        if (fg >= 8) {
            isBold = iTrue;
            fg -= 8;
        }
        if (bg >= 8) {
            bg -= 8;
            if (bg != COLOR_BLACK && bg != COLOR_BLUE) {
                fg = COLOR_BLACK;
            }
        }
        if (bg != COLOR_BLACK) {
            fg = COLOR_BLACK;
            if (!isColored_Video) return A_REVERSE | (isBold ? A_BOLD : 0);
        }
        else {
            bg = COLOR_BLACK;
            if (!isColored_Video) return 0;
        }
        if (bg == COLOR_BLUE && fg == COLOR_BLACK) {
            fg = COLOR_WHITE;
        }
        else if (fg == COLOR_BLUE && bg == COLOR_BLACK) {
            fg = COLOR_CYAN;
        }
    }
    SDL_Renderer *d = renderer_;
    for (unsigned int i = FIRST_COLOR_PAIR; i < d->numColorPairs; i++) {
        if ((d->colorPairs[i].col[0] == fg || fg == transparent_VisChar) &&
            d->colorPairs[i].col[1] == bg) {
            return i;
        }
    }
    if (d->numColorPairs == COLOR_RESET_THRESHOLD) {
        SDL_ResetColors(d);
        return 0;
    }
    uint32_t index = d->numColorPairs++;
    d->colorPairs[index].col[0] = fg;
    d->colorPairs[index].col[1] = bg;
    init_pair(index, fg, bg);
    return index | (isBold ? A_BOLD : 0);
}

struct SDL_Texture_s {
    iCanvas       canvas;
    SDL_BlendMode blend;
    SDL_Color     colorMod;
};

static iCanvas *activeCanvas_(SDL_Renderer *d) {
    if (d->target) {
        return &d->target->canvas;
    }
    return &d->defaultTarget->canvas;
}

void SDL_ResizeTexture(SDL_Texture *d, iInt2 size) {
    resize_Canvas(&d->canvas, size);
}

SDL_Texture *SDL_CreateTexture(SDL_Renderer *render, int format, int access, int w, int h) {
    SDL_Texture *d = calloc(sizeof(SDL_Texture), 1);
    init_Canvas(&d->canvas, init_I2(w, h));
    d->blend = SDL_BLENDMODE_NONE;
    d->colorMod = (SDL_Color){ 255, 255, 255, 255 };
    return d;
}

SDL_Texture *SDL_CreateTextureFromSurface(SDL_Renderer *render, SDL_Surface *surface) {
    SDL_Texture *d = SDL_CreateTexture(render, 0, 0, surface->w, surface->h);
    return d;
}

void SDL_DestroyTexture(SDL_Texture *d) {
    if (d) {
        deinit_Canvas(&d->canvas);
        free(d);
    }
}

void SDL_SetTextureBlendMode(SDL_Texture *d, SDL_BlendMode mode) {
    if (d) {
        d->blend = mode;
    }
}

void SDL_SetTextureColorMod(SDL_Texture *d, uint8_t r, uint8_t g, uint8_t b) {
    if (d) {
        d->colorMod.r = r;
        d->colorMod.g = g;
        d->colorMod.b = b;
    }
}

void SDL_SetTextureAlphaMod(SDL_Texture *d, uint8_t alpha) {
    if (d) {
        d->colorMod.a = alpha;
    }
}

int SDL_QueryTexture(SDL_Texture *d, uint32_t *format, int *access, int *w, int *h) {
    if (d) {
        if (w) *w = d->canvas.size.x;
        if (h) *h = d->canvas.size.y;
        return SDL_TRUE;
    }
    return SDL_FALSE;
}

/*----------------------------------------------------------------------------------------------*/

void SDL_UpdateRendererToWindowSize(void) {
    SDL_Renderer *d = renderer_;
    if (d) {
        if (d->defaultTarget->canvas.size.x != d->window->w ||
            d->defaultTarget->canvas.size.y != d->window->h) {
            SDL_ResizeTexture(d->defaultTarget, init_I2(d->window->w, d->window->h));
        }
    }
}

SDL_Renderer *SDL_CreateRenderer(SDL_Window *window, int index, uint32_t flags) {
    SDL_Renderer *d    = calloc(sizeof(SDL_Renderer), 1);
    d->window          = window;
    d->defaultTarget   = SDL_CreateTexture(d, 0, 0, window->w, window->h);
    d->target          = d->defaultTarget;
    d->blend           = SDL_BLENDMODE_NONE;
    d->drawColor       = (SDL_Color){ 255, 255, 255, 255 };
    d->drawColorId     = transparent_VisChar;
    d->textColorId     = transparent_VisChar;
    d->textFillColorId = transparent_VisChar;
    d->textAttribs     = 0;
    /* Initialize the standard VGA palette that we assume everyone can use. Of course, terminals
       may use a custom palette but we can ignore that. */ {
        d->numPalette = NUM_VGA_COLORS;
    }
    /* We can't rely on the default colors being knowable. */
    d->numColorPairs = FIRST_COLOR_PAIR;
    renderer_        = d;
    return d;
}

int SDL_CreateWindowAndRenderer(int w, int h, int flags, SDL_Window **win_out,
                                SDL_Renderer **rend_out) {
    *win_out  = SDL_CreateWindow("", 0, 0, w, h, flags);
    *rend_out = SDL_CreateRenderer(*win_out, 0, 0);
    return SDL_TRUE;
}

void SDL_DestroyRenderer(SDL_Renderer *d) {
    if (d) {
        SDL_DestroyTexture(d->defaultTarget);
        free(d);
    }
    if (renderer_ == d) {
        renderer_ = NULL;
    }
}

int SDL_GetRendererInfo(SDL_Renderer *d, SDL_RendererInfo *info) {
    iZap(*info);
    info->name = "SEALCurses";
    return SDL_TRUE;
}

void SDL_GetRendererOutputSize(SDL_Renderer *d, int *w, int *h) {
    if (w) *w = d->window->w;
    if (h) *h = d->window->h;
}

void SDL_SetRenderDrawColor(SDL_Renderer *d, int r, int g, int b, int a) {
    d->drawColor = (SDL_Color){ r, g, b, a };
    d->drawColorId = SDL_GetColorId(d, d->drawColor);
}

void SDL_SetRenderDrawBlendMode(SDL_Renderer *d, SDL_BlendMode mode) {
    d->blend = mode;
}

void SDL_RenderClear(SDL_Renderer *d) {
    clear_Canvas(activeCanvas_(d), d->drawColorId);
}


void SDL_RenderPresent(SDL_Renderer *d) {
    curs_set(0);
    present_Canvas(&d->defaultTarget->canvas, d->window->cwin);
    wrefresh(d->window->cwin);
    if (SDL_IsTextInputStarted()) {
        SDL_ShowCursor();
        curs_set(1);
        wrefresh(d->window->cwin);
    }
}

void SDL_RenderCopy(SDL_Renderer *d, SDL_Texture *texture, const SDL_Rect *src,
                    const SDL_Rect *dst) {
    if (!texture) {
        return;
    }
    const iCanvas *srcCanvas = &texture->canvas;
    SDL_Rect fullSrcRect = { 0, 0, srcCanvas->size.x, srcCanvas->size.y };
    if (src == NULL) {
        src = &fullSrcRect;
    }
    blit_Canvas(activeCanvas_(d),
                &texture->canvas,
                init_Rect(src->x, src->y, src->w, src->h),
                dst ? init_I2(dst->x, dst->y) : zero_I2(),
                texture->blend);
}

int drawColor_(SDL_Renderer *d) {
    return (isFixedVGAPalette_Video ? COLOR_WHITE : d->drawColorId);
}

void SDL_RenderDrawLines(SDL_Renderer *d, const SDL_Point *points, uint32_t count) {
    /* This function is expected to be used only in a limited manner, to draw full or
       half rectangles. */
    iCanvas  *canvas      = activeCanvas_(d);
    const int drawColorId = drawColor_(d);
    if (count == 5) {
        /* A full rectangle. */
        iRect rect = init_Rect(
            points[0].x, points[0].y, points[2].x - points[0].x, points[2].y - points[0].y);
        if (rect.size.y >= 4) {
            drawRect_Canvas(canvas, rect, drawColorId);
        }
        else if (rect.size.y == 1) {
            setChar_Canvas(canvas, init_I2(points[0].x, points[0].y), '[', d->drawColorId);
            setChar_Canvas(canvas, init_I2(points[1].x - 1, points[1].y), ']', d->drawColorId);
        }
        else if (rect.size.y == 2) {
            setChar_Canvas(canvas, init_I2(points[0].x, points[0].y), 0x250c, d->drawColorId);
            setChar_Canvas(canvas, init_I2(points[1].x - 1, points[1].y), 0x2510, d->drawColorId);
            setChar_Canvas(canvas, init_I2(points[0].x, points[3].y - 1), 0x2514, d->drawColorId);
            setChar_Canvas(canvas, init_I2(points[1].x - 1, points[2].y - 1), 0x2518, d->drawColorId);
        }
    }
    else if (count == 3) {
        setChar_Canvas(canvas, init_I2(points[0].x, points[0].y), '|', d->drawColorId);
    }
    else if (count == 2 && points[1].x > points[0].x) {
        fillRectChar_Canvas(
            canvas,
            (iRect){ init_I2(points[0].x, points[0].y), init_I2(points[1].x - points[0].x, 1) },
            transparent_VisChar,
            0x2500 /* box drawings light horizontal */,
            drawColorId);
    }
    else if (count == 2 && points[1].y > points[0].y) {
        fillRectChar_Canvas(
            canvas,
            (iRect){ init_I2(points[0].x, points[0].y), init_I2(1, points[1].y - points[0].y) },
            transparent_VisChar,
            0x2502 /* box drawings light vertical */,
            drawColorId);
    }
}

void SDL_RenderFillRect(SDL_Renderer *d, const SDL_Rect *rect) {
    iCanvas *canvas = activeCanvas_(d);
    SDL_Rect fullRect;
    if (!rect) {
        fullRect = (SDL_Rect){ 0, 0, canvas->size.x, canvas->size.y };
        rect = &fullRect;
    }
    if (d->drawColor.a == 255 || d->blend == SDL_BLENDMODE_NONE) {
        if (rect->w == 1 && rect->h >= 3) {
            fillRectChar_Canvas(canvas, *(const iRect *) rect, transparent_VisChar,
                                0x2503 /* box drawings heavy vertical */, drawColor_(d));
        }
        else if (rect->h == 0 && rect->w > 1) {
            fillRectChar_Canvas(canvas, init_Rect(rect->x, rect->y, rect->w, 1),
                                transparent_VisChar, 0x2500 /* box drawings light horizontal */,
                                drawColor_(d));
        }
        else {
            fillRectChar_Canvas(canvas, init_Rect(rect->x, rect->y, rect->w, rect->h),
                                d->drawColorId, ' ', d->textColorId);
        }
    }
}

void SDL_RenderDrawRect(SDL_Renderer *d, const SDL_Rect *rect) {}

void SDL_RenderSetClipRect(SDL_Renderer *d, const SDL_Rect *clip) {
    if (clip) {
        setClip_Canvas(activeCanvas_(d), *(const iRect *) clip);
    }
    else {
        unsetClip_Canvas(activeCanvas_(d));
    }
}

SDL_Texture *SDL_GetRenderTarget(SDL_Renderer *d) {
    return d->target;
}

void SDL_SetRenderTarget(SDL_Renderer *d, SDL_Texture *texture) {
    d->target = texture;
}

void SDL_SetRenderTextColor(SDL_Renderer *d, int r, int g, int b) {
    d->textColor   = (SDL_Color){ r, g, b, 255 };
    d->textColorId = SDL_GetColorId(d, d->textColor);
}

void SDL_SetRenderTextFillColor(SDL_Renderer *d, int r, int g, int b, int a) {
    d->textFillColor   = (SDL_Color){ r, g, b, a };
    d->textFillColorId = SDL_GetColorId(d, d->textFillColor);
}

void SDL_SetRenderTextAttributes(SDL_Renderer *d, int attributes) {
    d->textAttribs = attributes;
}

static const struct { uint32_t ucp; const char *str; } simpleChars_[] = {
    { 0x02013, "-" }, /* en dash */
    { 0x02014, "--" }, /* em dash */
    { 0x02022, "*" }, /* bullet */
    { 0x02026, "..." },
    { 0x02182, "(O)" }, /* roman numeral ten thousand */
    { 0x02237, "::" }, /* page */
    { 0x022ef, "..." }, /* midline horizontal ellipsis */
    { 0x02318, "88" }, /* place of interest */
    { 0x0231c, "\\" }, /* top left corner */
    { 0x0231d, "|" }, /* top right corner */
    { 0x0231e, "-" }, /* bottom left corner */
    { 0x0231f, "/" }, /* bottom right corner */
    { 0x0232b, "X" }, /* delete */
    { 0x02501, "\u2500" }, /* box drawings heavy horizontal */
    { 0x02503, "\u2502" }, /* box drawings heavy vertical */
    { 0x025e7, "\u2588\u2591" }, /* left half */
    { 0x025e8, "\u2591\u2588" }, /* right half */
    { 0x025ef, " " }, /* large circle */
    { 0x02605, "*" },
    { 0x02610, "[ ]" }, /* ballot */
    { 0x02611, "[*]" }, /* ballot */
    { 0x02615, "~b" }, /* hot beverage */
    { 0x02639, ":(" },
    { 0x0263a, ":)" },
    { 0x0263b, ":)" },
    { 0x0264a, "][" }, /* gemini */
    { 0x026a0, "/\\" }, /* warning */
    { 0x026f2, "`m'" }, /* fountain */
    { 0x02714, "X" }, /* check */
    { 0x02728, "`*`" }, /* sparkles */
    { 0x02795, "+" },
    { 0x027a4, ">>" }, /* right arrowhead */
    { 0x027e9, ">" }, /* closed folder */
    { 0x02913, "_v_" }, /* arrow down to bar */
    { 0x02937, "\u2514\u2500" }, /* arrow pointing downwards then curving rightwards */
    { 0x02a2f, "x" },
    { 0x02b9c, "<<" }, /* left arrowhead */
    { 0x0fe40, "v" }, /* down angle */
    { 0x1f310, "{}" }, /* globe */
    { 0x1f31f, ":*:" }, /* glowing star */
    { 0x1f337, "\\U/" }, /* tulip */
    { 0x1f33b, "O)-" }, /* sunflower */
    { 0x1f392, "c3" }, /* backpack */
    { 0x1f3e0, "<#" }, /* home */
    { 0x1f3e1, "<#:" }, /* house with garden */
    { 0x1f3f7, " /*" }, /* label tag */
    { 0x1f448, "<=" }, /* finger pointing left */
    { 0x1f449, "=>" }, /* finger pointing right */
    { 0x1f44d, "+1" }, /* thumbs up */
    { 0x1f44e, "-1" }, /* thumbs down */
    { 0x1f464, "@" }, /* person */
    { 0x1f465, "@@" }, /* silhouette of two people */
    { 0x1f47d, "\u00b0u\u00b0" }, /* alien */
    { 0x1f4aa, "6=" }, /* bicep flex */
    { 0x1f4ac, ".::" }, /* speech balloon */
    { 0x1f4c1, "//" }, /* folder */
    { 0x1f4c2, "[//" }, /* open folder */
    { 0x1f4c5, "|:|" }, /* calendar */
    { 0x1f4cc, " /^" }, /* pin */
    { 0x1f4e6, "[]]" }, /* package */
    { 0x1f4ea, "[]-" }, /* mailbox, lowered flag */
    { 0x1f4eb, "\\]-" }, /* mailbox, raised flag */
    { 0x1f503, "{}" }, /* clockwise arrows */
    { 0x1f504, "{}" }, /* counter-clockwise arrows */
    { 0x1f50d, "o-" }, /* left-pointing magnifying glass */
    { 0x1f50e, "-o" }, /* right-pointing magnifying glass */
    { 0x1f512, "c[]" }, /* closed lock */
    { 0x1f513, "-[]" }, /* open lock */
    { 0x1f514, "!!" }, /* bell */
    { 0x1f516, "$" }, /* bookmark */
    { 0x1f520, "[%]" }, /* input symbol: uppercase letters */
    { 0x1f553, "(\u2514)" }, /* clock */
    { 0x1f56e, "$" }, /* bookmarks */
    { 0x1f5ce, "[]" }, /* file */
    { 0x1f5de, "[!]" }, /* newspaper */
    { 0x1f600, ":-D" },
    { 0x1f601, ":D" },
    { 0x1f602, ":,D" },
    { 0x1f603, ":-D" },
    { 0x1f604, ":D" },
    { 0x1f605, "`:D" },
    { 0x1f606, "X-D" },
    { 0x1f607, "O:)" },
    { 0x1f608, "}:)" },
    { 0x1f609, ";-)" },
    { 0x1f60a, ":-)" },
    { 0x1f60b, ":)_" },
    { 0x1f60c, "3-)" },
    { 0x1f60d, "<3" },
    { 0x1f60e, "B-)" },
    { 0x1f60f, ";-]" },
    { 0x1f610, ":-|" },
    { 0x1f611, ":|" },
    { 0x1f612, ";(" },
    { 0x1f613, "`:(" },
    { 0x1f614, "3-(" },
    { 0x1f615, ":-\\" },
    { 0x1f616, "X-|" },
    { 0x1f617, ":-^" },
    { 0x1f618, ":<3" },
    { 0x1f619, ":-^" },
    { 0x1f61a, ":-^" },
    { 0x1f61b, ":-P" },
    { 0x1f61c, ";-P" },
    { 0x1f61d, "X-P" },
    { 0x1f61e, ":-(" },
    { 0x1f61f, "8-(" },
    { 0x1f620, ":(" },
    { 0x1f621, ">:(" },
    { 0x1f622, ":,(" },
    { 0x1f623, "X-<" },
    { 0x1f624, ":|*" },
    { 0x1f625, ":-<" },
    { 0x1f626, ":-o" },
    { 0x1f627, ":=o" },
    { 0x1f628, "]:O" },
    { 0x1f629, ":-c" },
    { 0x1f62a, "3-o" },
    { 0x1f62b, "X-O" },
    { 0x1f62c, ":-E" },
    { 0x1f62d, ":_o" },
    { 0x1f62e, "8-o" },
    { 0x1f62f, ":-o" },
    { 0x1f630, ",:|" },
    { 0x1f631, "/o\\" },
    { 0x1f632, ":-O" },
    { 0x1f633, "8-|" },
    { 0x1f634, "Zzz" },
    { 0x1f635, "X-o" },
    { 0x1f636, ":- " },
    { 0x1f637, ":##" },
    { 0x1f638, ":-3" },
    { 0x1f639, "`:3" },
    { 0x1f63a, ":-3" },
    { 0x1f63b, "=3" },
    { 0x1f63c, ";-3" },
    { 0x1f63d, "=-^" },
    { 0x1f63e, "=(" },
    { 0x1f63f, "=,(" },
    { 0x1f640, "/o\\" },
    { 0x1f641, ":-(" },
    { 0x1f642, ":-)" },
    { 0x1f643, "(-:" },
    { 0x1f644, "8-|" },
    { 0x1f645, "XX" },
    { 0x1f64c, "\\o/" },
    { 0x1f64f, "_:_" },
    { 0x1f6cd, "$$" }, /* shopping bags */
    { 0x1f6f0, "|=|" }, /* satellite */
    { 0x1f870, "<-" },
    { 0x1f871, "^" },
    { 0x1f872, "->" },
    { 0x1f929, "*,*" }, /* starry eyes */
    { 0x1f986, "^;=" }, /* duck */
    { 0x1f9d0, "`8/" }, /* monocle face */
    { 0x1fba5, ">" }, /* right angle */
};

static const char *simpleCharStr_(uint32_t ucp) {
    if (isSimpleCharMode_Video) {
        /* The table is sorted, so do a binary search. */ {
            iRangei range = { 0, iElemCount(simpleChars_) };
            while (ucp) {
                const int      mid    = (range.start + range.end) / 2;
                const uint32_t midUcp = simpleChars_[mid].ucp;
                if (ucp == midUcp) {
                    return simpleChars_[mid].str;
                }
                if (mid == range.start) {
                    break; /* too short */
                }
                if (ucp < midUcp) {
                    range.end = mid;
                }
                else {
                    range.start = mid;
                }
            }
        }
        if (ucp >= 0x2500 && ucp <= 0x257f) {
            return NULL; /* box drawing usually supported */
        }
        if (ucp >= 0x2580 && ucp <= 0x259f) {
            return NULL; /* block elements probably available */
        }
        if (isEmoji_Char(ucp) || isPictograph_Char(ucp)) {
            return ".."; /* generic fallback */
        }
    }
    return NULL;
}

iLocalDef void renderChar_(SDL_Renderer *d, iVisChar *vis, uint32_t ucp) {
    vis->ch = ucp;
    vis->attr = d->textAttribs;
    if (d->textFillColorId != transparent_VisChar) {
        vis->bg = d->textFillColorId;
    }
    vis->fg = d->textColorId;
}

void SDL_RenderDrawUnicode(SDL_Renderer *d, int x, int y, uint32_t ucp) {
    if (isVariationSelector_Char(ucp)) {
        return;
    }
    iInt2 pos = init_I2(x, y);
    iCanvas *canvas = activeCanvas_(d);
    if (contains_Rect(clipBounds_Canvas(canvas), pos)) {
        const char *simple = simpleCharStr_(ucp);
        if (simple) {
            iRangecc mbs = range_CStr(simple);
            for (;;) {
                int len = decodeBytes_MultibyteChar(mbs.start, mbs.end, &ucp);
                if (!ucp || len <= 0) break;
                mbs.start += len;
                iVisChar *vis = at_Canvas(canvas, pos);
                renderChar_(d, vis, ucp);
                pos.x++;
            }
        }
        else {
            if (ucp == 0x2014) {
                /* Em dashes are too wide for text mode so use En dash instead. This is more
                   compatible with fonts and terminal emulators. */
                ucp--;
            }
            renderChar_(d, at_Canvas(canvas, pos), ucp);
        }
    }
}

int SDL_UnicodeWidth(SDL_Renderer *d, uint32_t ucp) {
    if (isVariationSelector_Char(ucp)) {
        return 0;
    }
    const char *simple = simpleCharStr_(ucp);
    if (simple) {
        return strlen(simple);
    }
    if (ucp == 0x2014) {
        return 1; /* We're replacing Em with an En dash */
    }
    return width_Char(ucp);
}
