/*****************************************************************************
 * 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/.
 *****************************************************************************/

// shell.cpp : Defines the entry point for the application.
//

#include <sys/types.h>
#include <sys/stat.h>
#include <direct.h>
#include <stdio.h>
#include <shlobj.h>

#include <set>

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

#include "free42.h"
#include "shell.h"
#include "shell_skin.h"
#include "shell_spool.h"
#include "core_main.h"
#include "core_display.h"
#include "msg2string.h"
#include "StatesWindow.h"
#include "shell_main.h"

#include "VERSION.h"

using std::set;


#define MAX_LOADSTRING 100

// The maximum height of a Bitmap is 32767 pixels;
// so, if I want to use a larger buffer, I'll have to
// change repaint_printout() so that it creates
// its Bitmap objects a bit more cleverly. TODO.

#define PRINT_LINES 16384
#define PRINT_BYTESPERLINE 36
#define PRINT_SIZE 589824
/*
#define PRINT_LINES 32767
#define PRINT_BYTESPERLINE 36
#define PRINT_SIZE 1179612
*/
#define PRINT_TEXT_SIZE 22826

/**********************************************************/
/* 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[];


// Global Variables:
HINSTANCE hInst;                                   // current instance
static HWND hMainWnd;                              // our main window
static HWND hPrintOutWnd;                          // our print-out window
static char szMainTitle[MAX_LOADSTRING];           // The main title bar text
static char szPrintOutTitle[MAX_LOADSTRING];       // The print-out title bar text
static char szMainWindowClass[MAX_LOADSTRING];     // The main window class
static char szPrintOutWindowClass[MAX_LOADSTRING]; // The print-out window class

static UINT_PTR timer = 0;
static UINT_PTR timer3 = 0;
static bool running = false;
static bool enqueued = false;

static char *printout;
static int printout_top;
static int printout_bottom;
static int printout_pos;
static char *print_text;
static int print_text_top;
static int print_text_bottom;
static int print_text_pixel_height;

int ckey = 0;
int skey = -1;
static unsigned char *macro;
static int macro_type;
static int active_keycode = 0;
static bool ctrl_down = false;
static bool alt_down = false;
static bool shift_down = false;
static bool just_pressed_shift = false;
static bool mouse_key;

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


#define SHELL_VERSION 14

state_type state;
static int placement_saved = 0;
static int printOutWidth;
static int printOutHeight;

wchar_t free42dirname[FILENAMELEN];
static wchar_t statefilename[FILENAMELEN];
static FILE *statefile = NULL;
static wchar_t printfilename[FILENAMELEN];

static FILE *print_txt = NULL;
static FILE *print_gif = NULL;
static wchar_t print_gif_name[FILENAMELEN];
static int gif_seq = -1;
static int gif_lines;

static int sel_prog_count;
static int *sel_prog_list;

int ann_updown = 0;
int ann_shift = 0;
int ann_print = 0;
int ann_run = 0;
int ann_battery = 0;
int ann_g = 0;
int ann_rad = 0;
static UINT_PTR ann_print_timer = 0;


// Forward declarations of functions included in this code module:
static void MyRegisterClass(HINSTANCE hInstance);
static BOOL InitInstance(HINSTANCE, int);
static LRESULT CALLBACK MainWndProc(HWND, UINT, WPARAM, LPARAM);
static LRESULT CALLBACK PrintOutWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
static LRESULT CALLBACK About(HWND, UINT, WPARAM, LPARAM);
static LRESULT CALLBACK ExportProgram(HWND, UINT, WPARAM, LPARAM);
static LRESULT CALLBACK Preferences(HWND, UINT, WPARAM, LPARAM);
static void get_home_dir(wchar_t *path, int pathlen);
static void copy();
static void paste();
static void Quit();

static VOID CALLBACK repeater(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime);
static VOID CALLBACK timeout1(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime);
static VOID CALLBACK timeout2(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime);
static VOID CALLBACK timeout3(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime);
static VOID CALLBACK battery_checker(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime);

static void show_printout();
static void export_program();
static void import_program();
static void paper_advance();
static void copy_print_as_text();
static void copy_print_as_image();
static void clear_printout();
static void repaint_printout(int x, int y, int width, int height, int validate);
static void repaint_printout(HDC hdc, int destpos, int x, int y, int width, int height, int validate);
static void printout_scrolled(int offset);
static void printout_scroll_to_bottom(int offset);
static void printout_length_changed();

static void read_key_map(const wchar_t *keymapfilename);
static void init_shell_state(int4 version);
static int read_shell_state(int4 *version);
static int write_shell_state();
static void txt_writer(const char *text, int length);
static void txt_newliner();
static void gif_seeker(int4 pos);
static void gif_writer(const char *text, int length);

static wchar_t *utf2wide(const char *s);


int APIENTRY WinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPSTR     lpCmdLine,
                     int       nCmdShow)
{
    MSG msg;
    HACCEL hAccelTable;

    // Initialize global strings
#ifdef BCD_MATH
    LoadString(hInstance, IDS_APP_TITLE_DEC, szMainTitle, MAX_LOADSTRING);
#else
    LoadString(hInstance, IDS_APP_TITLE_BIN, szMainTitle, MAX_LOADSTRING);
#endif
    LoadString(hInstance, IDS_PRINTOUT_TITLE, szPrintOutTitle, MAX_LOADSTRING);
    LoadString(hInstance, IDC_FREE42, szMainWindowClass, MAX_LOADSTRING);
    LoadString(hInstance, IDC_FREE42_PRINTOUT, szPrintOutWindowClass, MAX_LOADSTRING);

    MyRegisterClass(hInstance);

    // GDI+ initialization
    GdiplusStartupInput gdiplusStartupInput;
    ULONG_PTR gdiplusToken;
    GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);

    // Perform application initialization:
    if (!InitInstance (hInstance, nCmdShow)) 
    {
        return FALSE;
    }

    hAccelTable = LoadAccelerators(hInstance, (LPCTSTR)IDC_FREE42);

    // Main message loop:
    while (1) {
        while (running && !PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE)) {
            bool dummy1;
            int dummy2;
            running = core_keydown(0, &dummy1, &dummy2);
        }
        if (!GetMessage(&msg, NULL, 0, 0)) 
            break;
        if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) 
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }

    Quit();
    GdiplusShutdown(gdiplusToken);
    return (int) msg.wParam;
}



//
//  FUNCTION: MyRegisterClass()
//
//  PURPOSE: Registers the window class.
//
//  COMMENTS:
//
//    This function and its usage is only necessary if you want this code
//    to be compatible with Win32 systems prior to the 'RegisterClassEx'
//    function that was added to Windows 95. It is important to call this function
//    so that the application will get 'well formed' small icons associated
//    with it.
//
static void MyRegisterClass(HINSTANCE hInstance)
{
    WNDCLASSEX wcex1, wcex2;

    wcex1.cbSize = sizeof(WNDCLASSEX); 

    wcex1.style         = CS_HREDRAW | CS_VREDRAW;
    wcex1.lpfnWndProc   = (WNDPROC) MainWndProc;
    wcex1.cbClsExtra    = 0;
    wcex1.cbWndExtra    = 0;
    wcex1.hInstance     = hInstance;
    wcex1.hIcon         = LoadIcon(hInstance, (LPCTSTR) IDI_FREE42);
    wcex1.hCursor       = LoadCursor(NULL, IDC_ARROW);
    wcex1.hbrBackground = (HBRUSH) (COLOR_WINDOW+1);
    wcex1.lpszMenuName  = (LPCSTR) IDC_FREE42;
    wcex1.lpszClassName = szMainWindowClass;
    wcex1.hIconSm       = NULL;

    RegisterClassEx(&wcex1);

    wcex2.cbSize = sizeof(WNDCLASSEX); 

    wcex2.style         = CS_HREDRAW | CS_VREDRAW;
    wcex2.lpfnWndProc   = (WNDPROC) PrintOutWndProc;
    wcex2.cbClsExtra    = 0;
    wcex2.cbWndExtra    = 0;
    wcex2.hInstance     = hInstance;
    wcex2.hIcon         = LoadIcon(hInstance, (LPCTSTR) IDI_FREE42);
    wcex2.hCursor       = LoadCursor(NULL, IDC_ARROW);
    wcex2.hbrBackground = (HBRUSH) (COLOR_WINDOW+1);
    wcex2.lpszMenuName  = NULL;
    wcex2.lpszClassName = szPrintOutWindowClass;
    wcex2.hIconSm       = NULL;

    RegisterClassEx(&wcex2);
}

//
//   FUNCTION: InitInstance(HANDLE, int)
//
//   PURPOSE: Saves instance handle and creates main window
//
//   COMMENTS:
//
//        In this function, we save the instance handle in a global variable and
//        create and display the main program window.
//
static BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
    HWND hPrevWnd = FindWindow(szMainWindowClass, szMainTitle);
    if (hPrevWnd != NULL) {
        if (IsIconic(hPrevWnd))
            OpenIcon(hPrevWnd);
        SetForegroundWindow(hPrevWnd);
        return FALSE;
    }

    hInst = hInstance; // Store instance handle in our global variable

    srand(GetTickCount());

    /**********************************************/
    /***** Try to create the Free42 directory *****/
    /**********************************************/

    get_home_dir(free42dirname, FILENAMELEN);
    _wmkdir(free42dirname);

    wchar_t keymapfilename[FILENAMELEN];
    swprintf(statefilename, L"%ls\\state.bin", free42dirname);
    swprintf(printfilename, L"%ls\\print.bin", free42dirname);
    swprintf(keymapfilename, L"%ls\\keymap.txt", free42dirname);

    read_key_map(keymapfilename);

    printout = (char *) malloc(PRINT_SIZE);
    print_text = (char *) malloc(PRINT_TEXT_SIZE);
    // TODO - handle memory allocation failure
    FILE *printfile = _wfopen(printfilename, L"rb");
    if (printfile != NULL) {
        size_t n = fread(&printout_bottom, 1, sizeof(int), printfile);
        if (n == sizeof(int)) {
            size_t bytes = printout_bottom * PRINT_BYTESPERLINE;
            n = fread(printout, 1, bytes, printfile);
            if (n == bytes) {
                n = fread(&print_text_bottom, 1, sizeof(int), printfile);
                size_t n2 = fread(&print_text_pixel_height, 1, sizeof(int), printfile);
                if (n == sizeof(int) && n2 == sizeof(int)) {
                    n = fread(print_text, 1, print_text_bottom, printfile);
                    if (n != print_text_bottom) {
                        print_text_bottom = 0;
                        print_text_pixel_height = 0;
                    }
                } else {
                    print_text_bottom = 0;
                    print_text_pixel_height = 0;
                }
            } else {
                printout_bottom = 0;
                print_text_bottom = 0;
                print_text_pixel_height = 0;
            }
        } else {
            printout_bottom = 0;
            print_text_bottom = 0;
            print_text_pixel_height = 0;
        }
        fclose(printfile);
    } else {
        printout_bottom = 0;
        print_text_bottom = 0;
        print_text_pixel_height = 0;
    }
    printout_top = 0;
    print_text_top = 0;

    printout_pos = printout_bottom;

    int init_mode;
    int4 version;
    wchar_t core_state_file_name[FILENAMELEN];
    size_t core_state_file_offset;

    statefile = _wfopen(statefilename, L"rb");
    if (statefile != NULL) {
        if (read_shell_state(&version))
            init_mode = 1;
        else {
            init_shell_state(-1);
            init_mode = 2;
        }
    } else {
        init_shell_state(-1);
        init_mode = 0;
    }
    if (init_mode == 1) {
        if (version > 25) {
            swprintf(core_state_file_name, L"%ls\\%ls.f42", free42dirname, state.coreName);
            core_state_file_offset = 0;
        } else {
            wcscpy(core_state_file_name, statefilename);
            core_state_file_offset = ftell(statefile);
        }
        fclose(statefile);
    } else {
        // The shell state was missing or corrupt, but there
        // may still be a valid core state...
        swprintf(core_state_file_name, L"%ls\\%ls.f42", free42dirname, state.coreName);
        if (GetFileAttributesW(core_state_file_name) != INVALID_FILE_ATTRIBUTES) {
            // Core state "Untitled.f42" exists; let's try to read it
            core_state_file_offset = 0;
            init_mode = 1;
            version = 26;
        }
    }

    RECT r;

    skin_load(state.skinName, free42dirname, &r.right, &r.bottom);
    r.top = 0;
    r.left = 0;
    if (state.mainWindowWidth != 0) {
        r.right = state.mainWindowWidth;
        r.bottom = state.mainWindowHeight;
        skin_set_window_size(state.mainWindowWidth, state.mainWindowHeight);
    }
    AdjustWindowRect(&r, WS_CAPTION|WS_SYSMENU|WS_MINIMIZEBOX|WS_SIZEBOX|WS_OVERLAPPED, 1);

    hMainWnd = CreateWindow(szMainWindowClass, szMainTitle,
                            WS_CAPTION|WS_SYSMENU|WS_MINIMIZEBOX|WS_SIZEBOX|WS_OVERLAPPED,
                            CW_USEDEFAULT, 0, r.right - r.left, r.bottom - r.top,
                            NULL, NULL, hInstance, NULL);

    if (hMainWnd == NULL)
        return FALSE;
    skin_set_window(hMainWnd);

    char *csfn = wide2utf(core_state_file_name);
    core_init(init_mode, version, csfn, (int) core_state_file_offset);
    free(csfn);

    if (state.mainPlacementValid) {
        // Fix the size, in case the saved settings are not appropriate
        // for the current desktop properties (the decor dimensions may
        // be different).
        RECT *r2 = &state.mainPlacement.rcNormalPosition;
        r2->right = r2->left + r.right - r.left;
        r2->bottom = r2->top + r.bottom - r.top;
        SetWindowPlacement(hMainWnd, &state.mainPlacement);
    }
    if (state.alwaysOnTop)
        SetWindowPos(hMainWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
    ShowWindow(hMainWnd, nCmdShow);
    UpdateWindow(hMainWnd);

    if (state.printOutOpen) {
        show_printout();
        BringWindowToTop(hMainWnd);
    }

    if (core_powercycle())
        running = true;
    SetTimer(NULL, 0, 60000, battery_checker);

    return TRUE;
}

static void shell_keydown(bool cshift) {
    if (ckey != 0) {
        if (skey == -1)
            skey = skin_find_skey(ckey, cshift);
        skin_invalidate_key(skey);
    }
    if (timer != 0) {
        KillTimer(NULL, timer);
        timer = 0;
    }
    if (timer3 != 0 && (macro != NULL || ckey != 28 /* SHIFT */)) {
        KillTimer(NULL, timer3);
        timer3 = 0; 
        core_timeout3(false);
    }
    int repeat;
    if (macro != NULL) {
        if (macro_type != 0) {
            running = core_keydown_command((const char *) macro, macro_type == 2, &enqueued, &repeat);
        } else {
            if (*macro == 0) {
                squeak();
                return;
            }
            bool one_key_macro = macro[1] == 0 || (macro[2] == 0 && macro[0] == 28);
            if (one_key_macro) {
                while (*macro != 0) {
                    running = core_keydown(*macro++, &enqueued, &repeat);
                    if (*macro != 0 && !enqueued)
                        core_keyup();
                }
            } else {
                bool waitForProgram = !program_running();
                while (*macro != 0) {
                    running = core_keydown(*macro++, &enqueued, &repeat);
                    if (*macro != 0 && !enqueued)
                        running = core_keyup();
                    while (waitForProgram && running)
                        running = core_keydown(0, &enqueued, &repeat);
                }
                repeat = 0;
            }
        }
    } else
        running = core_keydown(ckey, &enqueued, &repeat);
    if (!running) {
        if (repeat != 0)
            timer = SetTimer(NULL, 0, repeat == 1 ? 1000 : 500, repeater);
        else if (!enqueued)
            timer = SetTimer(NULL, 0, 250, timeout1);
    }
}

static void shell_keyup() {
    skin_invalidate_key(skey);
    ckey = 0;
    skey = -1;
    if (timer != 0) {
        KillTimer(NULL, timer);
        timer = 0;
    }
    if (!enqueued)
        running = core_keyup();
}

static bool keyboardShortcutsShowing = false;

static void toggle_keyboard_shortcuts() {
    keyboardShortcutsShowing = !keyboardShortcutsShowing;
    HMENU mainmenu = GetMenu(hMainWnd);
    HMENU helpmenu = GetSubMenu(mainmenu, 3);
    CheckMenuItem(helpmenu, IDM_SHORTCUTS, keyboardShortcutsShowing ? MF_CHECKED : MF_UNCHECKED);
    InvalidateRect(hMainWnd, NULL, FALSE);
}

//
//  FUNCTION: WndProc(HWND, unsigned, WORD, LONG)
//
//  PURPOSE:  Processes messages for the main window.
//
//  WM_COMMAND  - process the application menu
//  WM_PAINT    - Paint the main window
//  WM_DESTROY  - post a quit message and return
//
//
static LRESULT CALLBACK MainWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
#if 0
    static FILE *log = fopen("C:/Users/thomas/Desktop/log.txt", "w");
    if (message == WM_CHAR || message == WM_SYSCHAR
            || message == WM_KEYDOWN || message == WM_SYSKEYDOWN
            || message == WM_KEYUP || message == WM_SYSKEYUP) {
        fprintf(log, "message=%s wParam=0x%x lParam=0x%lx\n", msg2string(message), wParam, lParam);
        fflush(log);
    }
#endif

    switch (message) {
        case WM_CREATE: {
            BufferedPaintInit();
            return 0;
        }
        case WM_NCDESTROY: {
            BufferedPaintUnInit();
            return 0;
        }
        case WM_COMMAND: {
            int wmId    = LOWORD(wParam); 
            int wmEvent = HIWORD(wParam); 
            // Parse the menu selections:
            switch (wmId) {
                case IDM_STATES:
                    running = DialogBoxW(hInst, (LPCWSTR)IDD_STATES, hWnd, (DLGPROC)StatesDlgProc) != 0;
                    break;
                case IDM_SHOWPRINTOUT:
                    show_printout();
                    break;
                case IDM_PAPERADVANCE:
                    paper_advance();
                    break;
                case IDM_EXPORTPROGRAM:
                    export_program();
                    break;
                case IDM_IMPORTPROGRAM:
                    import_program();
                    break;
                case IDM_COPYPRINTASTEXT:
                    copy_print_as_text();
                    break;
                case IDM_COPYPRINTASIMAGE:
                    copy_print_as_image();
                    break;
                case IDM_CLEARPRINTOUT:
                    clear_printout();
                    break;
                case IDM_PREFERENCES:
                    DialogBoxW(hInst, (LPCWSTR)IDD_PREFERENCES, hWnd, (DLGPROC)Preferences);
                    break;
                case IDM_EXIT:
                    DestroyWindow(hWnd);
                    break;
                case ID_EDIT_COPY:
                    copy();
                    break;
                case ID_EDIT_PASTE:
                    paste();
                    break;
                case IDM_DOCUMENTATION:
                    ShellExecute(NULL, "open", "https://thomasokken.com/free42/#doc", NULL, NULL, SW_SHOWNORMAL);
                    break;
                case IDM_WEBSITE:
                    ShellExecute(NULL, "open", "https://thomasokken.com/free42/", NULL, NULL, SW_SHOWNORMAL);
                    break;
                case IDM_OTHER_WEBSITE:
                    ShellExecute(NULL, "open", "https://thomasokken.com/plus42/", NULL, NULL, SW_SHOWNORMAL);
                    break;
                case IDM_SHORTCUTS:
                    toggle_keyboard_shortcuts();
                    break;
                case IDM_EDIT_KEYMAP: {
                    wchar_t keymapname[FILENAMELEN];
                    get_home_dir(keymapname, FILENAMELEN);
                    wcscat(keymapname, L"\\keymap.txt");
                    ShellExecuteW(NULL, L"edit", keymapname, NULL, NULL, SW_SHOWNORMAL);
                    break;
                }
                case IDM_ABOUT:
                    DialogBoxW(hInst, (LPCWSTR)IDD_ABOUTBOX, hWnd, (DLGPROC)About);
                    break;
                default:
                    if (wmId >= 40000) {
                        // 'Skin' menu
                        HMENU mainmenu = GetMenu(hWnd);
                        HMENU skinmenu = GetSubMenu(mainmenu, 2);
                        MENUITEMINFOW mii;
                        mii.cbSize = sizeof(MENUITEMINFO);
                        mii.fMask = MIIM_TYPE;
                        mii.cch = FILENAMELEN;
                        mii.dwTypeData = state.skinName;
                        GetMenuItemInfoW(skinmenu, wmId, FALSE, &mii);

                        // The following is really just skin_load(), followed by
                        // resizing the window. Unfortunately, I couldn't find a
                        // function that resizes a window without setting its position
                        // at the same time, hence the contortions.
                        RECT r;
                        GetWindowRect(hWnd, &r);
                        long width, height;
                        skin_load(state.skinName, free42dirname, &width, &height);
                        core_repaint_display();
                        r.right = r.left + width;
                        r.bottom = r.top + height;
                        LONG dx = r.left;
                        LONG dy = r.top;
                        AdjustWindowRect(&r, WS_CAPTION|WS_SYSMENU|WS_MINIMIZEBOX|WS_OVERLAPPED, 1);
                        dx -= r.left;
                        dy -= r.top;
                        OffsetRect(&r, dx, dy);
                        MoveWindow(hWnd, r.left, r.top, r.right - r.left, r.bottom - r.top, TRUE);
                        SetRect(&r, 0, 0, width, height);
                        InvalidateRect(hWnd, &r, FALSE);
                        break;
                    }
                    return DefWindowProc(hWnd, message, wParam, lParam);
            }
            break;
        }
        case WM_GETMINMAXINFO: {
            RECT windowRect, clientRect;
            GetWindowRect(hWnd, &windowRect);
            GetClientRect(hWnd, &clientRect);
            if (IsRectEmpty(&clientRect))
                return DefWindowProc(hWnd, message, wParam, lParam);
            int vBorder = (windowRect.bottom - windowRect.top) - (clientRect.bottom - clientRect.top);
            int hBorder = (windowRect.right - windowRect.left) - (clientRect.right - clientRect.left);

            LPMINMAXINFO lpMMI = (LPMINMAXINFO) lParam;
            lpMMI->ptMinTrackSize.x = 160 + hBorder;
            lpMMI->ptMinTrackSize.y = 160 + vBorder;

            return 0;
        }
        case WM_SIZING: {
            // Enforce aspect ratio.

            PRECT newRect = (PRECT) lParam;
            RECT windowRect, clientRect;

            GetWindowRect(hWnd, &windowRect);
            GetClientRect(hWnd, &clientRect);
            int vBorder = (windowRect.bottom - windowRect.top) - (clientRect.bottom - clientRect.top);
            int hBorder = (windowRect.right - windowRect.left) - (clientRect.right - clientRect.left);

            int skinWidth, skinHeight;
            skin_get_size(&skinWidth, &skinHeight);
            double vScale = ((double) (newRect->bottom - newRect->top - vBorder)) / skinHeight;
            double hScale = ((double) (newRect->right - newRect->left - hBorder)) / skinWidth;

            switch (wParam) {
                case WMSZ_TOP:
                case WMSZ_BOTTOM: {
                    newRect->right += (int) ((vScale - hScale) * skinWidth + 0.5);
                    break;
                }
                case WMSZ_LEFT:
                case WMSZ_RIGHT: {
                    newRect->bottom += (int) ((hScale - vScale) * skinHeight + 0.5);
                    break;
                }
                case WMSZ_TOPRIGHT: {
                    if (hScale > vScale)
                        newRect->right -= (int) ((hScale - vScale) * skinWidth + 0.5);
                    else if (vScale > hScale)
                        newRect->top += (int) ((vScale - hScale) * skinHeight + 0.5);
                    break;
                }
                case WMSZ_BOTTOMRIGHT: {
                    if (hScale > vScale)
                        newRect->right -= (int) ((hScale - vScale) * skinWidth + 0.5);
                    else if (vScale > hScale)
                        newRect->bottom -= (int) ((vScale - hScale) * skinHeight + 0.5);
                    break;
                }
                case WMSZ_BOTTOMLEFT: {
                    if (hScale > vScale)
                        newRect->left += (int) ((hScale - vScale) * skinWidth + 0.5);
                    else if (vScale > hScale)
                        newRect->bottom -= (int) ((vScale - hScale) * skinHeight + 0.5);
                    break;
                }
                case WMSZ_TOPLEFT:
                {
                    if (hScale > vScale)
                        newRect->left += (int) ((hScale - vScale) * skinWidth + 0.5);
                    else if (vScale > hScale)
                        newRect->top += (int) ((vScale - hScale) * skinHeight + 0.5);
                    break;
                }
            }

            return true;
        }
        case WM_SIZE: {
            int w = LOWORD(lParam);
            int h = HIWORD(lParam);
            if (w != 0 && h != 0)
                skin_set_window_size(w, h);
            goto do_default;
        }
        case WM_PAINT: {
            skin_repaint(keyboardShortcutsShowing);
            break;
        }
        case WM_LBUTTONDOWN: {
            if (ckey == 0) {
                int x = LOWORD(lParam);  // horizontal position of cursor
                int y = HIWORD(lParam);  // vertical position of cursor
                skin_find_key(x, y, ann_shift != 0, &skey, &ckey);
                if (ckey != 0) {
                    macro = skin_find_macro(ckey, &macro_type);
                    shell_keydown(ann_shift != 0);
                    mouse_key = true;
                }
            }
            break;
        }
        case WM_LBUTTONUP:
            if (ckey != 0 && mouse_key)
                shell_keyup();
            break;
        case WM_KEYDOWN:
        case WM_CHAR:
        case WM_SYSKEYDOWN:
        case WM_SYSCHAR: {
            static int virtKey = 0;
            int keyChar;

            if ((lParam & (1 << 30)) != 0)
                // Auto-repeat event; ignore.
                break;
            if (message == WM_KEYDOWN || message == WM_SYSKEYDOWN) {
                keyChar = 0;
                virtKey = (int) wParam;
            } else
                keyChar = (int) wParam;
            just_pressed_shift = false;
            if (virtKey == 17) {
                ctrl_down = true;
                goto do_default;
            } else if (virtKey == 18) {
                alt_down = true;
                goto do_default;
            } else if (virtKey == 16) {
                shift_down = true;
                just_pressed_shift = true;
                goto do_default;
            }

            if (message == WM_KEYDOWN || message == WM_SYSKEYDOWN) {
                MSG cmsg;
                UINT cmsgtype = message == WM_KEYDOWN ? WM_CHAR : WM_SYSCHAR;
                if (PeekMessage(&cmsg, hWnd, cmsgtype, cmsgtype, PM_NOREMOVE)
                        && cmsg.lParam == lParam) {
                    // Keystrokes that are followed by a WM_CHAR or WM_SYSCHAR
                    // message; we defer handling them until then.
                    break;
                }
            }

            if (ckey == 0 || !mouse_key) {
                int i;
                bool printable = keyChar >= 32 && keyChar <= 126;
                if (ckey != 0) {
                    shell_keyup();
                    active_keycode = 0;
                }

                bool exact;
                bool extended = (lParam & (1 << 24)) != 0;
                bool cshift_down = ann_shift != 0;
                unsigned char *key_macro = skin_keymap_lookup(virtKey, ctrl_down, alt_down, extended, shift_down, cshift_down, &exact);
                if (key_macro == NULL || !exact) {
                    for (i = 0; i < keymap_length; i++) {
                        keymap_entry *entry = keymap + i;
                        if (ctrl_down == entry->ctrl
                                && alt_down == entry->alt
                                && shift_down == entry->shift
                                && virtKey == entry->keycode) {
                            if (extended == entry->extended && cshift_down == entry->cshift) {
                                key_macro = entry->macro;
                                break;
                            } else {
                                if ((extended || !entry->extended) && (cshift_down || !entry->cshift) && key_macro == NULL)
                                    key_macro = entry->macro;
                            }
                        }
                    }
                }

                if (key_macro == NULL || (key_macro[0] != 36 || key_macro[1] != 0)
                        && (key_macro[0] != 28 || key_macro[1] != 36 || key_macro[2] != 0)) {
                    // The test above is to make sure that whatever mapping is in
                    // effect for R/S will never be overridden by the special cases
                    // for the ALPHA and A..F menus.
                    if (printable && core_alpha_menu()) {
                        if (keyChar >= 'a' && keyChar <= 'z')
                            keyChar = keyChar + 'A' - 'a';
                        else if (keyChar >= 'A' && keyChar <= 'Z')
                            keyChar = keyChar + 'a' - 'A';
                        ckey = 1024 + keyChar;
                        skey = -1;
                        macro = NULL;
                        shell_keydown(false);
                        mouse_key = false;
                        active_keycode = virtKey;
                        break;
                    } else if (core_hex_menu() && ((keyChar >= 'a' && keyChar <= 'f')
                                || (keyChar >= 'A' && keyChar <= 'F'))) {
                        if (keyChar >= 'a' && keyChar <= 'f')
                            ckey = keyChar - 'a' + 1;
                        else
                            ckey = keyChar - 'A' + 1;
                        skey = -1;
                        macro = NULL;
                        shell_keydown(false);
                        mouse_key = false;
                        active_keycode = virtKey;
                        break;
                    } else if (virtKey == 37 || virtKey == 39 || virtKey == 46) {
                        int which;
                        if (virtKey == 37)
                            which = shift_down ? 2 : 1;
                        else if (virtKey == 39)
                            which = shift_down ? 4 : 3;
                        else if (virtKey == 46)
                            which = 5;
                        else
                            which = 0;
                        if (which != 0) {
                            which = core_special_menu_key(which);
                            if (which != 0) {
                                ckey = which;
                                skey = -1;
                                macro = NULL;
                                shell_keydown(false);
                                mouse_key = false;
                                active_keycode = virtKey;
                                break;
                            }
                        }
                    }
                }

                if (key_macro != NULL) {
                    // A keymap entry is a sequence of zero or more calculator
                    // keystrokes (1..37) and/or macros (38..255). We expand
                    // macros here before invoking shell_keydown().
                    // If the keymap entry is one key, or two keys with the
                    // first being 'shift', we highlight the key in question
                    // by setting ckey; otherwise, we set ckey to -10, which
                    // means no skin key will be highlighted.
                    ckey = -10;
                    skey = -1;
                    bool skin_shift = cshift_down;
                    if (key_macro[0] != 0)
                        if (key_macro[1] == 0)
                            ckey = key_macro[0];
                        else if (key_macro[2] == 0 && key_macro[0] == 28) {
                            ckey = key_macro[1];
                            skin_shift = true;
                        }
                    bool needs_expansion = false;
                    for (int j = 0; key_macro[j] != 0; j++)
                        if (key_macro[j] > 37) {
                            needs_expansion = true;
                            break;
                        }
                    if (needs_expansion) {
                        static unsigned char macrobuf[1024];
                        int p = 0;
                        for (int j = 0; key_macro[j] != 0 && p < 1023; j++) {
                            int c = key_macro[j];
                            if (c <= 37)
                                macrobuf[p++] = c;
                            else {
                                unsigned char *m = skin_find_macro(c, &macro_type);
                                if (m != NULL)
                                    while (*m != 0 && p < 1023)
                                        macrobuf[p++] = *m++;
                            }
                        }
                        macrobuf[p] = 0;
                        macro = macrobuf;
                    } else {
                        macro = key_macro;
                        macro_type = 0;
                    }
                    shell_keydown(skin_shift);
                    mouse_key = false;
                    active_keycode = virtKey;
                    break;
                }
            }
            goto do_default;
        }
        case WM_KEYUP:
        case WM_SYSKEYUP: {
            int virtKey = (int) wParam;
            if (virtKey == 17) {
                ctrl_down = false;
                goto do_default;
            } else if (virtKey == 18) {
                alt_down = false;
                goto do_default;
            } else if (virtKey == 16) {
                shift_down = false;
                if (ckey == 0 && just_pressed_shift) {
                    ckey = 28;
                    skey = -1;
                    macro = NULL;
                    shell_keydown(false);
                    shell_keyup();
                }
                goto do_default;
            }
            if (ckey != 0 && !mouse_key && virtKey == active_keycode) {
                shell_keyup();
                active_keycode = 0;
            }
            goto do_default;
        }
        case WM_ENDSESSION:
            if (wParam != 0)
                Quit();
            break;
        case WM_DESTROY:
            GetWindowPlacement(hMainWnd, &state.mainPlacement);
            state.mainPlacementValid = 1;
            if (state.printOutOpen) {
                GetWindowPlacement(hPrintOutWnd, &state.printOutPlacement);
                state.printOutPlacementValid = 1;
            }
            placement_saved = 1;
            PostQuitMessage(0);
            break;
        case WM_INITMENUPOPUP: {
            if (HIWORD(lParam) != 0 || LOWORD(lParam) != 2)
                // HIWORD(lParam) is nonzero if the system menu is selected;
                // LOWORD(lParam) is the index of the selected menu.
                // I want to update the 'Skin' menu, which is at index 2;
                // any other index I don't care about, since those other menus
                // are all static.
                break;
            HMENU menu = (HMENU) wParam;
            UINT id = 40000;
            if (state.skinName[0] == 0)
                wcscpy(state.skinName, skin_name[0]);

            int i;
            for (i = GetMenuItemCount(menu) - 1; i >= 0; i--)
                RemoveMenu(menu, i, MF_BYPOSITION);

            wchar_t path[MAX_PATH];
            path[MAX_PATH - 1] = 0;
            WIN32_FIND_DATAW wfd;

            // Search home directory
            set<ci_string> private_skins;
            wcsncpy(path, free42dirname, MAX_PATH - 1);
            wcsncat(path, L"\\*.layout", MAX_PATH - 1);
            path[MAX_PATH - 1] = 0;
            HANDLE search = FindFirstFileW(path, &wfd);
            if (search != INVALID_HANDLE_VALUE) {
                do {
                    if ((wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) {
                        wfd.cFileName[wcslen(wfd.cFileName) - 7] = 0;
                        private_skins.insert(ci_string(wfd.cFileName));
                    }
                } while (FindNextFileW(search, &wfd));
                FindClose(search);
            }

            // Search executable's directory
            set<ci_string> shared_skins;
            wchar_t exedir[MAX_PATH];
            GetModuleFileNameW(0, exedir, MAX_PATH - 1);
            wchar_t *lastbackslash = wcsrchr(exedir, L'\\');
            if (lastbackslash != 0)
                *lastbackslash = 0;
            else
                wcscpy(exedir, L"C:");
            if (_wcsicmp(exedir, free42dirname) != 0) {
                wcsncat(exedir, L"\\*.layout", MAX_PATH - 1);
                exedir[MAX_PATH - 1] = 0;
                search = FindFirstFileW(exedir, &wfd);
                if (search != INVALID_HANDLE_VALUE) {
                    do {
                        if ((wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) {
                            wfd.cFileName[wcslen(wfd.cFileName) - 7] = 0;
                            shared_skins.insert(ci_string(wfd.cFileName));
                        }
                    } while (FindNextFileW(search, &wfd));
                    FindClose(search);
                }
            }

            for (i = 0; i < skin_count; i++) {
                UINT flags = 0;
                ci_string name = skin_name[i];
                const wchar_t *cname = name.c_str();
                if (private_skins.find(name) == private_skins.end() && shared_skins.find(name) == shared_skins.end()) {
                    if (_wcsicmp(state.skinName, cname) == 0)
                        flags = MF_CHECKED;
                } else
                    flags = MF_DISABLED;
                AppendMenuW(menu, flags, id++, cname);
            }

            if (!shared_skins.empty()) {
                AppendMenu(menu, MF_SEPARATOR, 0, NULL);
                for (set<ci_string>::const_iterator i = shared_skins.begin(); i != shared_skins.end(); i++) {
                    UINT flags = 0;
                    ci_string name = *i;
                    const wchar_t *cname = name.c_str();
                    if (private_skins.find(name) == private_skins.end()) {
                        if (_wcsicmp(state.skinName, cname) == 0)
                            flags = MF_CHECKED;
                    } else
                        flags = MF_DISABLED;
                    AppendMenuW(menu, flags, id++, cname);
                }
            }

            if (!private_skins.empty()) {
                AppendMenu(menu, MF_SEPARATOR, 0, NULL);
                for (set<ci_string>::const_iterator i = private_skins.begin(); i != private_skins.end(); i++) {
                    UINT flags = 0;
                    ci_string name = *i;
                    const wchar_t *cname = name.c_str();
                    if (_wcsicmp(state.skinName, cname) == 0)
                        flags = MF_CHECKED;
                    AppendMenuW(menu, flags, id++, cname);
                }
            }
            break;
        }
        case WM_ACTIVATE: {
            int p = LOWORD(wParam);
            if (p == WA_INACTIVE)
                // This is needed because when using Alt-Tab to leave
                // Free42, Windows Vista and later don't send a key-up
                // for the Alt key, with the result that the app only
                // sees the key-down for Alt, and the menu bar remains
                // stuck in Alt mode.
                // (Windows Vista and 7 do do the right thing if the
                // Windows Classic theme is active, but of course you
                // can't depend on that theme being used, and in
                // Windows 8 and later, it isn't available any more.)
                PostMessage(hMainWnd, WM_SYSKEYUP, 18, 0);
            goto do_default;
        }
        default:
        do_default:
            return DefWindowProc(hWnd, message, wParam, lParam);
   }
   return 0;
}

//
//  FUNCTION: PrintOutWndProc(HWND, unsigned, WORD, LONG)
//
//  PURPOSE:  Processes messages for the print-out window.
//
//  WM_COMMAND  - process the application menu
//  WM_PAINT    - Paint the main window
//  WM_DESTROY  - post a quit message and return
//
//
static LRESULT CALLBACK PrintOutWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message) 
    {
        case WM_PAINT: {
            RECT r;
            if (GetUpdateRect(hWnd, &r, FALSE)) {
                PAINTSTRUCT ps;
                HDC hdc = BeginPaint(hWnd, &ps);
                repaint_printout(hdc, 1, r.left, r.top, r.right - r.left, r.bottom - r.top, 0);
                EndPaint(hWnd, &ps);
                break;
            }
        }
        case WM_DESTROY:
            GetWindowPlacement(hPrintOutWnd, &state.printOutPlacement);
            state.printOutPlacementValid = 1;
            state.printOutOpen = 0;
            hPrintOutWnd = NULL;
            return 0;
        case WM_SIZING: {
            RECT windowRect, clientRect;
            GetWindowRect(hPrintOutWnd, &windowRect);
            GetClientRect(hPrintOutWnd, &clientRect);
            int vBorder = (windowRect.bottom - windowRect.top) - (clientRect.bottom - clientRect.top);

            LPRECT r = (LPRECT) lParam;
            switch (wParam) {
                case WMSZ_TOPLEFT:
                case WMSZ_LEFT:
                case WMSZ_BOTTOMLEFT:
                    r->left = r->right - printOutWidth;
                    break;
                case WMSZ_TOPRIGHT:
                case WMSZ_RIGHT:
                case WMSZ_BOTTOMRIGHT:
                    r->right = r->left + printOutWidth;
                    break;
            }
            switch (wParam) {
                case WMSZ_TOPLEFT:
                case WMSZ_TOP:
                case WMSZ_TOPRIGHT:
                    r->top += (r->bottom - r->top - vBorder) % 18;
                    break;
                case WMSZ_BOTTOMLEFT:
                case WMSZ_BOTTOM:
                case WMSZ_BOTTOMRIGHT:
                    r->bottom -= (r->bottom - r->top - vBorder) % 18;
                    break;
            }
            return 1;
        }
        case WM_SIZE:
            if (wParam == SIZE_MAXIMIZED || wParam == SIZE_RESTORED) {
                printOutHeight = HIWORD(lParam);
                printout_length_changed();
            }
            return 0;
        case WM_VSCROLL: {
            int scroll_code = LOWORD(wParam);
            SCROLLINFO si;
            si.cbSize = sizeof(SCROLLINFO);
            si.fMask = SIF_ALL;
            GetScrollInfo(hPrintOutWnd, SB_VERT, &si);
            int pos;
            int p = si.nPage;
            if (p > 0)
                p--;
            int maxpos = si.nMax - p;
            switch (scroll_code) {
                case SB_TOP: pos = si.nMin; break;
                case SB_BOTTOM: pos = si.nMax; break;
                case SB_LINEUP: pos = printout_pos - 18; break;
                case SB_LINEDOWN: pos = printout_pos + 18; break;
                case SB_PAGEUP: pos = printout_pos - printOutHeight + 18; break;
                case SB_PAGEDOWN: pos = printout_pos + printOutHeight - 18; break;
                case SB_THUMBPOSITION:
                case SB_THUMBTRACK: pos = HIWORD(wParam); break;
                default: pos = printout_pos;
            }
            if (pos < 0)
                pos = 0;
            else if (pos > maxpos)
                pos = maxpos;
            if (pos != printout_pos) {
                int oldpos = printout_pos;
                printout_pos = pos;
                SetScrollPos(hPrintOutWnd, SB_VERT, printout_pos, TRUE);
                printout_scrolled(oldpos - printout_pos);
            }
            return 0;
        }
        case WM_COMMAND: {
            int wmId    = LOWORD(wParam); 
            int wmEvent = HIWORD(wParam); 
            // This is a bit hacky, but I want the Ctrl-<Key>
            // shortcuts to work even when the Print-Out window
            // is on top.
            switch (wmId) {
                case IDM_PAPERADVANCE:
                    paper_advance();
                    break;
                case IDM_COPYPRINTASTEXT:
                    copy_print_as_text();
                    break;
                case IDM_COPYPRINTASIMAGE:
                    copy_print_as_image();
                    break;
                case IDM_CLEARPRINTOUT:
                    clear_printout();
                    break;
                case IDM_EXIT:
                    DestroyWindow(hMainWnd);
                    break;
                case ID_EDIT_COPY:
                    copy();
                    break;
                case ID_EDIT_PASTE:
                    paste();
                    break;
                case IDM_SHORTCUTS:
                    toggle_keyboard_shortcuts();
                    break;
            }
            return 0;
        }
        default:
            return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

// Message handler for about box.
static LRESULT CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
        case WM_INITDIALOG:
            return TRUE;

        case WM_COMMAND: {
            int id = LOWORD(wParam);
            if (id == IDOK || id == IDCANCEL) 
            {
                EndDialog(hDlg, id);
                return TRUE;
            }
            else if (id == IDC_WEBSITELINK || id == IDC_FORUMLINK || id == IDC_WEBSITELINK_PLUS42)
            {
                char buf[256];
                GetDlgItemText(hDlg, id, buf, 255);
                ShellExecute(NULL, "open", buf, NULL, NULL, SW_SHOWNORMAL);
            }
            break;
        }

        case WM_CTLCOLORSTATIC: {
            HWND item = (HWND) lParam;
            if (item == GetDlgItem(hDlg, IDC_WEBSITELINK)
                    || item == GetDlgItem(hDlg, IDC_FORUMLINK)
                    || item == GetDlgItem(hDlg, IDC_WEBSITELINK_PLUS42)) {
                SetTextColor((HDC) wParam, GetSysColor(COLOR_HOTLIGHT));
                SetBkMode((HDC) wParam, TRANSPARENT);
                return (INT_PTR) GetSysColorBrush(COLOR_MENU);
            } else
                return FALSE;
        }

        case WM_SETCURSOR: {
            HWND item = (HWND)wParam;
            if (item == GetDlgItem(hDlg, IDC_WEBSITELINK)
                || item == GetDlgItem(hDlg, IDC_FORUMLINK)
                || item == GetDlgItem(hDlg, IDC_WEBSITELINK_PLUS42)) {
                SetCursor(LoadCursor(NULL, IDC_HAND));
                SetWindowLongPtr(hDlg, DWLP_MSGRESULT, TRUE);
                return TRUE;
            } else
                return FALSE;
        }
    }
    return FALSE;
}

// Message handler for Export Program dialog.
static LRESULT CALLBACK ExportProgram(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message) {
        case WM_INITDIALOG: {
            HWND list = GetDlgItem(hDlg, IDC_LIST1);
            char *buf = core_list_programs();
            if (buf != NULL) {
                int count = ((buf[0] & 255) << 24) | ((buf[1] & 255) << 16) | ((buf[2] & 255) << 8) | (buf[3] & 255);
                char *p = buf + 4;
                for (int i = 0; i < count; i++) {
                    int len = (int) strlen(p) + 1;
                    int wlen = MultiByteToWideChar(CP_UTF8, 0, p, len, NULL, 0);
                    wchar_t *wbuf = (wchar_t *) malloc(wlen * 2);
                    if (wbuf == NULL) {
                        SendMessageW(list, LB_ADDSTRING, 0, (WPARAM) L"<Low Mem>");
                    } else {
                        MultiByteToWideChar(CP_UTF8, 0, p, len, wbuf, wlen);
                        SendMessageW(list, LB_ADDSTRING, 0, (WPARAM) wbuf);
                        free(wbuf);
                    }
                    p += len;
                }
                free(buf);
            }
            return TRUE;
        }
        case WM_COMMAND: {
            int cmd = LOWORD(wParam);
            switch (cmd) {
                case IDOK: {
                    HWND list = GetDlgItem(hDlg, IDC_LIST1);
                    sel_prog_count = (int) SendMessage(list, LB_GETSELCOUNT, 0, 0);
                    if (sel_prog_count > 0) {
                        sel_prog_list = (int *) malloc(sel_prog_count * sizeof(int));
                        // TODO - handle memory allocation failure
                        SendMessage(list, LB_GETSELITEMS, sel_prog_count, (LPARAM) sel_prog_list);
                    }
                    EndDialog(hDlg, 1);
                    return TRUE;
                }
                case IDCANCEL: {
                    EndDialog(hDlg, 0);
                    return FALSE;
                }
            }
            return FALSE;
        }
    }
    return FALSE;
}

// Message handler for preferences dialog.
static LRESULT CALLBACK Preferences(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
    // TODO: track focus changes so that we can force the IDC_PRINTER_GIF_HEIGHT
    // text field to contain a legal value whenever it loses focus.
    // Question: HOW do you track focus changes? I don't know what message
    // to look for.

    switch (message)
    {
        case WM_INITDIALOG: {
            // Initialize the dialog from the prefs structures
            HWND ctl;
            if (core_settings.matrix_singularmatrix) {
                ctl = GetDlgItem(hDlg, IDC_MATRIX_SINGULARMATRIX);
                SendMessage(ctl, BM_SETCHECK, 1, 0);
            }
            if (core_settings.matrix_outofrange) {
                ctl = GetDlgItem(hDlg, IDC_MATRIX_OUTOFRANGE);
                SendMessage(ctl, BM_SETCHECK, 1, 0);
            }
            if (core_settings.auto_repeat) {
                ctl = GetDlgItem(hDlg, IDC_AUTO_REPEAT);
                SendMessage(ctl, BM_SETCHECK, 1, 0);
            }
            if (core_settings.allow_big_stack) {
                ctl = GetDlgItem(hDlg, IDC_ALLOW_BIG_STACK);
                SendMessage(ctl, BM_SETCHECK, 1, 0);
            }
            if (core_settings.localized_copy_paste) {
                ctl = GetDlgItem(hDlg, IDC_LOCALIZED_COPY_PASTE);
                SendMessage(ctl, BM_SETCHECK, 1, 0);
            }
            if (state.alwaysOnTop) {
                ctl = GetDlgItem(hDlg, IDC_ALWAYSONTOP);
                SendMessage(ctl, BM_SETCHECK, 1, 0);
            }
            if (state.printerToTxtFile) {
                ctl = GetDlgItem(hDlg, IDC_PRINTER_TXT);
                SendMessage(ctl, BM_SETCHECK, 1, 0);
            }
            SetDlgItemTextW(hDlg, IDC_PRINTER_TXT_NAME, state.printerTxtFileName);
            if (state.printerToGifFile) {
                ctl = GetDlgItem(hDlg, IDC_PRINTER_GIF);
                SendMessage(ctl, BM_SETCHECK, 1, 0);
            }
            SetDlgItemTextW(hDlg, IDC_PRINTER_GIF_NAME, state.printerGifFileName);
            SetDlgItemInt(hDlg, IDC_PRINTER_GIF_HEIGHT, state.printerGifMaxLength, TRUE);
            return TRUE;
        }

        case WM_COMMAND: {
            int cmd = LOWORD(wParam);
            switch (cmd) {
                case IDOK: {
                    // Update the prefs structures from the dialog
                    HWND ctl = GetDlgItem(hDlg, IDC_MATRIX_SINGULARMATRIX);
                    core_settings.matrix_singularmatrix = SendMessage(ctl, BM_GETCHECK, 0, 0) != 0;
                    ctl = GetDlgItem(hDlg, IDC_MATRIX_OUTOFRANGE);
                    core_settings.matrix_outofrange = SendMessage(ctl, BM_GETCHECK, 0, 0) != 0;
                    ctl = GetDlgItem(hDlg, IDC_AUTO_REPEAT);
                    core_settings.auto_repeat = SendMessage(ctl, BM_GETCHECK, 0, 0) != 0;
                    ctl = GetDlgItem(hDlg, IDC_ALLOW_BIG_STACK);
                    bool oldAllowBigStack = core_settings.allow_big_stack;
                    core_settings.allow_big_stack = SendMessage(ctl, BM_GETCHECK, 0, 0) != 0;
                    if (oldAllowBigStack != core_settings.allow_big_stack)
                        core_update_allow_big_stack();
                    ctl = GetDlgItem(hDlg, IDC_LOCALIZED_COPY_PASTE);
                    core_settings.localized_copy_paste = SendMessage(ctl, BM_GETCHECK, 0, 0) != 0;
                    ctl = GetDlgItem(hDlg, IDC_ALWAYSONTOP);
                    BOOL alwaysOnTop = SendMessage(ctl, BM_GETCHECK, 0, 0) != 0;
                    if (alwaysOnTop != state.alwaysOnTop) {
                        state.alwaysOnTop = alwaysOnTop;
                        SetWindowPos(hMainWnd, alwaysOnTop ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
                        if (hPrintOutWnd != NULL)
                            SetWindowPos(hPrintOutWnd, alwaysOnTop ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
                    }

                    ctl = GetDlgItem(hDlg, IDC_PRINTER_TXT);
                    state.printerToTxtFile = SendMessage(ctl, BM_GETCHECK, 0, 0) != 0;
                    wchar_t buf[FILENAMELEN];
                    GetDlgItemTextW(hDlg, IDC_PRINTER_TXT_NAME, buf, FILENAMELEN - 1);
                    int len = (int) wcslen(buf);
                    if (len > 0 && (len < 4 || _wcsicmp(buf + len - 4, L".txt") != 0))
                        wcscat(buf, L".txt");
                    if (print_txt != NULL && (!state.printerToTxtFile
                            || _wcsicmp(state.printerTxtFileName, buf) != 0)) {
                        fclose(print_txt);
                        print_txt = NULL;
                    }
                    wcscpy(state.printerTxtFileName, buf);
                    ctl = GetDlgItem(hDlg, IDC_PRINTER_GIF);
                    state.printerToGifFile = SendMessage(ctl, BM_GETCHECK, 0, 0) != 0;
                    BOOL success;
                    int maxlen = (int) GetDlgItemInt(hDlg, IDC_PRINTER_GIF_HEIGHT, &success, TRUE);
                    state.printerGifMaxLength = !success ? 256 : maxlen < 16 ? 16 : maxlen > 32767 ? 32767 : maxlen;
                    GetDlgItemTextW(hDlg, IDC_PRINTER_GIF_NAME, buf, FILENAMELEN - 1);
                    len = (int) wcslen(buf);
                    if (len > 0 && (len < 4 || _wcsicmp(buf + len - 4, L".gif") != 0))
                        wcscat(buf, L".gif");
                    if (print_gif != NULL && (!state.printerToGifFile
                            || _wcsicmp(state.printerGifFileName, buf) != 0)) {
                        shell_finish_gif(gif_seeker, gif_writer);
                        fclose(print_gif);
                        print_gif = NULL;
                        gif_seq = -1;
                    }
                    wcscpy(state.printerGifFileName, buf);
                    // fall through
                }
                case IDCANCEL:
                    EndDialog(hDlg, LOWORD(wParam));
                    return TRUE;
                case IDC_PRINTER_TXT_BROWSE: {
                    wchar_t buf[FILENAMELEN];
                    GetDlgItemTextW(hDlg, IDC_PRINTER_TXT_NAME, buf, FILENAMELEN - 1);
                    if (browse_file_w(hDlg,
                                    L"Select Text File Name",
                                    1,
                                    L"Text Files (*.txt)\0*.txt\0All Files (*.*)\0*.*\0\0",
                                    L"txt",
                                    buf,
                                    FILENAMELEN))
                        SetDlgItemTextW(hDlg, IDC_PRINTER_TXT_NAME, buf);
                    return TRUE;
                }
                case IDC_PRINTER_GIF_BROWSE: {
                    wchar_t buf[FILENAMELEN];
                    GetDlgItemTextW(hDlg, IDC_PRINTER_GIF_NAME, buf, FILENAMELEN - 1);
                    if (browse_file_w(hDlg,
                                    L"Select GIF File Name",
                                    1,
                                    L"GIF Files (*.gif)\0*.gif\0All Files (*.*)\0*.*\0\0",
                                    L"gif",
                                    buf,
                                    FILENAMELEN))
                        SetDlgItemTextW(hDlg, IDC_PRINTER_GIF_NAME, buf);
                    return TRUE;
                }
            }
            break;
        }
    }
    return FALSE;
}

int browse_file(HWND owner, wchar_t *title, int save, wchar_t *filter, wchar_t *defExt, char *buf, int buflen) {
    wchar_t wbuf[FILENAMELEN];
    int wlen = MultiByteToWideChar(CP_UTF8, 0, buf, (int) strlen(buf), wbuf, FILENAMELEN - 1);
    wbuf[wlen] = 0;

    OPENFILENAMEW ofn;
    ofn.lStructSize = sizeof(OPENFILENAMEW);
    ofn.hwndOwner = owner;
    ofn.lpstrFilter = filter;
    ofn.lpstrCustomFilter = NULL;
    ofn.nFilterIndex = 1;
    ofn.lpstrFile = wbuf;
    ofn.nMaxFile = FILENAMELEN - 1;
    ofn.lpstrFileTitle = NULL;
    ofn.lpstrInitialDir = NULL;
    ofn.lpstrTitle = title;
    ofn.Flags = OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT;
    ofn.lpstrDefExt = defExt;

    int ret = save ? GetSaveFileNameW(&ofn) : GetOpenFileNameW(&ofn);

    int len = WideCharToMultiByte(CP_UTF8, 0, wbuf, (int) wcslen(wbuf), buf, buflen - 1, NULL, NULL);
    buf[len] = 0;
    return ret;
}

int browse_file_w(HWND owner, wchar_t *title, int save, wchar_t *filter, wchar_t *defExt, wchar_t *buf, int buflen) {
    OPENFILENAMEW ofn;
    ofn.lStructSize = sizeof(OPENFILENAMEW);
    ofn.hwndOwner = owner;
    ofn.lpstrFilter = filter;
    ofn.lpstrCustomFilter = NULL;
    ofn.nFilterIndex = 1;
    ofn.lpstrFile = buf;
    ofn.nMaxFile = FILENAMELEN - 1;
    ofn.lpstrFileTitle = NULL;
    ofn.lpstrInitialDir = NULL;
    ofn.lpstrTitle = title;
    ofn.Flags = OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT;
    ofn.lpstrDefExt = defExt;

    return save ? GetSaveFileNameW(&ofn) : GetOpenFileNameW(&ofn);
}

static void move_state_file(wchar_t *olddir, wchar_t *newdir, wchar_t *filename) {
    wchar_t oldfile[FILENAMELEN];
    wchar_t newfile[FILENAMELEN];
    char buf[1024];
    FILE *in, *out;
    size_t n;

    wcscpy(oldfile, olddir);
    wcscat(oldfile, L"\\");
    wcscat(oldfile, filename);
    wcscpy(newfile, newdir);
    wcscat(newfile, L"\\");
    wcscat(newfile, filename);

    in = _wfopen(oldfile, L"rb");
    if (in == NULL)
        return;
    CreateDirectoryW(newdir, NULL);
    out = _wfopen(newfile, L"wb");
    if (out == NULL) {
        fclose(in);
        return;
    }

    while ((n = fread(buf, 1, 1024, in)) > 0)
        fwrite(buf, 1, n, out);

    fclose(in);
    fclose(out);
    _wremove(oldfile);
}

static void get_home_dir(wchar_t *path, int pathlen) {
    // Starting with release 1.5.1, changing the Free42 directory is no longer
    // supported. Instead, Free42 looks for a file or directory named 'portable'
    // in the executable's directory; if it exists, this is where the state files
    // will also be stored, and this will be the only directory searched for skins.
    // If there is no 'portable', then the state files will be stored under
    // %APPDATA%\Free42, and skins will be searched for in that directory as well,
    // *and* in the executable's directory.

    wchar_t exepath[FILENAMELEN];
    GetModuleFileNameW(0, exepath, FILENAMELEN);
    wchar_t *lastbackslash = wcsrchr(exepath, L'\\');
    if (lastbackslash != 0) {
        lastbackslash[1] = L'*';
        lastbackslash[2] = 0;
    }

    WIN32_FIND_DATAW wfd;
    HANDLE search = FindFirstFileW(exepath, &wfd);
    bool use_exedir = false;
    if (search != INVALID_HANDLE_VALUE) {
        do {
            if (_wcsicmp(wfd.cFileName, L"portable") == 0
                    || _wcsicmp(wfd.cFileName, L"portable.") == 0) {
                use_exedir = true;
                break;
            }
        } while (FindNextFileW(search, &wfd));
        FindClose(search);
    }

    if (use_exedir) {
        *lastbackslash = 0;
        wcsncpy(path, exepath, pathlen);
        path[pathlen - 1] = 0;
        return;
    }

    LPITEMIDLIST idlist;
    wchar_t newpath[MAX_PATH];
    if (SHGetSpecialFolderLocation(NULL, CSIDL_APPDATA, &idlist) == NOERROR) {
        if (!SHGetPathFromIDListW(idlist, newpath))
            wcscpy(newpath, L"C:");
        wcsncat(newpath, L"\\Free42", MAX_PATH - 1);
        newpath[MAX_PATH - 1] = 0;
        LPMALLOC imalloc;
        if (SHGetMalloc(&imalloc) == NOERROR)
            imalloc->Free(idlist);
        wcsncpy(path, newpath, pathlen);
        path[pathlen - 1] = 0;
    } else {
        wcsncpy(path, L"C:\\Free42", pathlen);
        path[pathlen - 1] = 0;
    }
}

static void copy() {
    if (!OpenClipboard(hMainWnd))
        return;
    char *buf = core_copy();
    if (buf == NULL)
        goto fail1;
    int len = (int) strlen(buf);
    if (len == 0)
        goto fail2;
    int wlen = MultiByteToWideChar(CP_UTF8, 0, buf, len + 1, NULL, 0);
    if (wlen == 0)
        goto fail2;
    HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, wlen * 2);
    if (h != NULL) {
        wchar_t *wbuf = (wchar_t *) GlobalLock(h);
        MultiByteToWideChar(CP_UTF8, 0, buf, len, wbuf, wlen);
        GlobalUnlock(h);
        EmptyClipboard();
        if (SetClipboardData(CF_UNICODETEXT, h) == NULL)
            GlobalFree(h);
    }
    fail2:
    free(buf);
    fail1:
    CloseClipboard();
}

static void paste() {
    if (!OpenClipboard(hMainWnd))
        return;
    HANDLE h = GetClipboardData(CF_UNICODETEXT);
    if (h != NULL) {
        wchar_t *wbuf = (wchar_t *) GlobalLock(h);
        if (wbuf != NULL) {
            int wlen = (int) GlobalSize(h) / 2;
            int len = WideCharToMultiByte(CP_UTF8, 0, wbuf, wlen, NULL, 0, NULL, NULL);
            if (len != 0) {
                char *buf = (char *) malloc(len + 1);
                if (buf != NULL) {
                    WideCharToMultiByte(CP_UTF8, 0, wbuf, wlen, buf, len, NULL, NULL);
                    buf[len] = 0;
                    core_paste(buf);
                    free(buf);
                }
            }
            GlobalUnlock(h);
        }
    }
    CloseClipboard();
}

static void Quit() {
    FILE *printfile;
    size_t n, length;
    
    printfile = _wfopen(printfilename, L"wb");
    if (printfile != NULL) {
        // Write bitmap
        length = printout_bottom - printout_top;
        if (length < 0)
            length += PRINT_LINES;
        n = fwrite(&length, 1, sizeof(int), printfile);
        if (n != sizeof(int))
            goto failed;
        if (printout_bottom >= printout_top) {
            n = fwrite(printout + PRINT_BYTESPERLINE * printout_top,
                       1, PRINT_BYTESPERLINE * length, printfile);
            if (n != PRINT_BYTESPERLINE * length)
                goto failed;
        } else {
            n = fwrite(printout + PRINT_BYTESPERLINE * printout_top,
                       1, PRINT_SIZE - PRINT_BYTESPERLINE * printout_top,
                       printfile);
            if (n != PRINT_SIZE - PRINT_BYTESPERLINE * printout_top)
                goto failed;
            n = fwrite(printout, 1,
                       PRINT_BYTESPERLINE * printout_bottom, printfile);
            if (n != PRINT_BYTESPERLINE * printout_bottom)
                goto failed;
        }
        // Write text
        length = print_text_bottom - print_text_top;
        if (length < 0)
            length += PRINT_TEXT_SIZE;
        n = fwrite(&length, 1, sizeof(int), printfile);
        if (n != sizeof(int))
            goto failed;
        n = fwrite(&print_text_pixel_height, 1, sizeof(int), printfile);
        if (n != sizeof(int))
            goto failed;
        if (print_text_bottom >= print_text_top) {
            n = fwrite(print_text + print_text_top, 1, length, printfile);
            if (n != length)
                goto failed;
        } else {
            n = fwrite(print_text + print_text_top, 1, PRINT_TEXT_SIZE - print_text_top, printfile);
            if (n != PRINT_TEXT_SIZE - print_text_top)
                goto failed;
            n = fwrite(print_text, 1, print_text_bottom, printfile);
            if (n != print_text_bottom)
                goto failed;
        }
        
        fclose(printfile);
        goto done;
        
    failed:
        fclose(printfile);
        _wremove(printfilename);
        
    done:
        ;
    }

    statefile = _wfopen(statefilename, L"wb");
    if (statefile != NULL) {
        if (!placement_saved) {
            GetWindowPlacement(hMainWnd, &state.mainPlacement);
            state.mainPlacementValid = 1;
            if (state.printOutOpen) {
                GetWindowPlacement(hPrintOutWnd, &state.printOutPlacement);
                state.printOutPlacementValid = 1;
            }
        }
        skin_get_window_size(&state.mainWindowWidth, &state.mainWindowHeight);
        write_shell_state();
        fclose(statefile);
    }
    wchar_t corefilename[FILENAMELEN];
    swprintf(corefilename, L"%ls/%ls.f42", free42dirname, state.coreName);
    char *cfn = wide2utf(corefilename);
    core_save_state(cfn);
    free(cfn);
    core_cleanup();

    if (print_txt != NULL)
        fclose(print_txt);
    
    if (print_gif != NULL) {
        shell_finish_gif(gif_seeker, gif_writer);
        fclose(print_gif);
    }

    shell_spool_exit();
}

static VOID CALLBACK repeater(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime) {
    KillTimer(NULL, timer);
    int repeat = core_repeat();
    if (repeat != 0)
        timer = SetTimer(NULL, 0, repeat == 1 ? 200 : 100, repeater);
    else
        timer = SetTimer(NULL, 0, 250, timeout1);
}

static VOID CALLBACK timeout1(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime) {
    KillTimer(NULL, timer);
    if (ckey != 0) {
        core_keytimeout1();
        timer = SetTimer(NULL, 0, 1750, timeout2);
    } else
        timer = 0;
}

static VOID CALLBACK timeout2(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime) {
    KillTimer(NULL, timer);
    if (ckey != 0)
        core_keytimeout2();
    timer = 0;
}

static VOID CALLBACK timeout3(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime) {
    KillTimer(NULL, timer3);
    bool keep_running = core_timeout3(true);
    timer3 = 0;
    if (keep_running) {
        running = true;
        // Post dummy message to get the message loop moving again
        PostMessage(hMainWnd, WM_USER, 0, 0);
    }
}

static VOID CALLBACK battery_checker(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime) {
    shell_low_battery();
}

static void show_printout() {
    if (hPrintOutWnd != NULL) {
        if (IsIconic(hPrintOutWnd))
            OpenIcon(hPrintOutWnd);
        BringWindowToTop(hPrintOutWnd);
        return;
    }

    RECT r;
    SetRect(&r, 0, 0, 358, 600);
    printOutHeight = r.bottom - r.top;
    AdjustWindowRect(&r, WS_CAPTION|WS_SYSMENU|WS_MINIMIZEBOX
                            /*|WS_MAXIMIZEBOX*/|WS_SIZEBOX|WS_OVERLAPPED, 0);

    // TODO: It would be nice if zooming the print-out window would stretch
    // it to the full height of the screen, while leaving its width set to
    // 'printOutWidth'. To do this, I think I'll have to provide my own
    // implementation of the maximize function (maximize box & the maximize
    // item in the window menu), but I don't know how to hook that in. It
    // must be possible, because Snood does it, but I have no idea how.
    // Anyway, since "standard" maximizing (full screen) is not appropriate for
    // the printout window, I just disable the maximize box for now.

    int sbwidth = GetSystemMetrics(SM_CXVSCROLL);

    printOutWidth = r.right - r.left + sbwidth;
    hPrintOutWnd = CreateWindow(szPrintOutWindowClass, szPrintOutTitle,
                            WS_CAPTION|WS_SYSMENU|WS_VSCROLL|WS_MINIMIZEBOX
                                /*|WS_MAXIMIZEBOX*/|WS_SIZEBOX|WS_OVERLAPPED,
                            CW_USEDEFAULT, 0, printOutWidth, printOutHeight,
                            NULL, NULL, hInst, NULL);

    if (state.printOutPlacementValid) {
        // Fix the width, in case the saved settings are not appropriate
        // for the current desktop properties (the decor dimensions may
        // be different).
        RECT *r2 = &state.printOutPlacement.rcNormalPosition;
        r2->right = r2->left + printOutWidth;
        SetWindowPlacement(hPrintOutWnd, &state.printOutPlacement);
    }
    if (state.alwaysOnTop)
        SetWindowPos(hPrintOutWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);

    printout_length_changed();

    printout_scroll_to_bottom(0);

    ShowWindow(hPrintOutWnd, SW_SHOW);
    UpdateWindow(hPrintOutWnd);
    state.printOutOpen = 1;
}

static void export_program() {
    if (!DialogBoxW(hInst, (LPCWSTR)IDD_SELECTPROGRAM, hMainWnd, (DLGPROC)ExportProgram))
        return;
    if (sel_prog_count == 0)
        return;
    /* The sel_prog_count global now has the number of selected items;
     * sel_prog_list is an array of integers containing the item numbers.
     */
    char export_file_name[FILENAMELEN];
    export_file_name[0] = 0;
    char *buf = core_list_programs();
    if (buf != NULL) {
        int count = ((buf[0] & 255) << 24) | ((buf[1] & 255) << 16) | ((buf[2] & 255) << 8) | (buf[3] & 255);
        char *p = buf + 4;
        int sel = sel_prog_list[0];
        while (sel > 0) {
            p += strlen(p) + 1;
            sel--;
        }
        if (p[0] == '"') {
            char *closing_quote = strchr(p + 1, '"');
            if (closing_quote != NULL) {
                *closing_quote = 0;
                int len = (int) strlen(p + 1);
                for (int i = 0; i < len; i++) {
                    char c = p[i + 1];
                    if (strchr("<>:\"/\\|?*\n", c) != NULL)
                        p[i + 1] = '_';
                }
                strcpy(export_file_name, p + 1);
            }
        }
        free(buf);
    }
    if (export_file_name[0] == 0)
        strcpy(export_file_name, "Untitled");

    if (browse_file(hMainWnd,
                     L"Export Programs",
                     1,
                     L"Program Files (*.raw)\0*.raw\0All Files (*.*)\0*.*\0\0",
                     L"raw",
                     export_file_name,
                     FILENAMELEN))
        core_export_programs(sel_prog_count, sel_prog_list, export_file_name);

    free(sel_prog_list);
}

static void import_program() {
    char buf[FILENAMELEN];
    buf[0] = 0;
    if (!browse_file(hMainWnd,
                     L"Import Programs",
                     0,
                     L"Program Files (*.raw)\0*.raw\0All Files (*.*)\0*.*\0\0",
                     NULL,
                     buf,
                     FILENAMELEN))
        return;

    core_import_programs(0, buf);
    redisplay();
}

static void paper_advance() {
    static const char *bits = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
    shell_print("", 0, bits, 18, 0, 0, 143, 9);
}

static char *tb;
static int tblen, tbcap;

static void tbwriter(const char *text, int length) {
    if (tblen + length > tbcap) {
        tbcap += length + 8192;
        tb = (char *) realloc(tb, tbcap);
    }
    if (tb != NULL) {
        memcpy(tb + tblen, text, length);
        tblen += length;
    }
}

static void tbnewliner() {
    tbwriter("\n", 1);
}

static void tbnonewliner() {
    // No-op
}

static void copy_print_as_text() {
    if (!OpenClipboard(hMainWnd))
        return;

    tb = NULL;
    tblen = tbcap = 0;

    int len = print_text_bottom - print_text_top;
    if (len < 0)
        len += PRINT_TEXT_SIZE;
    // Calculate effective top, since printout_top can point
    // at a truncated line, and we want to skip those when
    // copying
    int top = printout_bottom - 2 * print_text_pixel_height;
    if (top < 0)
        top += PRINT_LINES;
    int p = print_text_top;
    int pixel_v = 0;
    while (len > 0) {
        int z = print_text[p++] & 255;
        if (z == 255) {
            char buf[34];
            for (int v = 0; v < 16; v += 2) {
                for (int vv = 0; vv < 2; vv++) {
                    int V = top + (pixel_v + v + vv) * 2;
                    if (V >= PRINT_LINES)
                        V -= PRINT_LINES;
                    for (int h = 0; h < 17; h++) {
                        unsigned char a = printout[V * PRINT_BYTESPERLINE + 2 * h];
                        unsigned char b = printout[V * PRINT_BYTESPERLINE + 2 * h + 1];
                        buf[vv * 17 + h] = (((b & 2) << 6) | ((b & 8) << 3) | (b & 32) | ((b & 128) >> 3) | ((a & 2) << 2) | ((a & 8) >> 1) | ((a & 32) >> 4) | ((a & 128) >> 7)) ^ 255;
                    }
                }
                shell_spool_bitmap_to_txt(buf, 17, 0, 0, 131, 2, tbwriter, tbnewliner);
            }
            pixel_v += 16;
        } else {
            if (p + z < PRINT_TEXT_SIZE) {
                shell_spool_txt((const char *) (print_text + p), z, tbwriter, tbnewliner);
                p += z;
            } else {
                int d = PRINT_TEXT_SIZE - p;
                shell_spool_txt((const char *) (print_text + p), d, tbwriter, tbnonewliner);
                shell_spool_txt((const char *) print_text, z - d, tbwriter, tbnewliner);
                p = z - d;
            }
            len -= z;
            pixel_v += 9;
        }
        len--;
    }
    tbwriter("\0", 1);

    len = (int) strlen(tb);
    if (len == 0)
        goto fail;
    int wlen = MultiByteToWideChar(CP_UTF8, 0, tb, len + 1, NULL, 0);
    if (wlen == 0)
        goto fail;
    HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, wlen * 2);
    if (h != NULL) {
        wchar_t *wbuf = (wchar_t *) GlobalLock(h);
        MultiByteToWideChar(CP_UTF8, 0, tb, len, wbuf, wlen);
        GlobalUnlock(h);
        EmptyClipboard();
        if (SetClipboardData(CF_UNICODETEXT, h) == NULL)
            GlobalFree(h);
    }
    fail:
    free(tb);
    CloseClipboard();
}

static void copy_print_as_image() {
    if (!OpenClipboard(hMainWnd))
        return;

    int length = printout_bottom - printout_top;
    if (length < 0)
        length += PRINT_LINES;
    bool empty = length == 0;
    if (empty)
        length = 2;
    int bmsize = sizeof(BITMAPINFOHEADER) + 8 + 48 * length;

    HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, bmsize);
    if (h != NULL) {
        char *bmbuf = (char *) GlobalLock(h);
        BITMAPINFOHEADER *bi = (BITMAPINFOHEADER *) bmbuf;

        memset(bi, 0, sizeof(BITMAPINFOHEADER));
        bi->biSize = sizeof(BITMAPINFOHEADER);
        bi->biWidth = 358;
        bi->biHeight = length;
        bi->biPlanes = 1;
        bi->biBitCount = 1;
        bi->biCompression = BI_RGB;

        bmbuf += sizeof(BITMAPINFOHEADER);
        // Color table
        *bmbuf++ = 0;
        *bmbuf++ = 0;
        *bmbuf++ = 0;
        *bmbuf++ = 0;
        *bmbuf++ = (char) 255;
        *bmbuf++ = (char) 255;
        *bmbuf++ = (char) 255;
        *bmbuf++ = 0;

        if (empty) {
            memset(bmbuf, 255, 96);
        } else {
            for (int v = length - 1; v >= 0; v--) {
                int vv = printout_top + v;
                if (vv >= PRINT_LINES)
                    vv -= PRINT_LINES;
                char *src = printout + vv * PRINT_BYTESPERLINE;
                for (int i = 0; i < 4; i++)
                    *bmbuf++ = (char) 255;
                char pc = (char) 255;
                for (int h = 0; h <= 36; h++) {
                    char c = h == 36 ? 255 : src[h];
                    *bmbuf++ = ((c & 240) >> 4) | ((pc & 15) << 4);
                    pc = c;
                }
                for (int i = 0; i < 7; i++)
                    *bmbuf++ = (char) 255;
            }
        }

        GlobalUnlock(h);
        EmptyClipboard();
        if (SetClipboardData(CF_DIB, h) == NULL)
            GlobalFree(h);
    }
    CloseClipboard();
}

static void clear_printout() {
    printout_top = 0;
    printout_bottom = 0;
    printout_pos = 0;
    print_text_top = 0;
    print_text_bottom = 0;
    print_text_pixel_height = 0;
    printout_length_changed();
    if (hPrintOutWnd != NULL)
        InvalidateRect(hPrintOutWnd, NULL, FALSE);

    if (print_gif != NULL) {
        shell_finish_gif(gif_seeker, gif_writer);
        fclose(print_gif);
        print_gif = NULL;
    }
}

static void repaint_printout(int x, int y, int width, int height, int validate) {
    HDC hdc = GetDC(hPrintOutWnd);
    repaint_printout(hdc, 0, x, y, width, height, validate);
    ReleaseDC(hPrintOutWnd, hdc);
}

static void repaint_printout(HDC hdc, int destpos, int x, int y,
                             int width, int height, int validate) {
    HDC memdc;
    HBITMAP bitmap;
    int xdest, ydest;

    if (destpos) {
        xdest = x;
        ydest = y;
        y = ydest + printout_pos;
    } else {
        xdest = x;
        ydest = y - printout_pos;
    }

    int printout_length = printout_bottom - printout_top;
    if (printout_length < 0)
        printout_length += PRINT_LINES;
    if (y + height > printout_length) {
        RECT r;
        SetRect(&r, xdest, printout_length - printout_pos,
                    xdest + width, ydest + height);
        HBRUSH brush = (HBRUSH) GetStockObject(GRAY_BRUSH);
        SelectObject(hdc, brush);
        FillRect(hdc, &r, brush);
        if (y >= printout_length)
            return;
        height = printout_length - y;
    }

    memdc = CreateCompatibleDC(hdc);
    bitmap = CreateBitmap(286, PRINT_LINES, 1, 1, printout);
    SelectObject(memdc, bitmap);

    if (printout_bottom >= printout_top)
        /* The buffer is not wrapped */
        BitBlt(hdc, xdest, ydest, width, height,
                memdc, x - 36, printout_top + y, SRCCOPY);
    else {
        /* The buffer is wrapped */
        if (printout_top + y < PRINT_LINES) {
            if (printout_top + y + height <= PRINT_LINES)
                /* The rectangle is in the lower part of the buffer */
                BitBlt(hdc, xdest, ydest, width, height,
                        memdc, x - 36, printout_top + y, SRCCOPY);
            else {
                /* The rectangle spans both parts of the buffer */
                int part1_height = PRINT_LINES - printout_top - y;
                int part2_height = height - part1_height;
                BitBlt(hdc, xdest, ydest, width, part1_height,
                        memdc, x - 36, printout_top + y, SRCCOPY);
                BitBlt(hdc, xdest, ydest + part1_height, width, part2_height,
                        memdc, x - 36, 0, SRCCOPY);
            }
        } else
            /* The rectangle is in the upper part of the buffer */
            BitBlt(hdc, xdest, ydest, width, height,
                    memdc, x - 36, y + printout_top - PRINT_LINES, SRCCOPY);
    }

    DeleteDC(memdc);
    DeleteObject(bitmap);

    HBRUSH brush = (HBRUSH) GetStockObject(WHITE_BRUSH);
    SelectObject(hdc, brush);
    RECT r;
    SetRect(&r, 0, ydest,
                36, ydest + height);
    FillRect(hdc, &r, brush);
    SetRect(&r, 322, ydest,
                358, ydest + height);
    FillRect(hdc, &r, brush);

    if (validate) {
        RECT r;
        SetRect(&r, xdest, ydest, xdest + width, ydest + height);
        ValidateRect(hPrintOutWnd, &r);
    }
}

static void printout_scrolled(int offset) {
    RECT client;
    GetClientRect(hPrintOutWnd, &client);
    ScrollWindowEx(hPrintOutWnd, 0, offset, &client, &client, NULL, NULL, SW_INVALIDATE);
}

static void printout_scroll_to_bottom(int offset) {
    SCROLLINFO si;
    si.cbSize = sizeof(SCROLLINFO);
    si.fMask = SIF_POS | SIF_RANGE | SIF_PAGE;
    GetScrollInfo(hPrintOutWnd, SB_VERT, &si);
    si.fMask = SIF_POS;
    int p = si.nPage;
    int oldpos = si.nPos;
    if (p > 0)
        p--;
    si.nPos = si.nMax - p;
    printout_pos = si.nPos;
    SetScrollInfo(hPrintOutWnd, SB_VERT, &si, TRUE);
    printout_scrolled(oldpos - printout_pos - offset);
}

static void printout_length_changed() {
    SCROLLINFO si;
    si.cbSize = sizeof(SCROLLINFO);
    si.fMask = SIF_POS;
    GetScrollInfo(hPrintOutWnd, SB_VERT, &si);
    
    int printout_length = printout_bottom - printout_top;
    if (printout_length < 0)
        printout_length += PRINT_LINES;

    si.fMask = SIF_ALL | SIF_DISABLENOSCROLL;
    si.nMin = 0;
    si.nMax = printout_length > 0 ? printout_length - 1 : 0;
    si.nPage = printout_length < printOutHeight ? printout_length : printOutHeight;
    
    int p = si.nPage;
    if (p > 0)
        p--;
    int maxpos = si.nMax - p;
    if (si.nPos > maxpos)
        si.nPos = maxpos;

    printout_pos = si.nPos;
    SetScrollInfo(hPrintOutWnd, SB_VERT, &si, TRUE);
}

void shell_blitter(const char *bits, int bytesperline, int x, int y,
                   int width, int height) {
    skin_display_blitter(bits, bytesperline, x, y, width, height);
}

const char *shell_platform() {
    static char p[16];
    strcpy(p, FREE42_VERSION_2);
    strcat(p, " Windows");
    return p;
}

void shell_beeper(int tone) {
    const int sound_ids[] = { IDR_TONE0_WAVE, IDR_TONE1_WAVE, IDR_TONE2_WAVE,
         IDR_TONE3_WAVE, IDR_TONE4_WAVE, IDR_TONE5_WAVE, IDR_TONE6_WAVE,
         IDR_TONE7_WAVE, IDR_TONE8_WAVE, IDR_TONE9_WAVE, IDR_SQUEAK_WAVE };
    PlaySound(MAKEINTRESOURCE(sound_ids[tone]),
        GetModuleHandle(NULL),
        SND_RESOURCE);
}

static VOID CALLBACK ann_print_timeout(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime) {
    KillTimer(NULL, ann_print_timer);
    ann_print_timer = 0;
    ann_print = 0;
    skin_invalidate_annunciator(3);
}

/* shell_annunciators()
 * Callback invoked by the emulator core to change the state of the display
 * annunciators (up/down, shift, print, run, battery, (g)rad).
 * Every parameter can have values 0 (turn off), 1 (turn on), or -1 (leave
 * unchanged).
 * The battery annunciator is missing from the list; this is the only one of
 * the lot that the emulator core does not actually have any control over, and
 * so the shell is expected to handle that one by itself.
 */
void shell_annunciators(int updn, int shf, int prt, int run, int g, int rad) {
    if (updn != -1 && ann_updown != updn) {
        ann_updown = updn;
        skin_invalidate_annunciator(1);
    }
    if (shf != -1 && ann_shift != shf) {
        ann_shift = shf;
        skin_invalidate_annunciator(2);
    }
    if (prt != -1) {
        if (ann_print_timer != 0) {
            KillTimer(NULL, ann_print_timer);
            ann_print_timer = 0;
        }
        if (ann_print != prt)
            if (prt) {
                ann_print = 1;
                skin_invalidate_annunciator(3);
            } else {
                ann_print_timer = SetTimer(NULL, 0, 1000, ann_print_timeout);
            }
    }
    if (run != -1 && ann_run != run) {
        ann_run = run;
        skin_invalidate_annunciator(4);
    }
    if (g != -1 && ann_g != g) {
        ann_g = g;
        skin_invalidate_annunciator(6);
    }
    if (rad != -1 && ann_rad != rad) {
        ann_rad = rad;
        skin_invalidate_annunciator(7);
    }
}

bool shell_wants_cpu() {
    static DWORD lastCount = 0;
    DWORD count = GetTickCount();
    if (count - lastCount < 10)
        return false;
    lastCount = count;
    MSG msg;
    return PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE) != 0;
}

void shell_delay(int duration) {
    Sleep(duration);
}

/* Callback to ask the shell to call core_timeout3() after the given number of
 * milliseconds. If there are keystroke events during that time, the timeout is
 * cancelled. (Pressing 'shift' does not cancel the timeout.)
 * This function supports the delay after SHOW, MEM, and shift-VARMENU.
 */
void shell_request_timeout3(int delay) {
    if (timer3 != 0)
        KillTimer(NULL, timer3);
    timer3 = SetTimer(NULL, 0, delay, timeout3);
}

uint8 shell_get_mem() {
    MEMORYSTATUS memstat;
    GlobalMemoryStatus(&memstat);
    return memstat.dwAvailPhys;
}

bool shell_low_battery() {
    SYSTEM_POWER_STATUS powerstat;
    int lowbat;
    if (!GetSystemPowerStatus(&powerstat))
        lowbat = 0; // getting power status failed; assume we're fine
    else
        lowbat = powerstat.ACLineStatus == 0 // offline
                && (powerstat.BatteryFlag & 6) != 0; // low or critical
    if (ann_battery != lowbat) {
        ann_battery = lowbat;
        skin_invalidate_annunciator(5);
    }
    return lowbat != 0;
}

void shell_powerdown() {
    PostQuitMessage(0);
}

void shell_message(const char *message) {
    wchar_t *m = utf2wide(message);
    MessageBoxW(hMainWnd, m, L"Core", MB_ICONWARNING);
    free(m);
}

int8 shell_random_seed() {
    FILETIME ft;
    GetSystemTimeAsFileTime(&ft);
    return ((((int8) ft.dwHighDateTime) << 32) | (uint4) ft.dwLowDateTime) / 10000;
}

uint4 shell_milliseconds() {
    return GetTickCount();
}

const char *shell_number_format() {
    wchar_t dec[4];
    GetLocaleInfoW(LOCALE_USER_DEFAULT, LOCALE_SDECIMAL, dec, 4);
    wchar_t sep[4];
    GetLocaleInfoW(LOCALE_USER_DEFAULT, LOCALE_STHOUSAND, sep, 4);
    char grp[10];
    GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_SGROUPING, grp, 10);
    int g1, g2;
    int n = sscanf(grp, "%d;%d", &g1, &g2);
    wchar_t r[5];
    static char *ret = NULL;
    free(ret);
    if (n == 0) {
        r[0] = dec[0];
        r[1] = 0;
    } else {
        r[0] = dec[0];
        r[1] = sep[0];
        r[2] = L'0' + g1;
        if (n == 1 || g2 == 0)
            g2 = g1;
        r[3] = L'0' + g2;
        r[4] = 0;
    }
    ret = wide2utf(r);
    return ret;
}

int shell_date_format() {
    char fmt[80];
    GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_SSHORTDATE, fmt, 80);
    int y = (int) (strchr(fmt, 'y') - fmt);
    int m = (int) (strchr(fmt, 'M') - fmt);
    int d = (int) (strchr(fmt, 'd') - fmt);
    if (d < m && m < y)
        return 1;
    else if (y < m && m < d)
        return 2;
    else
        return 0;
}

bool shell_clk24() {
    char fmt[80];
    GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_STIMEFORMAT, fmt, 80);
    char *t = strchr(fmt, 't');
    char *s = strchr(fmt, ';');
    return t == NULL || s != NULL && s < t;
}

void shell_get_time_date(uint4 *time, uint4 *date, int *weekday) {
    SYSTEMTIME st;
    GetLocalTime(&st);
    if (time != NULL)
        *time = st.wHour * 1000000 + st.wMinute * 10000 + st.wSecond * 100 + st.wMilliseconds / 10;
    if (date != NULL)
        *date = st.wYear * 10000 + st.wMonth * 100 + st.wDay;
    if (weekday != NULL)
        *weekday = st.wDayOfWeek;
}

void shell_print(const char *text, int length,
         const char *bits, int bytesperline,
         int x, int y, int width, int height) {
    int xx, yy;
    int oldlength, newlength;

    for (yy = 0; yy < height; yy++) {
        int4 Y = (printout_bottom + 2 * yy) % PRINT_LINES;
        for (xx = 0; xx < 144; xx++) {
            int bit, px, py;
            if (xx < width) {
                char c = bits[(y + yy) * bytesperline + ((x + xx) >> 3)];
                bit = (c & (1 << ((x + xx) & 7))) != 0;
            } else
                bit = 0;
            for (px = xx * 2; px < (xx + 1) * 2; px++)
                for (py = Y; py < Y + 2; py++)
                    if (!bit)
                        printout[py * PRINT_BYTESPERLINE + (px >> 3)]
                            |= 128 >> (px & 7);
                    else
                        printout[py * PRINT_BYTESPERLINE + (px >> 3)]
                            &= ~(128 >> (px & 7));
        }
    }

    oldlength = printout_bottom - printout_top;
    if (oldlength < 0)
        oldlength += PRINT_LINES;
    printout_bottom = (printout_bottom + 2 * height) % PRINT_LINES;
    newlength = oldlength + 2 * height;

    if (newlength >= PRINT_LINES) {
        printout_top = (printout_bottom + 2) % PRINT_LINES;
        newlength = PRINT_LINES - 2;
        if (hPrintOutWnd != NULL) {
            if (newlength != oldlength)
                printout_length_changed();
            printout_scroll_to_bottom(2 * height + oldlength - newlength);
            repaint_printout(0, newlength - 2 * height, 358, 2 * height, 1);
        }
    } else {
        if (hPrintOutWnd != NULL) {
            printout_length_changed();
            printout_scroll_to_bottom(0);
            repaint_printout(0, oldlength, 358, 2 * height, 1);
        }
    }

    if (state.printerToTxtFile) {
        int err;
        wchar_t buf[1000];

        if (print_txt == NULL) {
            print_txt = _wfopen(state.printerTxtFileName, L"ab");
            if (print_txt == NULL) {
                err = errno;
                state.printerToTxtFile = 0;
                swprintf(buf, L"Can't open \"%ls\" for output: %hs (%d)\nPrinting to TXT file disabled.", state.printerTxtFileName, strerror(err), err);
                MessageBoxW(hMainWnd, buf, L"Message", MB_ICONWARNING);
                goto done_print_txt;
            }
        }

        if (text != NULL)
            shell_spool_txt(text, length, txt_writer, txt_newliner);
        else
            shell_spool_bitmap_to_txt(bits, bytesperline, x, y, width, height, txt_writer, txt_newliner);
        done_print_txt:;
    }

    if (state.printerToGifFile) {
        int err;
        
        if (print_gif != NULL
                && gif_lines + height > state.printerGifMaxLength) {
            shell_finish_gif(gif_seeker, gif_writer);
            fclose(print_gif);
            print_gif = NULL;
        }

        if (print_gif == NULL) {
            while (1) {
                int len, p;
                FILE *testfile;

                gif_seq = (gif_seq + 1) % 10000;

                wcscpy(print_gif_name, state.printerGifFileName);
                len = (int) wcslen(print_gif_name);

                /* Strip ".gif" extension, if present */
                if (len >= 4 &&
                    _wcsicmp(print_gif_name + len - 4, L".gif") == 0) {
                    len -= 4;
                    print_gif_name[len] = 0;
                }

                /* Strip ".[0-9]+", if present */
                p = len;
                while (p > 0 && print_gif_name[p] >= '0' && print_gif_name[p] <= '9')
                    p--;
                if (p < len && p >= 0 && print_gif_name[p] == '.')
                    print_gif_name[p] = 0;

                /* Make sure we have enough space for the ".nnnn.gif" */
                p = FILENAMELEN - 10;
                print_gif_name[p] = 0;
                p = (int) wcslen(print_gif_name);
                swprintf(print_gif_name + p, L".%04d", gif_seq);
                wcscat(print_gif_name, L".gif");
                
                /* I know, I know, the civilized thing to do would be to
                 * use stat(2) to find out if the file exists. Another time.
                 * (TODO)
                 */
                testfile = _wfopen(print_gif_name, L"rb");
                if (testfile != NULL)
                    fclose(testfile);
                else
                    break;
            }
            print_gif = _wfopen(print_gif_name, L"w+b");
            if (print_gif == NULL) {
                err = errno;
                state.printerToGifFile = 0;
                wchar_t cbuf[1000];
                swprintf(cbuf, L"Can't open \"%ls\" for output: %hs (%d)\nPrinting to GIF file disabled.", print_gif_name, strerror(err), err);
                MessageBoxW(hMainWnd, cbuf, L"Message", MB_ICONWARNING);
                goto done_print_gif;
            }
            if (!shell_start_gif(gif_writer, 143, state.printerGifMaxLength)) {
                state.printerToGifFile = 0;
                MessageBox(hMainWnd, "Not enough memory for the GIF encoder.\nPrinting to GIF file disabled.", "Message", MB_ICONWARNING);
                goto done_print_gif;
            }
            gif_lines = 0;
        }

        shell_spool_gif(bits, bytesperline, x, y, width, height, gif_writer);
        gif_lines += height;

        if (print_gif != NULL && gif_lines + 9 > state.printerGifMaxLength) {
            shell_finish_gif(gif_seeker, gif_writer);
            fclose(print_gif);
            print_gif = NULL;
        }
        done_print_gif:;
    }

    print_text[print_text_bottom++] = (char) (text == NULL ? 255 : length);
    if (print_text_bottom == PRINT_TEXT_SIZE)
        print_text_bottom = 0;
    if (text != NULL) {
        if (print_text_bottom + length < PRINT_TEXT_SIZE) {
            memcpy(print_text + print_text_bottom, text, length);
            print_text_bottom += length;
        } else {
            int part = PRINT_TEXT_SIZE - print_text_bottom;
            memcpy(print_text + print_text_bottom, text, part);
            memcpy(print_text, text + part, length - part);
            print_text_bottom = length - part;
        }
    }
    print_text_pixel_height += text == NULL ? 16 : 9;
    while (print_text_pixel_height > PRINT_LINES / 2 - 1) {
        int tll = print_text[print_text_top] == 255 ? 16 : 9;
        print_text_pixel_height -= tll;
        print_text_top += tll == 16 ? 1 : (print_text[print_text_top] + 1);
        if (print_text_top >= PRINT_TEXT_SIZE)
            print_text_top -= PRINT_TEXT_SIZE;
    }
}

extern const long keymap_filesize;
extern const char keymap_filedata[];

static void read_key_map(const wchar_t *keymapfilename) {
    FILE *keymapfile = _wfopen(keymapfilename, L"r");
    int kmcap = 0;
    char line[1024];
    int lineno = 0;

    if (keymapfile == NULL) {
        /* Try to create default keymap file */

        keymapfile = _wfopen(keymapfilename, L"wb");
        if (keymapfile == NULL)
            return;
        size_t n = fwrite(keymap_filedata, 1, keymap_filesize, keymapfile);
        if (n != keymap_filesize) {
            int err = errno;
            fwprintf(stderr, L"Error writing \"%ls\": %hs (%d)\n",
                            keymapfilename, strerror(err), err);
        }
        fclose(keymapfile);

        keymapfile = _wfopen(keymapfilename, L"r");
        if (keymapfile == NULL)
            return;
    }

    while (fgets(line, 1024, keymapfile) != NULL) {
        lineno++;
        keymap_entry *entry = parse_keymap_entry(line, lineno);
        if (entry != NULL) {
            /* Create new keymap entry */
            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));
        }
    }

    fclose(keymapfile);
}

static void init_shell_state(int4 version) {
    switch (version) {
        case -1:
            state.extras = 0;
            // fall through
        case 0:
            state.mainPlacement.length = sizeof(WINDOWPLACEMENT);
            state.mainPlacementValid = 0;
            state.printOutPlacement.length = sizeof(WINDOWPLACEMENT);
            state.printOutPlacementValid = 0;
            state.printOutOpen = 0;
            // fall through
        case 1:
            state.printerToTxtFile = 0;
            state.printerToGifFile = 0;
            state.printerTxtFileName[0] = 0;
            state.printerGifFileName[0] = 0;
            // fall through
        case 2:
            state.printerGifMaxLength = 256;
            // fall through
        case 3:
            state.skinName[0] = 0;
            // fall through
        case 4:
            state.alwaysOnTop = FALSE;
            // fall through
        case 5:
            state.singleInstance = TRUE;
            // fall through
        case 6:
            // fall through
        case 7:
            wcscpy(state.coreName, L"Untitled");
            // fall through
        case 8:
            core_settings.matrix_singularmatrix = false;
            core_settings.matrix_outofrange = false;
            core_settings.auto_repeat = true;
            // fall through
        case 9:
            // fall through
        case 10:
            core_settings.allow_big_stack = false;
            // fall through
        case 11:
            // fall through
        case 12:
            core_settings.localized_copy_paste = true;
            // fall through
        case 13:
            state.mainWindowWidth = 0;
            state.mainWindowHeight = 0;
            // fall through
        case 14:
            // current version (SHELL_VERSION = 14),
            // so nothing to do here since everything
            // was initialized from the state file.
            ;
    }
}

struct old_state_type {
    BOOL extras;
    WINDOWPLACEMENT mainPlacement;
    int mainPlacementValid;
    WINDOWPLACEMENT printOutPlacement;
    int printOutPlacementValid;
    int printOutOpen;
    int printerToTxtFile;
    int printerToGifFile;
    char printerTxtFileName[FILENAMELEN];
    char printerGifFileName[FILENAMELEN];
    int printerGifMaxLength;
    char skinName[FILENAMELEN];
    BOOL alwaysOnTop;
    BOOL singleInstance;
    BOOL calculatorKey;
    char coreName[FILENAMELEN];
    bool matrix_singularmatrix;
    bool matrix_outofrange;
    bool auto_repeat;
};

static int read_shell_state(int4 *ver) {
    int4 magic;
    int4 version;
    int4 state_size;
    int4 state_version;

    if (fread(&magic, 1, sizeof(int4), statefile) != sizeof(int4))
        return 0;
    if (magic != FREE42_MAGIC)
        return 0;

    if (fread(&version, 1, sizeof(int4), statefile) != sizeof(int4))
        return 0;

    if (version > 0) {
        if (fread(&state_size, 1, sizeof(int4), statefile) != sizeof(int4))
            return 0;
        if (fread(&state_version, 1, sizeof(int4), statefile) != sizeof(int4))
            return 0;
        if (state_version < 0 || state_version > SHELL_VERSION)
            /* Unknown shell state version */
            return 0;
        if (version > 27) {
            if (fread(&state, 1, state_size, statefile) != state_size)
                return 0;
            core_settings.matrix_singularmatrix = state.matrix_singularmatrix;
            core_settings.matrix_outofrange = state.matrix_outofrange;
            core_settings.auto_repeat = state.auto_repeat;
            core_settings.allow_big_stack = state.allow_big_stack;
            core_settings.localized_copy_paste = state.localized_copy_paste;
        } else {
            old_state_type old_state;
            if (fread(&old_state, 1, state_size, statefile) != state_size)
                return 0;
            state.extras = old_state.extras;
            state.mainPlacement = old_state.mainPlacement;
            state.mainPlacementValid = old_state.mainPlacementValid;
            state.printOutPlacement = old_state.printOutPlacement;
            state.printOutPlacementValid = old_state.printOutPlacementValid;
            state.printOutOpen = old_state.printOutOpen;
            state.printerToTxtFile = old_state.printerToTxtFile;
            state.printerToGifFile = old_state.printerToGifFile;
            MultiByteToWideChar(CP_ACP, 0, old_state.printerTxtFileName, FILENAMELEN, state.printerTxtFileName, FILENAMELEN);
            MultiByteToWideChar(CP_ACP, 0, old_state.printerGifFileName, FILENAMELEN, state.printerGifFileName, FILENAMELEN);
            state.printerGifMaxLength = old_state.printerGifMaxLength;
            MultiByteToWideChar(CP_ACP, 0, old_state.skinName, FILENAMELEN, state.skinName, FILENAMELEN);
            state.alwaysOnTop = old_state.alwaysOnTop;
            state.singleInstance = old_state.singleInstance;
            MultiByteToWideChar(CP_ACP, 0, old_state.coreName, FILENAMELEN, state.coreName, FILENAMELEN);
            state.matrix_singularmatrix = old_state.matrix_singularmatrix;
            state.matrix_outofrange = old_state.matrix_outofrange;
            state.auto_repeat = old_state.auto_repeat;
            if (state_version >= 9) {
                core_settings.matrix_singularmatrix = old_state.matrix_singularmatrix;
                core_settings.matrix_outofrange = old_state.matrix_outofrange;
                core_settings.auto_repeat = old_state.auto_repeat;
            }
        }
        // Initialize the parts of the shell state
        // that were NOT read from the state file
        init_shell_state(state_version);
    } else
        init_shell_state(-1);

    *ver = version;
    return 1;
}

static int write_shell_state() {
    int4 magic = FREE42_MAGIC;
    int4 version = 28;
    int4 state_size = sizeof(state_type);
    int4 state_version = SHELL_VERSION;

    if (fwrite(&magic, 1, sizeof(int4), statefile) != sizeof(int4))
        return 0;
    if (fwrite(&version, 1, sizeof(int4), statefile) != sizeof(int4))
        return 0;
    if (fwrite(&state_size, 1, sizeof(int4), statefile) != sizeof(int4))
        return 0;
    if (fwrite(&state_version, 1, sizeof(int4), statefile) != sizeof(int4))
        return 0;
    state.matrix_singularmatrix = core_settings.matrix_singularmatrix;
    state.matrix_outofrange = core_settings.matrix_outofrange;
    state.auto_repeat = core_settings.auto_repeat;
    state.allow_big_stack = core_settings.allow_big_stack;
    state.localized_copy_paste = core_settings.localized_copy_paste;
    if (fwrite(&state, 1, sizeof(state_type), statefile) != sizeof(state_type))
        return 0;

    return 1;
}

/* Callbacks used by shell_print() and shell_spool_txt() / shell_spool_gif() */

static void txt_writer(const char *text, int length) {
    if (print_txt == NULL)
        return;
    size_t n = fwrite(text, 1, length, print_txt);
    if (n != length) {
        wchar_t buf[1000];
        state.printerToTxtFile = 0;
        fclose(print_txt);
        print_txt = NULL;
        swprintf(buf, L"Error while writing to \"%ls\".\nPrinting to TXT file disabled", state.printerTxtFileName);
        MessageBoxW(hMainWnd, buf, L"Message", MB_ICONWARNING);
    }
}

static void txt_newliner() {
    if (print_txt == NULL)
        return;
    fputs("\r\n", print_txt);
    fflush(print_txt);
}

static void gif_seeker(int4 pos) {
    if (print_gif == NULL)
        return;
    if (fseek(print_gif, pos, SEEK_SET) == -1) {
        wchar_t buf[1000];
        state.printerToGifFile = 0;
        fclose(print_gif);
        print_gif = NULL;
        swprintf(buf, L"Error while seeking \"%ls\".\nPrinting to GIF file disabled", print_gif_name);
        MessageBoxW(hMainWnd, buf, L"Message", MB_ICONWARNING);
    }
}

static void gif_writer(const char *text, int length) {
    if (print_gif == NULL)
        return;
    size_t n = fwrite(text, 1, length, print_gif);
    if (n != length) {
        wchar_t buf[1000];
        state.printerToGifFile = 0;
        fclose(print_gif);
        print_gif = NULL;
        swprintf(buf, L"Error while writing to \"%ls\".\nPrinting to GIF file disabled", print_gif_name);
        MessageBoxW(hMainWnd, buf, L"Message", MB_ICONWARNING);
    }
}

static FILE *logfile = NULL;

void shell_log(const char *message) {
    if (logfile == NULL)
        logfile = fopen("free42.log", "w");
    fprintf(logfile, "%s\n", message);
    fflush(logfile);
}

ci_string GetDlgItemTextLong(HWND hWnd, int item) {
    // If you're losing sleep over GetDlgItemText() potentially returning truncated values...
    size_t sz = 256;
    wchar_t *buf = (wchar_t *) malloc(sz * 2);
    if (buf == NULL)
        return L"";
    while (true) {
        GetDlgItemTextW(hWnd, item, buf, (int) sz);
        if (wcslen(buf) < sz - 1) {
            ci_string retval(buf);
            free(buf);
            return retval;
        }
        sz += 256;
        wchar_t *buf2 = (wchar_t *) realloc(buf, sz * 2);
        if (buf2 == NULL) {
            free(buf);
            return L"";
        }
        buf = buf2;
    }
}

ci_string to_ci_string(int i) {
    wchar_t buf[22];
    swprintf(buf, L"%d", i);
    return buf;
}

char *wide2utf(const wchar_t *w) {
    int wlen = (int) wcslen(w);
    int slen = WideCharToMultiByte(CP_UTF8, 0, w, wlen, NULL, 0, NULL, NULL);
    char *s = (char *) malloc(slen + 1);
    WideCharToMultiByte(CP_UTF8, 0, w, wlen, s, slen, NULL, NULL);
    s[slen] = 0;
    return s;
}

static wchar_t *utf2wide(const char *s) {
    int slen = (int) strlen(s);
    int wlen = MultiByteToWideChar(CP_UTF8, 0, s, slen, NULL, 0);
    wchar_t *w = (wchar_t *) malloc(wlen * 2 + 2);
    MultiByteToWideChar(CP_UTF8, 0, s, slen, w, wlen);
    w[wlen] = 0;
    return w;
}

FILE *my_fopen(const char *name, const char *mode) {
    wchar_t *wname = utf2wide(name);
    wchar_t *wmode = utf2wide(mode);
    FILE *ret = _wfopen(wname, wmode);
    free(wname);
    free(wmode);
    return ret;
}

int my_rename(const char *oldname, const char *newname) {
    wchar_t *woldname = utf2wide(oldname);
    wchar_t *wnewname = utf2wide(newname);
    int ret = _wrename(woldname, wnewname);
    free(woldname);
    free(wnewname);
    return ret;
}

int my_remove(const char *name) {
    wchar_t *wname = utf2wide(name);
    int ret = _wremove(wname);
    free(wname);
    return ret;
}

void get_keymap(keymap_entry **map, int *length) {
    *map = keymap;
    *length = keymap_length;
}
