/*****************************************************************************
 * Free42 -- an HP-42S calculator simulator
 * Copyright (C) 2004-2026  Thomas Okken
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License, version 2,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see http://www.gnu.org/licenses/.
 *****************************************************************************/

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <string>
#include <set>

#include "shell_skin.h"
#include "shell_main.h"
#include "shell_loadimage.h"
#include "core_main.h"

#include <gdiplus.h>
#include <gdiplusgraphics.h>
#include <uxtheme.h>
using namespace Gdiplus;

using std::wstring;
using std::to_wstring;
using std::set;


/**************************/
/* Skin description stuff */
/**************************/

struct SkinPoint {
    int x, y;
};

struct SkinRect {
    int x, y, width, height;
};

struct SkinKey {
    int code, shifted_code;
    SkinRect sens_rect;
    SkinRect disp_rect;
    SkinPoint src;
};

#define SKIN_MAX_MACRO_LENGTH 63
#define SKIN_MAX_HALF_MACRO_LENGTH ((SKIN_MAX_MACRO_LENGTH - 1) / 2)

struct SkinMacro {
    int code;
    bool isName;
    char secondType; // 0:none, 1:name, 2:text
    unsigned char macro[SKIN_MAX_HALF_MACRO_LENGTH + 1];
    unsigned char macro2[SKIN_MAX_HALF_MACRO_LENGTH + 1];
    SkinMacro *next;
};

struct SkinAnnunciator {
    SkinRect disp_rect;
    SkinPoint src;
};

static SkinRect skin;
static SkinPoint display_loc;
static double display_scale_x, display_scale_y;
static bool display_scale_int;
static COLORREF display_bg, display_fg;
static SkinKey *keylist = NULL;
static int nkeys = 0;
static int keys_cap = 0;
static SkinMacro *macrolist = NULL;
static SkinAnnunciator annunciators[7];

static FILE *external_file;
static long builtin_length;
static long builtin_pos;
static const unsigned char *builtin_file;

static int skin_type;
static int skin_width, skin_height;
static int skin_ncolors;
static const SkinColor *skin_colors = NULL;
static int skin_y;
static unsigned char *skin_data = NULL;
static int skin_bytesperline;
static BITMAPINFOHEADER *skin_header = NULL;
static Gdiplus::Bitmap *skin_bitmap = NULL;
static Gdiplus::Bitmap *disp_bitmap = NULL;
static unsigned char *disp_bits = NULL;
static int disp_bytesperline;

static keymap_entry *keymap = NULL;
static int keymap_length = 0;

static HWND window;
static int window_width, window_height;


/**********************************************************/
/* Linked-in skins; defined in the skins.c, which in turn */
/* is generated by skin2c.c under control of skin2c.conf  */
/**********************************************************/

extern const int skin_count;
extern const wchar_t * const skin_name[];
extern const long skin_layout_size[];
extern const unsigned char * const skin_layout_data[];
extern const long skin_bitmap_size[];
extern const unsigned char * const skin_bitmap_data[];


/*****************/
/* Keymap parser */
/*****************/

keymap_entry *parse_keymap_entry(char *line, int lineno) {
    char *p;
    static keymap_entry entry;

    p =  strchr(line, '#');
    if (p != NULL)
        *p = 0;
    p = strchr(line, '\n');
    if (p != NULL)
        *p = 0;
    p = strchr(line, '\r');
    if (p != NULL)
        *p = 0;

    p = strchr(line, ':');
    if (p != NULL) {
        char *val = p + 1;
        char *tok;
        bool ctrl = false;
        bool alt = false;
        bool extended = false;
        bool shift = false;
        bool cshift = false;
        int keycode = 0;
        int done = 0;
        unsigned char macro[KEYMAP_MAX_MACRO_LENGTH + 1];
        int macrolen = 0;

        /* Parse keycode */
        *p = 0;
        tok = strtok(line, " \t");
        while (tok != NULL) {
            if (done) {
                fprintf(stderr, "Keymap, line %d: Excess tokens in key spec.\n", lineno);
                return NULL;
            }
            if (_stricmp(tok, "ctrl") == 0)
                ctrl = true;
            else if (_stricmp(tok, "alt") == 0)
                alt = true;
            else if (_stricmp(tok, "extended") == 0)
                extended = true;
            else if (_stricmp(tok, "shift") == 0)
                shift = true;
            else if (_stricmp(tok, "cshift") == 0)
                cshift = true;
            else {
                char *endptr;
                long k = strtol(tok, &endptr, 10);
                if (k < 1 || *endptr != 0) {
                    fprintf(stderr, "Keymap, line %d: Bad keycode.\n", lineno);
                    return NULL;
                }
                keycode = k;
                done = 1;
            }
            tok = strtok(NULL, " \t");
        }
        if (!done) {
            fprintf(stderr, "Keymap, line %d: Unrecognized keycode.\n", lineno);
            return NULL;
        }

        /* Parse macro */
        tok = strtok(val, " \t");
        while (tok != NULL) {
            char *endptr;
            long k = strtol(tok, &endptr, 10);
            if (*endptr != 0 || k < 1 || k > 255) {
                fprintf(stderr, "Keymap, line %d: Bad value (%s) in macro.\n", lineno, tok);
                return NULL;
            } else if (macrolen == KEYMAP_MAX_MACRO_LENGTH) {
                fprintf(stderr, "Keymap, line %d: Macro too long (max=%d).\n", lineno, KEYMAP_MAX_MACRO_LENGTH);
                return NULL;
            } else
                macro[macrolen++] = (unsigned char) k;
            tok = strtok(NULL, " \t");
        }
        macro[macrolen] = 0;

        entry.ctrl = ctrl;
        entry.alt = alt;
        entry.extended = extended;
        entry.shift = shift;
        entry.cshift = cshift;
        entry.keycode = keycode;
        strcpy((char *) entry.macro, (const char *) macro);
        return &entry;
    } else
        return NULL;
}


/*******************/
/* Local functions */
/*******************/

static bool skin_open(const wchar_t *skinname, const wchar_t *basedir, bool open_layout, bool force_builtin);
static int skin_gets(char *buf, int buflen);
static void skin_close();


static bool skin_open(const wchar_t *skinname, const wchar_t *basedir, bool open_layout, bool force_builtin) {
    if (!force_builtin) {
        wchar_t namebuf[1024];
        wchar_t exedir[MAX_PATH];
        wchar_t *lastbackslash;

        /* Look for file in home dir */
        swprintf(namebuf, L"%ls\\%ls.%ls", basedir, skinname,
                                            open_layout ? L"layout" : L"gif");
        external_file = _wfopen(namebuf, L"rb");
        if (external_file != NULL)
            return true;

        /* name did not match in home dir; now try in exe dir */
        GetModuleFileNameW(0, exedir, MAX_PATH - 1);
        lastbackslash = wcsrchr(exedir, L'\\');
        if (lastbackslash != 0)
            *lastbackslash = 0;
        else
            wcscpy(exedir, L"C:");
        swprintf(namebuf, L"%ls\\%ls.%ls", exedir, skinname,
                                            open_layout ? L"layout" : L"gif");
        external_file = _wfopen(namebuf, L"rb");
        if (external_file != NULL)
            return true;
    }

    /* Look for built-in skin last */
    for (int i = 0; i < skin_count; i++) {
        if (wcscmp(skinname, skin_name[i]) == 0) {
            external_file = NULL;
            builtin_pos = 0;
            if (open_layout) {
                builtin_length = skin_layout_size[i];
                builtin_file = skin_layout_data[i];
            } else {
                builtin_length = skin_bitmap_size[i];
                builtin_file = skin_bitmap_data[i];
            }
            return true;
        }
    }

    /* Nothing found */
    return false;
}

int skin_getchar() {
    if (external_file != NULL)
        return fgetc(external_file);
    else if (builtin_pos < builtin_length)
        return builtin_file[builtin_pos++];
    else
        return EOF;
}

static int skin_gets(char *buf, int buflen) {
    int p = 0;
    int eof = -1;
    int comment = 0;
    while (p < buflen - 1) {
        int c = skin_getchar();
        if (eof == -1)
            eof = c == EOF;
        if (c == EOF || c == '\n' || c == '\r')
            break;
        /* Remove comments */
        if (c == '#')
            comment = 1;
        if (comment)
            continue;
        /* Suppress leading spaces */
        if (p == 0 && isspace(c))
            continue;
        buf[p++] = c;
    }
    buf[p++] = 0;
    return p > 1 || !eof;
}

static void skin_close() {
    if (external_file != NULL)
        fclose(external_file);
}

static void skin_invalidate(int left, int top, int right, int bottom) {
    RECT r;
    r.left = (int) (((double) left) * window_width / skin.width);
    r.top = (int) (((double) top) * window_height / skin.height);
    r.right = (int) ceil(((double) right) * window_width / skin.width);
    r.bottom = (int) ceil(((double) bottom) * window_height / skin.height);
    InvalidateRect(window, &r, FALSE);
}

void skin_load(wchar_t *skinname, const wchar_t *basedir, long *width, long *height) {
    char line[1024];
    bool force_builtin = false;

    if (skinname[0] == 0) {
        fallback_on_1st_builtin_skin:
        wcscpy(skinname, skin_name[0]);
        force_builtin = true;
    }

    /*************************/
    /* Load skin description */
    /*************************/

    if (!skin_open(skinname, basedir, 1, force_builtin))
        goto fallback_on_1st_builtin_skin;

    if (keylist != NULL)
        free(keylist);
    keylist = NULL;
    nkeys = 0;
    keys_cap = 0;

    while (macrolist != NULL) {
        SkinMacro *m = macrolist->next;
        free(macrolist);
        macrolist = m;
    }

    if (keymap != NULL)
        free(keymap);
    keymap = NULL;
    keymap_length = 0;
    int kmcap = 0;

    int lineno = 0;

    while (skin_gets(line, 1024)) {
        lineno++;
        if (*line == 0)
            continue;
        if (_strnicmp(line, "skin:", 5) == 0) {
            int x, y, width, height;
            if (sscanf(line + 5, " %d,%d,%d,%d", &x, &y, &width, &height) == 4){
                skin.x = x;
                skin.y = y;
                skin.width = width;
                skin.height = height;
            }
        } else if (_strnicmp(line, "display:", 8) == 0) {
            int x, y;
            double xscale, yscale;
            unsigned long bg, fg;
            if (sscanf(line + 8, " %d,%d %lf %lf %lx %lx", &x, &y,
                                            &xscale, &yscale, &bg, &fg) == 6) {
                display_loc.x = x;
                display_loc.y = y;
                display_scale_x = xscale;
                display_scale_y = yscale;
                display_scale_int = xscale == (int) xscale && yscale == (int) yscale;
                display_bg = bg;
                display_fg = fg;
            }
        } else if (_strnicmp(line, "key:", 4) == 0) {
            char keynumbuf[20];
            int keynum, shifted_keynum;
            int sens_x, sens_y, sens_width, sens_height;
            int disp_x, disp_y, disp_width, disp_height;
            int act_x, act_y;
            if (sscanf(line + 4, " %s %d,%d,%d,%d %d,%d,%d,%d %d,%d",
                                 keynumbuf,
                                 &sens_x, &sens_y, &sens_width, &sens_height,
                                 &disp_x, &disp_y, &disp_width, &disp_height,
                                 &act_x, &act_y) == 11) {
                int n = sscanf(keynumbuf, "%d,%d", &keynum, &shifted_keynum);
                if (n > 0) {
                    if (n == 1)
                        shifted_keynum = keynum;
                    SkinKey *key;
                    if (nkeys == keys_cap) {
                        keys_cap += 50;
                        keylist = (SkinKey *) realloc(keylist, keys_cap * sizeof(SkinKey));
                        // TODO - handle memory allocation failure
                    }
                    key = keylist + nkeys;
                    key->code = keynum;
                    key->shifted_code = shifted_keynum;
                    key->sens_rect.x = sens_x;
                    key->sens_rect.y = sens_y;
                    key->sens_rect.width = sens_width;
                    key->sens_rect.height = sens_height;
                    key->disp_rect.x = disp_x;
                    key->disp_rect.y = disp_y;
                    key->disp_rect.width = disp_width;
                    key->disp_rect.height = disp_height;
                    key->src.x = act_x;
                    key->src.y = act_y;
                    nkeys++;
                }
            }
        } else if (_strnicmp(line, "macro:", 6) == 0) {
            char *quot1 = strchr(line + 6, '"');
            if (quot1 != NULL) {
                char *quot2 = strchr(quot1 + 1, '"');
                if (quot2 != NULL) {
                    long len = (long) (quot2 - quot1 - 1);
                    if (len > SKIN_MAX_HALF_MACRO_LENGTH)
                        len = SKIN_MAX_HALF_MACRO_LENGTH;
                    int n;
                    if (sscanf(line + 6, "%d", &n) == 1 && n >= 38 && n <= 255) {
                        SkinMacro *macro = (SkinMacro *) malloc(sizeof(SkinMacro));
                        // TODO - handle memory allocation failure
                        macro->code = n;
                        macro->isName = true;
                        memcpy(macro->macro, quot1 + 1, len);
                        macro->macro[len] = 0;
                        macro->secondType = 0;
                        quot1 = strchr(quot2 + 1, '"');
                        if (quot1 == NULL)
                            quot1 = strchr(quot2 + 1, '\'');
                        if (quot1 != NULL) {
                            quot2 = strchr(quot1 + 1, *quot1);
                            if (quot2 != NULL) {
                                len = (long) (quot2 - quot1 - 1);
                                if (len > SKIN_MAX_HALF_MACRO_LENGTH)
                                    len = SKIN_MAX_HALF_MACRO_LENGTH;
                                memcpy(macro->macro2, quot1 + 1, len);
                                macro->macro2[len] = 0;
                                macro->secondType = *quot1 == '"' ? 1 : 2;
                            }
                        }
                        macro->next = macrolist;
                        macrolist = macro;
                    }
                }
            } else {
                char *tok = strtok(line + 6, " \t");
                int len = 0;
                SkinMacro *macro = NULL;
                while (tok != NULL) {
                    char *endptr;
                    long n = strtol(tok, &endptr, 10);
                    if (*endptr != 0) {
                        /* Not a proper number; ignore this macro */
                        if (macro != NULL) {
                            free(macro);
                            macro = NULL;
                            break;
                        }
                    }
                    if (macro == NULL) {
                        if (n < 38 || n > 255)
                            /* Macro code out of range; ignore this macro */
                            break;
                        macro = (SkinMacro *) malloc(sizeof(SkinMacro));
                        // TODO - handle memory allocation failure
                        macro->code = n;
                        macro->isName = false;
                    } else if (len < SKIN_MAX_MACRO_LENGTH) {
                        if (n < 1 || n > 37) {
                            /* Key code out of range; ignore this macro */
                            free(macro);
                            macro = NULL;
                            break;
                        }
                        macro->macro[len++] = (unsigned char) n;
                    }
                    tok = strtok(NULL, " \t");
                }
                if (macro != NULL) {
                    macro->macro[len++] = 0;
                    macro->next = macrolist;
                    macrolist = macro;
                }
            }
        } else if (_strnicmp(line, "annunciator:", 12) == 0) {
            int annnum;
            int disp_x, disp_y, disp_width, disp_height;
            int act_x, act_y;
            if (sscanf(line + 12, " %d %d,%d,%d,%d %d,%d",
                                  &annnum,
                                  &disp_x, &disp_y, &disp_width, &disp_height,
                                  &act_x, &act_y) == 7) {
                if (annnum >= 1 && annnum <= 7) {
                    SkinAnnunciator *ann = annunciators + (annnum - 1);
                    ann->disp_rect.x = disp_x;
                    ann->disp_rect.y = disp_y;
                    ann->disp_rect.width = disp_width;
                    ann->disp_rect.height = disp_height;
                    ann->src.x = act_x;
                    ann->src.y = act_y;
                }
            }
        } else if (_strnicmp(line, "winkey:", 7) == 0) {
            keymap_entry *entry = parse_keymap_entry(line + 7, lineno);
            if (entry != NULL) {
                if (keymap_length == kmcap) {
                    kmcap += 50;
                    keymap = (keymap_entry *) realloc(keymap, kmcap * sizeof(keymap_entry));
                    // TODO - handle memory allocation failure
                }
                memcpy(keymap + (keymap_length++), entry, sizeof(keymap_entry));
            }
        }
    }

    skin_close();

    /********************/
    /* Load skin bitmap */
    /********************/

    if (!skin_open(skinname, basedir, 0, force_builtin))
        goto fallback_on_1st_builtin_skin;

    /* shell_loadimage() calls skin_getchar() to load the image from the
     * compiled-in or on-disk file; it calls skin_init_image(),
     * skin_put_pixels(), and skin_finish_image() to create the in-memory
     * representation.
     */
    bool success = shell_loadimage();
    skin_close();

    if (!success)
        goto fallback_on_1st_builtin_skin;

    *width = skin.width;
    *height = skin.height;

    /****************************/
    /* Build the display bitmap */
    /****************************/

    if (disp_bits == NULL) {
        // Allocating a bitmap with 4x4 pixels for each calculator pixel, and
        // margins of four pixels around the edges. The upscaling is done to
        // reduce blurriness when scaling, and the margin is to avoid edge
        // effects when blurring runs across the edge.
        disp_bytesperline = (((131 * 4 + 8) + 31) >> 3) & ~3;
        int size = disp_bytesperline * (16 * 4 + 8);
        disp_bits = (unsigned char *) malloc(size);
        disp_bitmap = new Gdiplus::Bitmap(131 * 4 + 8, 16 * 4 + 8, disp_bytesperline, PixelFormat1bppIndexed, disp_bits);
        memset(disp_bits, 0, size);
    }
    struct {
        Gdiplus::ColorPalette pal;
        ARGB entry2;
    } pal;
    pal.pal.Flags = 0;
    pal.pal.Count = 2;
    pal.pal.Entries[0] = display_bg | 0xff000000;
    pal.pal.Entries[1] = display_fg | 0xff000000;
    disp_bitmap->SetPalette(&pal.pal);
}

bool skin_init_image(int type, int ncolors, const SkinColor *colors,
                     int width, int height) {
    if (skin_bitmap != NULL) {
        delete skin_bitmap;
        skin_bitmap = NULL;
    }
    if (skin_data != NULL) {
        free(skin_data);
        skin_data = NULL;
    }
    if (skin_header != NULL) {
        free(skin_header);
        skin_header = NULL;
    }

    skin_type = type;
    skin_ncolors = ncolors;
    skin_colors = colors;
    
    switch (type) {
        case IMGTYPE_MONO:
            skin_bytesperline = ((width + 15) >> 3) & ~1;
            break;
        case IMGTYPE_GRAY:
        case IMGTYPE_COLORMAPPED:
            skin_bytesperline = (width + 3) & ~3;
            break;
        case IMGTYPE_TRUECOLOR:
            skin_bytesperline = (width * 3 + 3) & ~3;
            break;
        default:
            return false;
    }

    skin_data = (unsigned char *) malloc(skin_bytesperline * height);
    // TODO - handle memory allocation failure
    skin_width = width;
    skin_height = height;
    skin_y = 0;
    return skin_data != NULL;
}

void skin_put_pixels(unsigned const char *data) {
    unsigned char *dst = skin_data + skin_y * skin_bytesperline;
    if (skin_type == IMGTYPE_MONO) {
        int i;
        for (i = 0; i < skin_bytesperline; i++) {
            unsigned char c = data[i];
            c = (c >> 7) | ((c >> 5) & 2) | ((c >> 3) & 4) | ((c >> 1) & 8)
                | ((c << 1) & 16) | ((c << 3) & 32) | ((c << 5) & 64) | (c << 7);
            dst[i] = c;
        }
    } else if (skin_type == IMGTYPE_TRUECOLOR) {
        int i;
        for (i = 0; i < skin_width; i++) {
            *dst++ = data[2];
            *dst++ = data[1];
            *dst++ = data[0];
            data += 3;
        }
    } else
        memcpy(dst, data, skin_bytesperline);
    skin_y++;
}

void skin_finish_image() {
    BITMAPINFOHEADER *bh;
    
    if (skin_type == IMGTYPE_MONO) {
        skin_bitmap = new Gdiplus::Bitmap(skin_width, skin_height, skin_bytesperline, PixelFormat1bppIndexed, skin_data);
        skin_header = NULL;
        return;
    }

    if (skin_type == IMGTYPE_COLORMAPPED) {
        RGBQUAD *cmap;
        int i;
        bh = (BITMAPINFOHEADER *) malloc(sizeof(BITMAPINFOHEADER) + skin_ncolors * sizeof(RGBQUAD));
        // TODO - handle memory allocation failure
        cmap = (RGBQUAD *) (bh + 1);
        for (i = 0; i < skin_ncolors; i++) {
            cmap[i].rgbRed = skin_colors[i].r;
            cmap[i].rgbGreen = skin_colors[i].g;
            cmap[i].rgbBlue = skin_colors[i].b;
            cmap[i].rgbReserved = 0;
        }
    } else if (skin_type == IMGTYPE_GRAY) {
        RGBQUAD *cmap;
        int i;
        bh = (BITMAPINFOHEADER *) malloc(sizeof(BITMAPINFOHEADER) + 256 * sizeof(RGBQUAD));
        // TODO - handle memory allocation failure
        cmap = (RGBQUAD *) (bh + 1);
        for (i = 0; i < 256; i++) {
            cmap[i].rgbRed = cmap[i].rgbGreen = cmap[i].rgbBlue = i;
            cmap[i].rgbReserved = 0;
        }
    } else
        bh = (BITMAPINFOHEADER *) malloc(sizeof(BITMAPINFOHEADER));
        // TODO - handle memory allocation failure

    bh->biSize = sizeof(BITMAPINFOHEADER);
    bh->biWidth = skin_width;
    bh->biHeight = -skin_height;
    bh->biPlanes = 1;
    switch (skin_type) {
        case IMGTYPE_MONO:
            bh->biBitCount = 1;
            break;
        case IMGTYPE_GRAY:
            bh->biBitCount = 8;
            break;
        case IMGTYPE_COLORMAPPED:
            bh->biBitCount = 8;
            break;
        case IMGTYPE_TRUECOLOR:
            bh->biBitCount = 24;
            break;
    }
    bh->biCompression = BI_RGB;
    bh->biSizeImage = skin_bytesperline * skin_height;
    bh->biXPelsPerMeter = 2835;
    bh->biYPelsPerMeter = 2835;
    bh->biClrUsed = 0;
    bh->biClrImportant = 0;
    
    skin_header = bh;

    skin_bitmap = new Gdiplus::Bitmap((BITMAPINFO *) bh, skin_data);
}

static bool need_to_paint_only_display(RECT* r) {
    int d_left = (int) (((double) display_loc.x) * window_width / skin.width);
    int d_top = (int) (((double) display_loc.y) * window_height / skin.height);
    int d_right = (int) ceil((display_loc.x + 131 * display_scale_x) * window_width / skin.width);
    int d_bottom = (int) ceil((display_loc.y + 16 * display_scale_y) * window_height / skin.height);
    return r->left >= d_left
        && r->top >= d_top
        && r->right <= d_right
        && r->bottom <= d_bottom;
}

static void skin_repaint_annunciator(Graphics *g, int which) {
    SkinAnnunciator* ann = annunciators + (which - 1);
    g->DrawImage(skin_bitmap, ann->disp_rect.x, ann->disp_rect.y, ann->src.x, ann->src.y, ann->disp_rect.width, ann->disp_rect.height, Gdiplus::UnitPixel);
}

struct KeyShortcutInfo {
    int x, y, width, height;
    wstring unshifted, shifted;
    KeyShortcutInfo *next;
    
    KeyShortcutInfo(SkinKey *k) {
        x = k->sens_rect.x;
        y = k->sens_rect.y;
        width = k->sens_rect.width;
        height = k->sens_rect.height;
        unshifted = L"";
        shifted = L"";
    }
    
    bool sameRect(SkinKey *that) {
        return x == that->sens_rect.x
                && y == that->sens_rect.y
                && width == that->sens_rect.width
                && height == that->sens_rect.height;
    }
    
    void add(wstring entryStr, bool shifted) {
        wstring *str = shifted ? &this->shifted : &this->unshifted;
        *str = entryStr + L" " + *str;
    }
    
    wstring text() {
        wstring u, s;
        if (unshifted.size() == 0)
            u = L"n/a";
        else
            u = unshifted.substr(0, unshifted.size() - 1);
        if (shifted.size() == 0)
            s = L"n/a";
        else
            s = shifted.substr(0, shifted.size() - 1);
        return s + L"\n" + u;
    }
};

static wstring keycode_to_text(int code) {
    // source: https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
    // dated 06/06/2024

    if (code >= '0' && code <= '9' || code >= 'A' && code <= 'Z') {
        wchar_t s[2] = { (wchar_t) code, 0 };
        return s;
    } else if (code >= VK_NUMPAD0 && code <= VK_NUMPAD9) {
        return wstring(L"Kp") + to_wstring(code - VK_NUMPAD0);
    } else if (code >= VK_F1 && code <= VK_F24) {
        return wstring(L"F") + to_wstring(code - VK_F1 + 1);
    }

    switch (code) {
        case VK_LBUTTON: return L"LBUTTON"; // Left mouse button
        case VK_RBUTTON: return L"RBUTTON"; // Right mouse button
        case VK_CANCEL: return L"CANCEL"; // Control - break processing
        case VK_MBUTTON: return L"MBUTTON"; // Middle mouse button
        case VK_XBUTTON1: return L"XBUTTON1"; // X1 mouse button
        case VK_XBUTTON2: return L"XBUTTON2"; // X2 mouse button
        case VK_BACK: return L"\x232B"; // BACKSPACE key
        case VK_TAB: return L"TAB"; // TAB key
        case VK_CLEAR: return L"CLEAR"; // CLEAR key
        case VK_RETURN: return L"Enter"; // ENTER key
        case VK_SHIFT: return L"SHIFT"; // SHIFT key
        case VK_CONTROL: return L"CONTROL"; // CTRL key
        case VK_MENU: return L"MENU"; // ALT key
        case VK_PAUSE: return L"PAUSE"; // PAUSE key
        case VK_CAPITAL: return L"CAPITAL"; // CAPS LOCK key
        case VK_KANA: return L"KANA"; // IME Kana mode
        //case VK_HANGUL: return L"HANGUL"; // IME Hangul mode
        case VK_IME_ON: return L"IME_ON"; // IME On
        case VK_JUNJA: return L"JUNJA"; // IME Junja mode
        case VK_FINAL: return L"FINAL"; // IME final mode
        case VK_HANJA: return L"HANJA"; // IME Hanja mode
        //case VK_KANJI: return L"KANJI"; // IME Kanji mode
        case VK_IME_OFF: return L"IME_OFF"; // IME Off
        case VK_ESCAPE: return L"Esc"; // ESC key
        case VK_CONVERT: return L"CONVERT"; // IME convert
        case VK_NONCONVERT: return L"NONCONVERT"; // IME nonconvert
        case VK_ACCEPT: return L"ACCEPT"; // IME accept
        case VK_MODECHANGE: return L"MODECHANGE"; // IME mode change request
        case VK_SPACE: return L"SPACE"; // SPACEBAR
        case VK_PRIOR: return L"PRIOR"; // PAGE UP key
        case VK_NEXT: return L"Next"; // PAGE DOWN key
        case VK_END: return L"End"; // END key
        case VK_HOME: return L"Home"; // HOME key
        case VK_LEFT: return L"\x2190"; // LEFT ARROW key
        case VK_UP: return L"\x2191"; // UP ARROW key
        case VK_RIGHT: return L"\x2192"; // RIGHT ARROW key
        case VK_DOWN: return L"\x2193"; // DOWN ARROW key
        case VK_SELECT: return L"SELECT"; // SELECT key
        case VK_PRINT: return L"PRINT"; // PRINT key
        case VK_EXECUTE: return L"EXECUTE"; // EXECUTE key
        case VK_SNAPSHOT: return L"SNAPSHOT"; // PRINT SCREEN key
        case VK_INSERT: return L"Ins"; // INS key
        case VK_DELETE: return L"\x2326"; // DEL key
        case VK_HELP: return L"HELP"; // HELP key
        case VK_LWIN: return L"LWIN"; // Left Windows key
        case VK_RWIN: return L"RWIN"; // Right Windows key
        case VK_APPS: return L"APPS"; // Applications key
        case VK_SLEEP: return L"SLEEP"; // Computer Sleep key
        case VK_MULTIPLY: return L"*"; // Multiply key
        case VK_ADD: return L"+"; // Add key
        case VK_SEPARATOR: return L","; // Separator key
        case VK_SUBTRACT: return L"-"; // Subtract key
        case VK_DECIMAL: return L"."; // Decimal key
        case VK_DIVIDE: return L"/"; // Divide key
        case VK_NUMLOCK: return L"NUMLOCK"; // NUM LOCK key
        case VK_SCROLL: return L"SCROLL"; // SCROLL LOCK key
        case VK_LSHIFT: return L"LSHIFT"; // Left SHIFT key
        case VK_RSHIFT: return L"RSHIFT"; // Right SHIFT key
        case VK_LCONTROL: return L"LCONTROL"; // Left CONTROL key
        case VK_RCONTROL: return L"RCONTROL"; // Right CONTROL key
        case VK_LMENU: return L"LMENU"; // Left ALT key
        case VK_RMENU: return L"RMENU"; // Right ALT key
        case VK_BROWSER_BACK: return L"BROWSER_BACK"; // Browser Back key
        case VK_BROWSER_FORWARD: return L"BROWSER_FORWARD"; // Browser Forward key
        case VK_BROWSER_REFRESH: return L"BROWSER_REFRESH"; // Browser Refresh key
        case VK_BROWSER_STOP: return L"BROWSER_STOP"; // Browser Stop key
        case VK_BROWSER_SEARCH: return L"BROWSER_SEARCH"; // Browser Search key
        case VK_BROWSER_FAVORITES: return L"BROWSER_FAVORITES"; // Browser Favorites key
        case VK_BROWSER_HOME: return L"BROWSER_HOME"; // Browser Start and Home key
        case VK_VOLUME_MUTE: return L"VOLUME_MUTE"; // Volume Mute key
        case VK_VOLUME_DOWN: return L"VOLUME_DOWN"; // Volume Down key
        case VK_VOLUME_UP: return L"VOLUME_UP"; // Volume Up key
        case VK_MEDIA_NEXT_TRACK: return L"MEDIA_NEXT_TRACK"; // Next Track key
        case VK_MEDIA_PREV_TRACK: return L"MEDIA_PREV_TRACK"; // Previous Track key
        case VK_MEDIA_STOP: return L"MEDIA_STOP"; // Stop Media key
        case VK_MEDIA_PLAY_PAUSE: return L"MEDIA_PLAY_PAUSE"; // Play / Pause Media key
        case VK_LAUNCH_MAIL: return L"LAUNCH_MAIL"; // Start Mail key
        case VK_LAUNCH_MEDIA_SELECT: return L"LAUNCH_MEDIA_SELECT"; // Select Media key
        case VK_LAUNCH_APP1: return L"LAUNCH_APP1"; // Start Application 1 key
        case VK_LAUNCH_APP2: return L"LAUNCH_APP2"; // Start Application 2 key
        case VK_OEM_1: return L";"; // Used for miscellaneous characters; it can vary by keyboard.For the US standard keyboard, the; : key
        case VK_OEM_PLUS: return L"+"; // For any country / region, the + key
        case VK_OEM_COMMA: return L","; // For any country / region, the, key
        case VK_OEM_MINUS: return L"-"; // For any country / region, the - key
        case VK_OEM_PERIOD: return L"."; // For any country / region, the.key
        case VK_OEM_2: return L"/"; // Used for miscellaneous characters; it can vary by keyboard.For the US standard keyboard, the / ? key
        case VK_OEM_3: return L"`"; // Used for miscellaneous characters; it can vary by keyboard.For the US standard keyboard, the `~ key
        case VK_OEM_4: return L"["; // Used for miscellaneous characters; it can vary by keyboard.For the US standard keyboard, the[{ key
        case VK_OEM_5: return L"\\"; // Used for miscellaneous characters; it can vary by keyboard.For the US standard keyboard, the \\ | key
        case VK_OEM_6: return L"]"; // Used for miscellaneous characters; it can vary by keyboard.For the US standard keyboard, the]} key
        case VK_OEM_7: return L"'"; // Used for miscellaneous characters; it can vary by keyboard.For the US standard keyboard, the '" key
        case VK_OEM_8: return L"OEM_8"; // Used for miscellaneous characters; it can vary by keyboard.
        case VK_OEM_102: return L"OEM_102"; // The <> keys on the US standard keyboard, or the \\ | key on the non - US 102 - key keyboard
        case VK_PROCESSKEY: return L"PROCESSKEY"; // IME PROCESS key
        case VK_PACKET: return L"PACKET"; // Used to pass Unicode characters as if they were keystrokes.The VK_PACKET key is the low word of a 32 - bit Virtual Key value used for non - keyboard input methods.For more information, see Remark in KEYBDINPUT, SendInput, WM_KEYDOWN, and WM_KEYUP
        case VK_ATTN: return L"ATTN"; // Attn key
        case VK_CRSEL: return L"CRSEL"; // CrSel key
        case VK_EXSEL: return L"EXSEL"; // ExSel key
        case VK_EREOF: return L"EREOF"; // Erase EOF key
        case VK_PLAY: return L"PLAY"; // Play key
        case VK_ZOOM: return L"ZOOM"; // Zoom key
        case VK_NONAME: return L"NONAME"; // Reserved
        case VK_PA1: return L"PA1"; // PA1 key
        case VK_OEM_CLEAR: return L"OEM_CLEAR"; // Clear key
        default: return wstring(L"VK(") + to_wstring(code) + L")";
    }
}

static wstring entry_to_text(keymap_entry *e) {
    wstring mods = L"";
    if (e->extended)
        mods += L"{e}";
    if (e->ctrl)
        mods += L"^";
    if (e->alt)
        mods += L"\x2325";
    if (e->shift)
        mods += L"\x21e7";
    return mods + keycode_to_text(e->keycode);
}

static KeyShortcutInfo *get_shortcut_info() {
    KeyShortcutInfo *head = NULL;
    set<wstring> seen;
    for (int km = 0; km < 2; km++) {
        keymap_entry *kmap;
        int kmap_len;
        if (km == 0) {
            kmap = keymap;
            kmap_len = keymap_length;
        } else
            get_keymap(&kmap, &kmap_len);
        for (int i = kmap_len - 1; i >= 0; i--) {
            keymap_entry *e = kmap + i;
            if (e->cshift)
                continue;
            int key;
            bool shifted;
            if (e->macro[1] == 0) {
                key = e->macro[0];
                shifted = false;
            } else if (e->macro[0] == 28 && e->macro[2] == 0) {
                key = e->macro[1];
                shifted = true;
            } else
                continue;
            SkinKey *k = NULL;
            for (int j = 0; j < nkeys; j++) {
                k = keylist + j;
                if (key == k->code)
                    break;
                if (key == k->shifted_code) {
                    shifted = true;
                    break;
                }
                k = NULL;
            }
            if (k == NULL)
                continue;
            wstring entryStr = entry_to_text(e);
            if (seen.find(entryStr) != seen.end())
                continue;
            seen.insert(entryStr);
            for (KeyShortcutInfo *p = head; p != NULL; p = p->next) {
                if (p->sameRect(k)) {
                    p->add(entryStr, shifted);
                    goto endloop;
                }
            }
            KeyShortcutInfo *ki;
            ki = new KeyShortcutInfo(k);
            ki->add(entryStr, shifted);
            ki->next = head;
            head = ki;
            endloop:;
        }
    }
    return head;
}

void skin_repaint(bool shortcuts) {
    PAINTSTRUCT ps;
    HDC hdc = BeginPaint(window, &ps);
    RECT rc;
    GetClientRect(window, &rc);
    HDC memdc;
    HPAINTBUFFER hbuf = BeginBufferedPaint(hdc, &rc, BPBF_COMPATIBLEBITMAP, NULL, &memdc);
    Graphics g(memdc);
    g.ScaleTransform((Gdiplus::REAL) (((double) window_width) / skin.width),
                     (Gdiplus::REAL) (((double) window_height) / skin.height));

    bool only_disp = need_to_paint_only_display(&ps.rcPaint);
    if (!only_disp) {
        g.SetInterpolationMode(InterpolationModeBilinear);
        g.DrawImage(skin_bitmap, 0, 0, skin.x, skin.y, skin.width, skin.height, Gdiplus::UnitPixel);
    }

    Region oldClip;
    g.GetClip(&oldClip);
    g.SetClip(Rect(display_loc.x - 1, display_loc.y - 1, ((int) ceil(display_scale_x * 131)) + 2, ((int) ceil(display_scale_y * 16)) + 2));
    Matrix oldTransform;
    g.GetTransform(&oldTransform);
    g.TranslateTransform((REAL) display_loc.x, (REAL) display_loc.y);
    g.ScaleTransform((REAL) display_scale_x / 4, (REAL) display_scale_y / 4);
    if (display_scale_int && skin.width == window_width && skin.height == window_height)
        g.SetInterpolationMode(InterpolationModeNearestNeighbor);
    else
        g.SetInterpolationMode(InterpolationModeBilinear);
    if (skey >= -7 && skey <= -2) {
        int key = -1 - skey;
        int vo = (16 - 7) + 1;
        int ho = ((key - 1) * 22) + 1;
        for (int i = 0; i < 2; i++) {
            for (int v = 0; v < 7; v++)
                for (int h = 0; h < 22 - 1; h++) {
                    int baddr = (v + vo) * 4 * disp_bytesperline + ((h + ho) >> 1);
                    int newb = disp_bits[baddr] ^= ((h + ho) & 1) == 0 ? 240 : 15;
                    for (int i = 0; i < 4; i++) {
                        disp_bits[baddr] = newb;
                        baddr += disp_bytesperline;
                    }
                }
            if (i == 0)
                g.DrawImage(disp_bitmap, (REAL) -4.5, (REAL) -4.5);
        }
    } else {
        g.DrawImage(disp_bitmap, (REAL) -4.5, (REAL) -4.5);
    }
    g.SetTransform(&oldTransform);
    g.SetClip(&oldClip);


    if (!only_disp) {
        g.SetInterpolationMode(InterpolationModeBilinear);
        if (ann_updown)
            skin_repaint_annunciator(&g, 1);
        if (ann_shift)
            skin_repaint_annunciator(&g, 2);
        if (ann_print)
            skin_repaint_annunciator(&g, 3);
        if (ann_run)
            skin_repaint_annunciator(&g, 4);
        if (ann_battery)
            skin_repaint_annunciator(&g, 5);
        if (ann_g)
            skin_repaint_annunciator(&g, 6);
        if (ann_rad)
            skin_repaint_annunciator(&g, 7);
        if (skey >= 0 && skey < nkeys) {
            SkinKey* key = keylist + skey;
            g.DrawImage(skin_bitmap, key->disp_rect.x, key->disp_rect.y, key->src.x, key->src.y,
                        key->disp_rect.width, key->disp_rect.height, Gdiplus::UnitPixel);
        }
    }

    if (shortcuts) {
        SolidBrush transparentWhite(Color(127, 255, 255, 255));
        SolidBrush opaqueBlack(Color(255, 0, 0, 0));
        g.FillRectangle(&transparentWhite, 0, 0, skin.width, skin.height);
        KeyShortcutInfo *ksinfo = get_shortcut_info();
        FontFamily fontFamily(L"Arial");
        double fsize = sqrt(((double) skin.width) * skin.height) / 50;
        Font font(&fontFamily, (REAL) fsize, FontStyleRegular, UnitPoint);
        while (ksinfo != NULL) {
            g.FillRectangle(&transparentWhite, ksinfo->x + 2, ksinfo->y + 2, ksinfo->width - 4, ksinfo->height - 4);
            RectF r((REAL) (ksinfo->x + 4), (REAL) (ksinfo->y + 4), (REAL) (ksinfo->width - 8), (REAL) (ksinfo->height - 8));
            g.DrawString(ksinfo->text().c_str(), -1, &font, r, NULL, &opaqueBlack);
            KeyShortcutInfo *next = ksinfo->next;
            delete ksinfo;
            ksinfo = next;
        }
    }

    EndBufferedPaint(hbuf, TRUE);
    EndPaint(window, &ps);
}

void skin_invalidate_annunciator(int which) {
    SkinAnnunciator *ann = annunciators + (which - 1);
    skin_invalidate(ann->disp_rect.x, ann->disp_rect.y,
        ann->disp_rect.x + ann->disp_rect.width, ann->disp_rect.y + ann->disp_rect.height);
}

void skin_find_key(int x, int y, bool cshift, int *skey, int *ckey) {
    x = (int) (((double) x) * skin.width / window_width);
    y = (int) (((double) y) * skin.height / window_height);
    int i;
    if (core_menu()
            && x >= display_loc.x
            && x < display_loc.x + 131 * display_scale_x
            && y >= display_loc.y + 9 * display_scale_y
            && y < display_loc.y + 16 * display_scale_y) {
        int softkey = (int) ((x - display_loc.x) / (22 * display_scale_x) + 1);
        *skey = -1 - softkey;
        *ckey = softkey;
        return;
    }
    for (i = 0; i < nkeys; i++) {
        SkinKey *k = keylist + i;
        int rx = x - k->sens_rect.x;
        int ry = y - k->sens_rect.y;
        if (rx >= 0 && rx < k->sens_rect.width
                && ry >= 0 && ry < k->sens_rect.height) {
            *skey = i;
            *ckey = cshift ? k->shifted_code : k->code;
            return;
        }
    }
    *skey = -1;
    *ckey = 0;
}

int skin_find_skey(int ckey, bool cshift) {
    int fuzzy_match = -1;
    for (int i = 0; i < nkeys; i++)
        if (keylist[i].code == ckey || keylist[i].shifted_code == ckey)
            if ((cshift ? keylist[i].shifted_code : keylist[i].code) == ckey)
                return i;
            else if (fuzzy_match == -1)
                fuzzy_match = i;
    return fuzzy_match;
}

unsigned char *skin_find_macro(int ckey, int *type) {
    SkinMacro *m = macrolist;
    while (m != NULL) {
        if (m->code == ckey) {
            if (!m->isName || m->secondType == 0 || !core_alpha_menu()) {
                *type = m->isName ? 1 : 0;
                return m->macro;
            } else {
                *type = m->secondType;
                return m->macro2;
            }
        }
        m = m->next;
    }
    return NULL;
}

unsigned char *skin_keymap_lookup(int keycode, bool ctrl, bool alt, bool extended, bool shift, bool cshift, bool *exact) {
    int i;
    unsigned char *macro = NULL;
    for (i = 0; i < keymap_length; i++) {
        keymap_entry *entry = keymap + i;
        if (keycode == entry->keycode
                && ctrl == entry->ctrl
                && alt == entry->alt
                && shift == entry->shift) {
            if (extended == entry->extended && cshift == entry->cshift) {
                *exact = true;
                return entry->macro;
            }
            if ((extended || !entry->extended) && (cshift || !entry->cshift))
                macro = entry->macro;
        }
    }
    *exact = false;
    return macro;
}

void skin_invalidate_key(int key) {
    if (key >= -7 && key <= -2) {
        /* Soft key */
        key = -1 - key;
        double left = display_loc.x + ((key - 1) * 22 - 1) * display_scale_x;
        double top = display_loc.y + (9 - 1) * display_scale_y;
        double right = left + (21 + 2) * display_scale_x;
        double bottom = top + (7 + 2) * display_scale_y;
        skin_invalidate((int) left, (int) top, (int) ceil(right), (int) ceil(bottom));
    } else if (key < 0 || key >= nkeys) {
        return;
    } else {
        SkinRect* rect = &keylist[key].disp_rect;
        skin_invalidate(rect->x, rect->y, rect->x + rect->width, rect->y + rect->height);
    }
}

void skin_display_blitter(const char *bits, int bytesperline, int x, int y, int width, int height) {
    // Drawing calculator pixels as 4x4 pixel blocks in order to reduce blurriness when scaled
    for (int v = y; v < y + height; v++)
        for (int h = x; h < x + width; h++) {
            int pixel = (bits[v * bytesperline + (h >> 3)] & (1 << (h & 7))) != 0;
            int baddr = (v * 4 + 4) * disp_bytesperline + ((h + 1) >> 1);
            char newb;
            if (pixel)
                newb = disp_bits[baddr] | ((h & 1) != 0 ? 240 : 15);
            else
                newb = disp_bits[baddr] & ((h & 1) != 0 ? 15 : 240);
            for (int i = 0; i < 4; i++) {
                disp_bits[baddr] = newb;
                baddr += disp_bytesperline;
            }
        }
    
    skin_invalidate((int) (display_loc.x + (x - 1) * display_scale_x),
                    (int) (display_loc.y + (y - 1) * display_scale_y),
                    (int) ceil(display_loc.x + (x + width + 1) * display_scale_x),
                    (int) ceil(display_loc.y + (y + height + 1) * display_scale_y));
}

void skin_get_size(int *width, int *height) {
    *width = skin.width;
    *height = skin.height;
}

void skin_set_window(HWND win) {
    window = win;
}

void skin_set_window_size(int width, int height) {
    window_width = width;
    window_height = height;
}

void skin_get_window_size(int *width, int *height) {
    *width = window_width;
    *height = window_height;
}
