#include "framework.h"
#include "shellapi.h"
#include "commdlg.h"
#include "CommCtrl.h"
#include "zxianui.h"
#include "Controls.h"
#include "combaseapi.h"
#include "FileUtilities.h"
#include "History.h"

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// FOR DURING DEVELOPMENT!!!!!!!!
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
//#define IS_DURING_DEVELOPMENT
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

#ifdef IS_DURING_DEVELOPMENT
#define ZXIAN_EXE_PATH                  L"..\\..\\Release\\zxian.exe"
#define ZXIAN_DIRECTORY                 L"..\\..\\Release\\"
#define ZXIAN_CONTROLLER_CONFIG_DIR     L"..\\..\\Release\\controller"
#define ZXIANUI_HISTORY_FILE             "..\\..\\Release\\zxianui.his"
#else
#define ZXIAN_EXE_PATH                  L"zxian.exe"
#define ZXIAN_DIRECTORY                 L"."
#define ZXIAN_CONTROLLER_CONFIG_DIR     L".\\controller"
#define ZXIANUI_HISTORY_FILE             "zxianui.his"
#endif

#define WINDOW_WIDTH 550
#define WINDOW_HEIGHT 720

#define MAX_LOADSTRING 256
#define COMPONENT_PADDING 15
#define WINDOW_PADDING COMPONENT_PADDING

// this height is used immediately inside at the top of a group box
#define TOP_BOX_PADDING ((int)(1.5*COMPONENT_PADDING))

// this width is used for most dropdowns
#define STANDARD_DROPDOWN_WIDTH (390-COMPONENT_PADDING)

#define SELECTLIST_HEIGHT 20

#define FILE_GROUPBOX_ID 14
#define FILE_GROUPBOX_X (WINDOW_PADDING)
#define FILE_GROUPBOX_Y 0 // top of the window
#define FILE_GROUPBOX_WIDTH (WINDOW_WIDTH-WINDOW_PADDING*3)
#define FILE_GROUPBOX_HEIGHT 180

#define FILE_BUTTON_WIDTH 100
#define FILE_BUTTON_HEIGHT 30
#define FILE_BUTTON_Y (FILE_GROUPBOX_Y + 2*COMPONENT_PADDING)

#define FILE__BROWSE__BUTON_X (FILE_GROUPBOX_X + COMPONENT_PADDING)
#define FILE__RUN__BUTON_X (FILE__BROWSE__BUTON_X + FILE_BUTTON_WIDTH + COMPONENT_PADDING)
#define FILE__CLEAR__BUTON_X (FILE_GROUPBOX_X + FILE_GROUPBOX_WIDTH - FILE_BUTTON_WIDTH - COMPONENT_PADDING)

#define FILE_PATH_TEXT_BOX_X (FILE_GROUPBOX_X + COMPONENT_PADDING)
#define FILE_PATH_TEXT_BOX_Y (FILE_BUTTON_Y + FILE_BUTTON_HEIGHT + COMPONENT_PADDING)
#define FILE_PATH_TEXT_BOX_HEIGHT 60
#define FILE_PATH_TEXT_BOX_WIDTH (FILE_GROUPBOX_WIDTH - 2*COMPONENT_PADDING)

#define VIDEO_GROUPBOX_ID 8
#define VIDEO_GROUPBOX_X (WINDOW_PADDING)
#define VIDEO_GROUPBOX_Y (FILE_GROUPBOX_Y + FILE_GROUPBOX_HEIGHT + COMPONENT_PADDING)
#define VIDEO_GROUPBOX_WIDTH (WINDOW_WIDTH-WINDOW_PADDING*3)
#define VIDEO_GROUPBOX_HEIGHT 226

#define ZOOM_BOX_WIDTH 50
#define ZOOM_TEXT_WIDTH 100
#define ZOOM_X (WINDOW_PADDING + COMPONENT_PADDING)
#define ZOOM_Y (VIDEO_GROUPBOX_Y + TOP_BOX_PADDING)

#define DISPLAY_MODE_TEXT_WIDTH 100
#define DISPLAY_MODE_X (ZOOM_X)
#define DISPLAY_MODE_Y (ZOOM_Y + SELECTLIST_HEIGHT + COMPONENT_PADDING)
#define DISPLAY_MODE_BOX_WIDTH STANDARD_DROPDOWN_WIDTH

#define TIMER_TEXT_WIDTH 100
#define TIMER_X (ZOOM_X)
#define TIMER_Y (DISPLAY_MODE_Y + SELECTLIST_HEIGHT + COMPONENT_PADDING)
#define TIMER_BOX_WIDTH STANDARD_DROPDOWN_WIDTH

#define FRAMEMS_EDITBOX_ID 11
#define FRAMEMS_EDITBOX_X (WINDOW_PADDING + COMPONENT_PADDING + 100)
#define FRAMEMS_EDITBOX_Y (TIMER_Y + SELECTLIST_HEIGHT + COMPONENT_PADDING)
#define FRAMEMS_EDITBOX_WIDTH 30
#define FRAMEMS_LABEL_X (WINDOW_PADDING + COMPONENT_PADDING)

#define RENDERER_TEXT_WIDTH 100
#define RENDERER_X (ZOOM_X)
#define RENDERER_Y (FRAMEMS_EDITBOX_Y + COMPONENT_PADDING + 54)
#define RENDERER_BOX_WIDTH STANDARD_DROPDOWN_WIDTH

#define SCANLINES_CHECKBOX_ID 3
#define SCANLINES_X (ZOOM_X + COMPONENT_PADDING + 140)
#define SCANLINES_Y (ZOOM_Y)
#define SCANLINES_WIDTH 85

#define NOFRAMESKIP_CHECKBOX_ID 10
#define NOFRAMESKIP_X (SCANLINES_X + COMPONENT_PADDING + 80)
#define NOFRAMESKIP_Y (SCANLINES_Y)
#define NOFRAMESKIP_WIDTH 110

#define POKE_GROUPBOX_ID 6
#define POKE_GROUPBOX_X (WINDOW_PADDING)
#define POKE_GROUPBOX_Y (VIDEO_GROUPBOX_Y + VIDEO_GROUPBOX_HEIGHT + COMPONENT_PADDING)
#define POKE_GROUPBOX_WIDTH (WINDOW_WIDTH-WINDOW_PADDING*3)
#define POKE_GROUPBOX_HEIGHT 50

#define POKE_CHECKBOX_ID 4
#define POKE_X (WINDOW_PADDING + COMPONENT_PADDING)
#define POKE_Y (POKE_GROUPBOX_Y + (int)TOP_BOX_PADDING)
#define POKE_WIDTH 70

#define POKE_ADDRESS_EDITBOX_ID 5
#define POKE_ADDRESS_EDITBOX_X (POKE_X + POKE_WIDTH + 80)
#define POKE_ADDRESS_EDITBOX_Y (POKE_Y)
#define POKE_ADDRESS_EDITBOX_WIDTH 50
#define POKE_ADDRESS_LABEL_X (POKE_ADDRESS_EDITBOX_X-62)

#define POKE_VALUE_EDITBOX_ID 7
#define POKE_VALUE_EDITBOX_X (POKE_ADDRESS_EDITBOX_X + 110)
#define POKE_VALUE_EDITBOX_Y (POKE_ADDRESS_EDITBOX_Y)
#define POKE_VALUE_EDITBOX_WIDTH 30
#define POKE_VALUE_LABEL_X (POKE_VALUE_EDITBOX_X-42)

#define AUDIO_GROUPBOX_ID 9
#define AUDIO_GROUPBOX_X (WINDOW_PADDING)
#define AUDIO_GROUPBOX_Y (POKE_GROUPBOX_Y + POKE_GROUPBOX_HEIGHT + COMPONENT_PADDING)
#define AUDIO_GROUPBOX_WIDTH (WINDOW_WIDTH-WINDOW_PADDING*3)
#define AUDIO_GROUPBOX_HEIGHT 95

#define SILENCE_CHECKBOX_ID 1
#define SILENCE_X (POKE_X)
#define SILENCE_Y (AUDIO_GROUPBOX_Y + TOP_BOX_PADDING)
#define SILENCE_WIDTH 70

#define SAMPLE_RATE_TEXT_WIDTH 100
#define SAMPLE_RATE_X (SILENCE_X)
#define SAMPLE_RATE_Y (SILENCE_Y + COMPONENT_PADDING + 10)
#define SAMPLE_RATE_WIDTH STANDARD_DROPDOWN_WIDTH

#define CONTROLLER_GROUPBOX_ID 13
#define CONTROLLER_GROUPBOX_WIDTH (WINDOW_WIDTH-WINDOW_PADDING*3)
#define CONTROLLER_GROUPBOX_X (WINDOW_PADDING)
#define CONTROLLER_GROUPBOX_Y (AUDIO_GROUPBOX_Y + AUDIO_GROUPBOX_HEIGHT + COMPONENT_PADDING)
#define CONTROLLER_GROUPBOX_HEIGHT 48

#define CONTROLLER_CONFIG_LABEL_X (SAMPLE_RATE_X)
#define CONTROLLER_CONFIG_LABEL_Y (CONTROLLER_GROUPBOX_Y + TOP_BOX_PADDING)
#define CONTROLLER_CONFIG_LABEL_TEXT_WIDTH 100

#define CONTROLLER_X (SILENCE_X)
#define CONTROLLER_Y (CONTROLLER_GROUPBOX_Y + COMPONENT_PADDING)
#define CONTROLLER_BOX_WIDTH STANDARD_DROPDOWN_WIDTH

// this is for the dropdown at the top of the UI, but because it was introduced later,
// it is declared here

#define HISTORY_LABEL_X (CONTROLLER_CONFIG_LABEL_X)
#define HISTORY_LABEL_Y (FILE_PATH_TEXT_BOX_Y + FILE_PATH_TEXT_BOX_HEIGHT + COMPONENT_PADDING)
#define HISTORY_LABEL_TEXT_WIDTH 100

#define HISTORY_X (CONTROLLER_X)
#define HISTORY_Y (FILE_PATH_TEXT_BOX_Y + FILE_PATH_TEXT_BOX_HEIGHT + COMPONENT_PADDING)
#define HISTORY_BOX_WIDTH STANDARD_DROPDOWN_WIDTH

// ========================================
// 
//             NEXT ID IS 15
// 
// ========================================


#define MAX_COMMAND_LINE_LENGTH (MAX_PATH * 5)
#define MAX_BROWSED_FILE_SELECTOR (MAX_PATH + 50)

HINSTANCE _hInst;                                // current instance
WCHAR _szTitle[MAX_LOADSTRING];                  // The title bar text
WCHAR _szWindowClass[MAX_LOADSTRING];            // the main window class name

HWND _hRunZxianButton;
HWND _hBrowseButton;
HWND _hClearButton;
HWND _hFilePathTextbox;
HWND _hZoomDropdown;
HWND _hDisplayModeDropdown;
HWND _hTimerDropdown;
HWND _hSilenceCheckbox;
HWND _hPokeCheckbox;
HWND _hScanlinesCheckbox;
HWND _hPokeAddressEditbox;
HWND _hPokeValueEditbox;
HWND _hPokeGroupbox;
HWND _hFileGroupbox;
HWND _hVideoGroupbox;
HWND _hAudioGroupbox;
HWND _hControllerConfigGroupBox;
HWND _hNoFrameskipCheckbox;
HWND _hFrameMsEditbox;
HWND _hRendererDropdown;
HWND _hSampleRateDropdown;
HWND _hControllerConfigDropdown;
HWND _hHistoryDropdown;


// these are used to build up a command line for zxian
WCHAR _szBrowsedFilePath[MAX_PATH+1] = {};

WCHAR __FILE_TYPE_SELECTOR__SNA[] = L"-sna";
WCHAR __FILE_TYPE_SELECTOR__TAP[] = L"-tap";
WCHAR* _szBrowsedFileTypeSelector = NULL;

WCHAR __SILENCE_SELECTOR__ON[] = L"-silence";
WCHAR __SILENCE_SELECTOR__OFF[] = L"";
WCHAR* _szSilenceSelector = __SILENCE_SELECTOR__OFF;

WCHAR __POKE_SELECTOR__ON[] = L"-poke";
WCHAR __POKE_SELECTOR__OFF[] = L"";
WCHAR* _szPokeSelector = __POKE_SELECTOR__OFF;

WCHAR __SCANLINES_SELECTOR__ON[] = L"-scanlines";
WCHAR __SCANLINES_SELECTOR__OFF[] = L"";
WCHAR* _szScanlinesSelector = __SCANLINES_SELECTOR__OFF;

WCHAR __NOFRAMESKIP_SELECTOR__ON[] = L"-noframeskip";
WCHAR __NOFRAMESKIP_SELECTOR__OFF[] = L"";
WCHAR* _szNoFrameskipSelector = __NOFRAMESKIP_SELECTOR__OFF;

#define DEFAULT_ZOOM_VALUE 3    // these have to match
WCHAR _szZoomValue[64] = L"3";  // these have to match

#define DEFAULT_DISPLAY_MODE_VALUE 0    // these have to match
WCHAR _szDisplayModeValue[64] = L"0";   // these have to match

#define DEFAULT_TIMER_VALUE 0   // these have to match
WCHAR _szTimerValue[64] = L"0"; // these have to match

#define DEFAULT_RENDERER_VALUE 0   // these have to match
WCHAR _szRendererValue[64] = L"0"; // these have to match

#define DEFAULT_SAMPLE_RATE_VALUE 0   // these have to match
WCHAR _szSampleRateValue[64] = L"384000"; // these have to match

#define DEFAULT_CONTROLLER_CONFIG_VALUE 0   // these have to match
WCHAR _szControlleConfigValue[129] = L":NONE:  use internal config (kempston)"; // these have to match

const WCHAR* _szUnsupportedFile = L"UNSUPPORTED FILE\r\n\r\nClick [Run] to start in BASIC, or [Browse] to load a file\r\nYou can also pick from the history entries below.";

// forward declarations of functions
ATOM                MyRegisterClass(HINSTANCE hInstance);
BOOL                InitInstance(HINSTANCE, int);
LRESULT CALLBACK    WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK    About(HWND, UINT, WPARAM, LPARAM);

int APIENTRY wWinMain(
    _In_ HINSTANCE hInstance,
    _In_opt_ HINSTANCE hPrevInstance,
    _In_ LPWSTR    lpCmdLine,
    _In_ int       nCmdShow)
{
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);

    // initialize global strings
    LoadStringW(hInstance, IDS_APP_TITLE, _szTitle, MAX_LOADSTRING);
    LoadStringW(hInstance, IDC_ZXIANUI, _szWindowClass, MAX_LOADSTRING);
    MyRegisterClass(hInstance);

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

    // main message loop
    MSG msg;
    while (GetMessage(&msg, nullptr, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return (int)msg.wParam;
}

// Registers the window class
ATOM MyRegisterClass(HINSTANCE hInstance)
{
    WNDCLASSEXW wcex;

    wcex.cbSize = sizeof(WNDCLASSEX);

    wcex.style = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc = WndProc;
    wcex.cbClsExtra = 0;
    wcex.cbWndExtra = 0;
    wcex.hInstance = hInstance;
    wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ZXIANUI));
    wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
    wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_ZXIANUI);
    wcex.lpszClassName = _szWindowClass;
    wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_ZXIANUI));

    return RegisterClassExW(&wcex);
}

// Saves instance handle and creates main window
// In this function, we save the instance handle in a global variable and create and display the main program window
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
    _hInst = hInstance;

    int windowHeight = WINDOW_HEIGHT;
    int windowWidth = WINDOW_WIDTH;
    int windowX = (GetSystemMetrics(SM_CXSCREEN) - windowWidth) / 2;
    int windowY = (GetSystemMetrics(SM_CYSCREEN) - windowHeight) / 2;

    HWND hMainWindow = CreateWindowW(
        _szWindowClass,
        _szTitle,
        WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
        windowX,
        windowY,
        windowWidth,
        windowHeight,
        nullptr,
        nullptr,
        hInstance,
        nullptr);

    if (!hMainWindow)
    {
        return FALSE;
    }

    ShowWindow(hMainWindow, nCmdShow);
    UpdateWindow(hMainWindow);

    return TRUE;
}

UINT_PTR CALLBACK OFNHookProcOldStyle(HWND hdlg, UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
    return 0;
}

WCHAR* get_extension(WCHAR* filename) {
    if (filename == NULL) {
        return NULL;
    }
    WCHAR* dot = wcsrchr(filename, L'.');
    if (!dot || dot == filename) {
        // not found
        // or 
        // found on first position (e.g. ".config")
        return NULL;
    }
    return dot;
}

/// <summary>
/// 1. Populate text box with either:
///     - file name, or
///     - an "extension unsupported" message
/// 2. Set file type selector to either:
///     - -sna, -tap, etc. when browsed file has a supported extension
///     - NULL otherwise
/// 
/// ASSUMES: _szBrowsedFilePath already populated with user choice of file path
/// </summary>
void HandleBrowsedFileChanged(HWND hWnd) {
    // extract extension
    WCHAR* extensionLowerCase = (WCHAR*)malloc((MAX_PATH + 1) * sizeof(WCHAR));
    if (!extensionLowerCase) {
        return;
    }
    
    WCHAR* extension = get_extension(_szBrowsedFilePath);
    if (extension != NULL) {
        // based on extension, select a file selector (-sna, -tap, etc.)
        wcscpy_s(extensionLowerCase, MAX_PATH, extension);
        _wcslwr_s(extensionLowerCase, MAX_PATH);

        if (!wcscmp(extensionLowerCase, L".sna") || !wcscmp(extensionLowerCase, L".z80")) {
            _szBrowsedFileTypeSelector = __FILE_TYPE_SELECTOR__SNA;
            // change box text
            SendMessage(_hFilePathTextbox, WM_SETTEXT, 0, (LPARAM)_szBrowsedFilePath);
        } else if (!wcscmp(extensionLowerCase, L".tap")) {
            _szBrowsedFileTypeSelector = __FILE_TYPE_SELECTOR__TAP;
            // change box text
            SendMessage(_hFilePathTextbox, WM_SETTEXT, 0, (LPARAM)_szBrowsedFilePath);
        } else {
            // an extension was not found, so it's an invalid file
            _szBrowsedFileTypeSelector = NULL;
            
            // change box text
            SendMessage(_hFilePathTextbox, WM_SETTEXT, 0, (LPARAM)_szUnsupportedFile);
        }
    }
    else {
        // an extension was not found, so it's an invalid file
        _szBrowsedFileTypeSelector = NULL;
        // change box text
        SendMessage(_hFilePathTextbox, WM_SETTEXT, 0, (LPARAM)_szUnsupportedFile);
    }
    
    // force a re-render of the entire window
    InvalidateRect(hWnd, NULL, NULL);

    free(extensionLowerCase);
}

void TryHandleBrowseButton(HWND hWnd, LPARAM lParam) {
    if ((HWND)lParam != _hBrowseButton) {
        return;
    }

    OPENFILENAME ofn;
    ZeroMemory(&ofn, sizeof(ofn));
    ofn.lStructSize = sizeof(ofn);
    ofn.hwndOwner = hWnd;
    ofn.lpstrFilter = L"All supported (.z80, .sna, .tap)\0*.z80;*.sna;*.tap\0Snapshots (.z80, .sna)\0*.z80;*.sna\0Tape images (.tap)\0*.tap\0\0";
    ofn.lpstrFile = _szBrowsedFilePath;
    ofn.nMaxFile = ARRAYSIZE(_szBrowsedFilePath);
    ofn.Flags = OFN_NOCHANGEDIR;    // GetOpenFileName changes current directory by default,
                                    // so without this, invocation of zxian will fail
    ofn.lpfnHook = &OFNHookProcOldStyle;

    BOOL isSuccessful = GetOpenFileName(&ofn);
    if (!isSuccessful)
    {
        return;
    }

    HandleBrowsedFileChanged(hWnd);
}

void TryHandleClearButton(HWND hWnd, LPARAM lParam) {
    if ((HWND)lParam != _hClearButton) {
        return;
    }

    _szBrowsedFileTypeSelector = NULL;

    SendMessage(_hFilePathTextbox, WM_SETTEXT, 0, (LPARAM)TEXT("Click [Run] to start in BASIC, or [Browse] to load a file\r\nYou can also pick from the history entries below."));
    InvalidateRect(hWnd, NULL, NULL);
}

void TryHandleZoomChange(HWND hWnd, LPARAM lParam) {
    if ((HWND)lParam != _hZoomDropdown) {
        return;
    }

    LRESULT index = SendMessage((HWND)lParam, (UINT)CB_GETCURSEL, (WPARAM)0, (LPARAM)0);
    // read currently-selected value into a variable
    SendMessage((HWND)lParam, (UINT)CB_GETLBTEXT, (WPARAM)index, (LPARAM)_szZoomValue);
}

void TryHandleHistoryChange(HWND hWnd, WPARAM wParam, LPARAM lParam) {
    if ((HWND)lParam != _hHistoryDropdown) {
        return;
    }

    if (HIWORD(wParam) != CBN_SELCHANGE) {
        // as opposed to most other dropdowns (which are find to be handled
        // on their mere population),
        // this one must cause a change ONLY WHEN the user actually selects a value
        return;
    }

    LRESULT index = SendMessage((HWND)lParam, (UINT)CB_GETCURSEL, (WPARAM)0, (LPARAM)0);
    // look up selected index into list of history entries
    int entriesCount;
    const char** entries = history_get_entries(&entriesCount);
    if (index >= entriesCount) {
        // this shouldn't really happen...
        swprintf_s(_szBrowsedFilePath, MAX_PATH + 1, L"Invalid");
    }
    else {
        // copy currently-selected value into our variable, converting ASCII to wide
        size_t convertedBytes;
        mbstowcs_s(&convertedBytes, _szBrowsedFilePath, MAX_PATH + 1, entries[index], strlen(entries[index]) + 1);
    }

    HandleBrowsedFileChanged(hWnd);
}

void TryHandleDisplayModeChange(HWND hWnd, LPARAM lParam) {
    if ((HWND)lParam != _hDisplayModeDropdown) {
        return;
    }

    LRESULT index = SendMessage((HWND)lParam, (UINT)CB_GETCURSEL, (WPARAM)0, (LPARAM)0);
    // read currently-selected value into a variable
    SendMessage((HWND)lParam, (UINT)CB_GETLBTEXT, (WPARAM)index, (LPARAM)_szDisplayModeValue);
    _szDisplayModeValue[1] = 0; // terminate after one characters, thus keeping just the numeric
                                // value
}

void TryHandleRendererDropdownChange(HWND hWnd, LPARAM lParam) {
    if ((HWND)lParam != _hRendererDropdown) {
        return;
    }

    LRESULT index = SendMessage((HWND)lParam, (UINT)CB_GETCURSEL, (WPARAM)0, (LPARAM)0);
    // read currently-selected value into a variable
    SendMessage((HWND)lParam, (UINT)CB_GETLBTEXT, (WPARAM)index, (LPARAM)_szRendererValue);
    _szRendererValue[1] = 0; // terminate after one characters, thus keeping just the numeric
    // value
}

void TryHandleTimerDropdownChange(HWND hWnd, LPARAM lParam) {
    if ((HWND)lParam != _hTimerDropdown) {
        return;
    }

    LRESULT index = SendMessage((HWND)lParam, (UINT)CB_GETCURSEL, (WPARAM)0, (LPARAM)0);
    // read currently-selected value into a variable
    SendMessage((HWND)lParam, (UINT)CB_GETLBTEXT, (WPARAM)index, (LPARAM)_szTimerValue);
    _szTimerValue[1] = 0; // terminate after one characters, thus keeping just the numeric
    // value
}

void TryHandleControllerConfigDropdownChange(HWND hWnd, LPARAM lParam) {
    if ((HWND)lParam != _hControllerConfigDropdown) {
        return;
    }

    LRESULT index = SendMessage((HWND)lParam, (UINT)CB_GETCURSEL, (WPARAM)0, (LPARAM)0);
    // read currently-selected value into a variable
    SendMessage((HWND)lParam, (UINT)CB_GETLBTEXT, (WPARAM)index, (LPARAM)_szControlleConfigValue);
}

void TryHandleSampleRateDropdownChange(HWND hWnd, LPARAM lParam) {
    if ((HWND)lParam != _hSampleRateDropdown) {
        return;
    }

    LRESULT index = SendMessage((HWND)lParam, (UINT)CB_GETCURSEL, (WPARAM)0, (LPARAM)0);
    // read currently-selected value into a variable
    SendMessage((HWND)lParam, (UINT)CB_GETLBTEXT, (WPARAM)index, (LPARAM)_szSampleRateValue);
    _szSampleRateValue[6] = 0; // terminate after longest possible numeric value
    // value
}

void TryHandleCheckboxChange(HWND hWnd, WPARAM wParam, LPARAM lParam, HWND hCheckbox, int checkboxId) {
    if (
        (HWND)lParam != hCheckbox ||
        LOWORD(wParam) != checkboxId
        ) {
        return;
    }

    BOOL isChecked = IsDlgButtonChecked(hWnd, checkboxId);
    CheckDlgButton(hWnd, checkboxId, isChecked ? BST_UNCHECKED : BST_CHECKED);
}

void TryHandleSilenceCheckboxChange(HWND hWnd, WPARAM wParam, LPARAM lParam) {
    TryHandleCheckboxChange(hWnd, wParam, lParam, _hSilenceCheckbox, SILENCE_CHECKBOX_ID);
    _szSilenceSelector = IsDlgButtonChecked(hWnd, SILENCE_CHECKBOX_ID) ? __SILENCE_SELECTOR__ON : __SILENCE_SELECTOR__OFF;
}

void TryHandlePokeCheckboxChange(HWND hWnd, WPARAM wParam, LPARAM lParam) {
    TryHandleCheckboxChange(hWnd, wParam, lParam, _hPokeCheckbox, POKE_CHECKBOX_ID);
    _szPokeSelector = IsDlgButtonChecked(hWnd, POKE_CHECKBOX_ID) ? __POKE_SELECTOR__ON : __POKE_SELECTOR__OFF;
}

void TryHandleScanlinesCheckboxChange(HWND hWnd, WPARAM wParam, LPARAM lParam) {
    TryHandleCheckboxChange(hWnd, wParam, lParam, _hScanlinesCheckbox, SCANLINES_CHECKBOX_ID);
    _szScanlinesSelector = IsDlgButtonChecked(hWnd, SCANLINES_CHECKBOX_ID) ? __SCANLINES_SELECTOR__ON : __SCANLINES_SELECTOR__OFF;
}

void TryHandleNoFrameskipCheckboxChange(HWND hWnd, WPARAM wParam, LPARAM lParam) {
    TryHandleCheckboxChange(hWnd, wParam, lParam, _hNoFrameskipCheckbox, NOFRAMESKIP_CHECKBOX_ID);
    _szNoFrameskipSelector = IsDlgButtonChecked(hWnd, NOFRAMESKIP_CHECKBOX_ID) ? __NOFRAMESKIP_SELECTOR__ON : __NOFRAMESKIP_SELECTOR__OFF;
}

void TryHandleRunZxianButton(HWND hWnd, LPARAM lParam) {
    if ((HWND)lParam != _hRunZxianButton) {
        return;
    }

    // build the command line
    WCHAR* commandLineArguments = (WCHAR*)malloc((MAX_COMMAND_LINE_LENGTH + 1) * sizeof(WCHAR));
    if (!commandLineArguments) {
        return;
    }

    WCHAR* browsedFile = (WCHAR*)malloc((MAX_BROWSED_FILE_SELECTOR + 1) * sizeof(WCHAR));
    if (!browsedFile) {
        free(commandLineArguments);
        return;
    }

    browsedFile[0] = 0;     // browsedFile := empty string
    if (_szBrowsedFileTypeSelector != NULL) {
        // we are specifying a file to load
        swprintf_s(browsedFile, MAX_BROWSED_FILE_SELECTOR, L" %s \"%s\"",
            _szBrowsedFileTypeSelector,
            _szBrowsedFilePath
        );

        // save it into history
        char* toAscii = (char*)malloc((MAX_PATH + 1) * sizeof(char));
        if (toAscii != NULL) {
            size_t bytesConverted;
            // convert wide to ASCII
            wcstombs_s(&bytesConverted, toAscii, MAX_PATH + 1, _szBrowsedFilePath, wcslen(_szBrowsedFilePath)+1);
            history_add(toAscii);
        }

        // reload history dropdown, since history may have changed
        controls_repopulate_history_dropdown(_hHistoryDropdown);
    }

    WCHAR frameMsValue[8];
    frameMsValue[0] = 0;
    GetWindowText(_hFrameMsEditbox, frameMsValue, 8);
    
    // specifying -pokeaddr without -poke will still enable the poke UI
    // so specify either all poke "group" options, or none
    WCHAR pokeAddrValue[8];
    pokeAddrValue[0] = 0;
    GetWindowText(_hPokeAddressEditbox, pokeAddrValue, 8);

    WCHAR pokeValueValue[8];
    pokeValueValue[0] = 0;
    GetWindowText(_hPokeValueEditbox, pokeValueValue, 8);

    WCHAR pokeGroup[64];
    pokeGroup[0] = 0;
    int isPokeUiEnabled = _szPokeSelector[0] != 0;
    if (isPokeUiEnabled) {
        swprintf_s(pokeGroup, 63, L" -poke -pokeaddr %s -pokeval %s",
            pokeAddrValue,
            pokeValueValue
        );
    }

    WCHAR controllerConfigGroup[MAX_PATH];
    controllerConfigGroup[0] = 0;
    if (_szControlleConfigValue[0] != ':') {
        // we detect real file name selection knowing that the "none" hardcoded
        // value starts with a character that's invalid as part of a file name
        swprintf_s(controllerConfigGroup, MAX_PATH-1, L"-controllerconfig \"%s\"",
            _szControlleConfigValue
        );
    }

    swprintf_s(commandLineArguments, MAX_COMMAND_LINE_LENGTH, L"-soundsamples %s -renderer %s -framems %s %s %s %s %s -zoom %s -displaymode %s -videomode %s %s %s",
        _szSampleRateValue,
        _szRendererValue,
        frameMsValue,
        _szNoFrameskipSelector,
        _szScanlinesSelector,
        pokeGroup,
        _szSilenceSelector,
        _szZoomValue,
        _szDisplayModeValue,
        _szTimerValue,
        controllerConfigGroup,
        browsedFile
    );

    // ShellExecute might delegate to COM, so initialize COM
    const HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);

    ShellExecuteW(hWnd, NULL, ZXIAN_EXE_PATH, commandLineArguments, ZXIAN_DIRECTORY, SW_NORMAL);

    free(browsedFile);
    free(commandLineArguments);
}

// Processes messages for the main window
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_CREATE:
    {
        // window has just been created

        // initialize modules
        history_start(ZXIANUI_HISTORY_FILE);

        // create controls
        _hBrowseButton = MakeButton(hWnd, L"Browse", FILE__BROWSE__BUTON_X, FILE_BUTTON_Y, FILE_BUTTON_WIDTH, FILE_BUTTON_HEIGHT);
        _hRunZxianButton = MakeButton(hWnd, L"Run", FILE__RUN__BUTON_X, FILE_BUTTON_Y, FILE_BUTTON_WIDTH, FILE_BUTTON_HEIGHT);
        _hClearButton = MakeButton(hWnd, L"Clear", FILE__CLEAR__BUTON_X, FILE_BUTTON_Y, FILE_BUTTON_WIDTH, FILE_BUTTON_HEIGHT);

        _hZoomDropdown = MakeZoomDropdown(hWnd, DEFAULT_ZOOM_VALUE - 1, ZOOM_X + ZOOM_TEXT_WIDTH, ZOOM_Y, ZOOM_BOX_WIDTH, 300);
        _hDisplayModeDropdown = MakeDisplayModeDropdown(hWnd, DEFAULT_DISPLAY_MODE_VALUE, DISPLAY_MODE_X + DISPLAY_MODE_TEXT_WIDTH, DISPLAY_MODE_Y, DISPLAY_MODE_BOX_WIDTH, 300);
        _hTimerDropdown = MakeTimerDropdown(hWnd, DEFAULT_TIMER_VALUE, TIMER_X + TIMER_TEXT_WIDTH, TIMER_Y, TIMER_BOX_WIDTH, 300);
        _hRendererDropdown = MakeRendererDropdown(hWnd, DEFAULT_RENDERER_VALUE, RENDERER_X + RENDERER_TEXT_WIDTH, RENDERER_Y, RENDERER_BOX_WIDTH, 300);
        _hSampleRateDropdown = MakeSampleRateDropdown(hWnd, DEFAULT_SAMPLE_RATE_VALUE, SAMPLE_RATE_X + SAMPLE_RATE_TEXT_WIDTH, SAMPLE_RATE_Y, SAMPLE_RATE_WIDTH, 300);
        _hControllerConfigDropdown = MakeControllerConfigDropdown((WCHAR*)ZXIAN_CONTROLLER_CONFIG_DIR, hWnd, DEFAULT_CONTROLLER_CONFIG_VALUE, CONTROLLER_X + CONTROLLER_CONFIG_LABEL_TEXT_WIDTH, CONTROLLER_Y + 3, CONTROLLER_BOX_WIDTH, 400);
        _hHistoryDropdown = MakeHistoryDropdown(hWnd, HISTORY_X + HISTORY_LABEL_TEXT_WIDTH, HISTORY_Y, HISTORY_BOX_WIDTH, 400);

        _hFilePathTextbox = MakeFilenameTextBox(hWnd, FILE_PATH_TEXT_BOX_X, FILE_PATH_TEXT_BOX_Y, FILE_PATH_TEXT_BOX_WIDTH, FILE_PATH_TEXT_BOX_HEIGHT);
        _hSilenceCheckbox = MakeCheckbox(hWnd, L"Silence", (HMENU)SILENCE_CHECKBOX_ID, SILENCE_X, SILENCE_Y, SILENCE_WIDTH, 20);
        _hPokeCheckbox = MakeCheckbox(hWnd, L"Show UI", (HMENU)POKE_CHECKBOX_ID, POKE_X, POKE_Y, POKE_WIDTH, 20);
        _hScanlinesCheckbox = MakeCheckbox(hWnd, L"Scanlines", (HMENU)SCANLINES_CHECKBOX_ID, SCANLINES_X, SCANLINES_Y, SCANLINES_WIDTH, 20);
        _hPokeAddressEditbox = MakeEditbox(hWnd, L"32768", (HMENU)POKE_ADDRESS_EDITBOX_ID, POKE_ADDRESS_EDITBOX_X, POKE_ADDRESS_EDITBOX_Y, POKE_ADDRESS_EDITBOX_WIDTH, 20);
        _hPokeValueEditbox = MakeEditbox(hWnd, L"127", (HMENU)POKE_VALUE_EDITBOX_ID, POKE_VALUE_EDITBOX_X, POKE_VALUE_EDITBOX_Y, POKE_VALUE_EDITBOX_WIDTH, 20);
        _hNoFrameskipCheckbox = MakeCheckbox(hWnd, L"No frameskip", (HMENU)NOFRAMESKIP_CHECKBOX_ID, NOFRAMESKIP_X, NOFRAMESKIP_Y, NOFRAMESKIP_WIDTH, 20);
        _hFrameMsEditbox = MakeEditbox(hWnd, L"20", (HMENU)FRAMEMS_EDITBOX_ID, FRAMEMS_EDITBOX_X, FRAMEMS_EDITBOX_Y, FRAMEMS_EDITBOX_WIDTH, 20);

        _hFileGroupbox = MakeGroupbox(hWnd, L"File", (HMENU)FILE_GROUPBOX_ID, FILE_GROUPBOX_X, FILE_GROUPBOX_Y, FILE_GROUPBOX_WIDTH, FILE_GROUPBOX_HEIGHT);
        _hPokeGroupbox = MakeGroupbox(hWnd, L"Pokes", (HMENU)POKE_GROUPBOX_ID, POKE_GROUPBOX_X, POKE_GROUPBOX_Y, POKE_GROUPBOX_WIDTH, POKE_GROUPBOX_HEIGHT);
        _hVideoGroupbox = MakeGroupbox(hWnd, L"Video", (HMENU)VIDEO_GROUPBOX_ID, VIDEO_GROUPBOX_X, VIDEO_GROUPBOX_Y, VIDEO_GROUPBOX_WIDTH, VIDEO_GROUPBOX_HEIGHT);
        _hAudioGroupbox = MakeGroupbox(hWnd, L"Audio", (HMENU)AUDIO_GROUPBOX_ID, AUDIO_GROUPBOX_X, AUDIO_GROUPBOX_Y, AUDIO_GROUPBOX_WIDTH, AUDIO_GROUPBOX_HEIGHT);
        _hControllerConfigGroupBox = MakeGroupbox(hWnd, L"Controller", (HMENU)CONTROLLER_GROUPBOX_ID, CONTROLLER_GROUPBOX_X, CONTROLLER_GROUPBOX_Y, CONTROLLER_GROUPBOX_WIDTH, CONTROLLER_GROUPBOX_HEIGHT);

        if (!_hBrowseButton || 
            !_hRunZxianButton || 
            !_hFilePathTextbox || 
            !_hZoomDropdown || 
            !_hSilenceCheckbox || 
            !_hPokeCheckbox || 
            !_hDisplayModeDropdown || 
            !_hTimerDropdown || 
            !_hScanlinesCheckbox ||
            !_hPokeGroupbox || 
            !_hPokeAddressEditbox ||
            !_hPokeValueEditbox ||
            !_hVideoGroupbox ||
            !_hAudioGroupbox || 
            !_hControllerConfigGroupBox ||
            !_hNoFrameskipCheckbox ||
            !_hFrameMsEditbox ||
            !_hRendererDropdown ||
            !_hSampleRateDropdown ||
            !_hClearButton ||
            !_hControllerConfigDropdown || 
            !_hFileGroupbox || 
            !_hHistoryDropdown
            ) {

            PostQuitMessage(0);
            return 0;
        }

        CheckDlgButton(hWnd, NOFRAMESKIP_CHECKBOX_ID, BST_CHECKED);

        // if there exists a history entry, then populate it into the file text box
        int entriesCount;
        const char** entries = history_get_entries(&entriesCount);
        if (entriesCount > 0) {
            // copy into our variable, converting ASCII to wide
            size_t convertedBytes;
            mbstowcs_s(&convertedBytes, _szBrowsedFilePath, MAX_PATH + 1, entries[0], strlen(entries[0]) + 1);
            HandleBrowsedFileChanged(hWnd);
        }

        // set text
        //SendMessageW(_hPokeAddressEditbox, WM_SETTEXT, 0, (LPARAM)L"32768");
    }
    break;
    case WM_COMMAND:
    {
        // process user actions

        TryHandleClearButton(hWnd, lParam);
        TryHandleBrowseButton(hWnd, lParam);
        TryHandleRunZxianButton(hWnd, lParam);
        TryHandleZoomChange(hWnd, lParam);
        TryHandleTimerDropdownChange(hWnd, lParam);
        TryHandleSampleRateDropdownChange(hWnd, lParam);
        TryHandleDisplayModeChange(hWnd, lParam);
        TryHandleSilenceCheckboxChange(hWnd, wParam, lParam);
        TryHandlePokeCheckboxChange(hWnd, wParam, lParam);
        TryHandleScanlinesCheckboxChange(hWnd, wParam, lParam);
        TryHandleNoFrameskipCheckboxChange(hWnd, wParam, lParam);
        TryHandleRendererDropdownChange(hWnd, lParam);
        TryHandleControllerConfigDropdownChange(hWnd, lParam);

        TryHandleHistoryChange(hWnd, wParam, lParam);
    }
    break;
    case WM_PAINT:
    {
        // window is being drawn

        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hWnd, &ps);

        // custom rendering

        TCHAR info[] = L"Further configuration is accessible only via zxian command line";
        TextOut(hdc, WINDOW_PADDING, WINDOW_HEIGHT - WINDOW_PADDING - 45, info, (int)_tcslen(info));

        TCHAR zoomLabel[] = L"Zoom";
        TextOut(hdc, ZOOM_X, ZOOM_Y + 3, zoomLabel, (int)_tcslen(zoomLabel));

        TCHAR displayModeLabel[] = L"Display Mode";
        TextOut(hdc, DISPLAY_MODE_X, DISPLAY_MODE_Y + 3, displayModeLabel, (int)_tcslen(displayModeLabel));

        TCHAR timerLabel[] = L"Timer";
        TextOut(hdc, TIMER_X, TIMER_Y + 3, timerLabel, (int)_tcslen(timerLabel));

        TCHAR pokeAddressLabel[] = L"Address";
        TextOut(hdc, POKE_ADDRESS_LABEL_X, POKE_ADDRESS_EDITBOX_Y + 3, pokeAddressLabel, (int)_tcslen(pokeAddressLabel));

        TCHAR pokeValueLabel[] = L"Value";
        TextOut(hdc, POKE_VALUE_LABEL_X, POKE_VALUE_EDITBOX_Y + 3, pokeValueLabel, (int)_tcslen(pokeValueLabel));

        TCHAR frameMsLabel[] = L"Frame duration";
        TextOut(hdc, FRAMEMS_LABEL_X, FRAMEMS_EDITBOX_Y + 3, frameMsLabel, (int)_tcslen(frameMsLabel));
        TCHAR frameMsLabel2[] = L"milliseconds";
        TextOut(hdc, FRAMEMS_LABEL_X + FRAMEMS_EDITBOX_WIDTH + 105, FRAMEMS_EDITBOX_Y + 2, frameMsLabel2, (int)_tcslen(frameMsLabel2));
        TCHAR frameMsLabel3[] = L"(for very short durations, use SDL_Timer)";
        TextOut(hdc, FRAMEMS_LABEL_X, FRAMEMS_EDITBOX_Y + 22, frameMsLabel3, (int)_tcslen(frameMsLabel3));
        TCHAR frameMsLabel4[] = L"(for very long durations, a lower audio sample rate may be necessary)";
        TextOut(hdc, FRAMEMS_LABEL_X, FRAMEMS_EDITBOX_Y + 41, frameMsLabel4, (int)_tcslen(frameMsLabel4));

        TCHAR rendererLabel[] = L"Renderer";
        TextOut(hdc, RENDERER_X, RENDERER_Y + 3, rendererLabel, (int)_tcslen(rendererLabel));

        TCHAR sampleRateLabel[] = L"Sample rate";
        TextOut(hdc, SAMPLE_RATE_X, SAMPLE_RATE_Y + 3, sampleRateLabel, (int)_tcslen(sampleRateLabel));
        TCHAR sampleRateLabelAdditional[] = L"(if sound is corrupted or inaudible, lower this value)";
        TextOut(hdc, SAMPLE_RATE_X, SAMPLE_RATE_Y + 24, sampleRateLabelAdditional, (int)_tcslen(sampleRateLabelAdditional));

        TCHAR controllerConfigLabel[] = L"Config file";
        TextOut(hdc, CONTROLLER_CONFIG_LABEL_X, CONTROLLER_CONFIG_LABEL_Y, controllerConfigLabel, (int)_tcslen(controllerConfigLabel));

        TCHAR historyLabel[] = L"History";
        TextOut(hdc, HISTORY_LABEL_X, HISTORY_LABEL_Y + 3, historyLabel, (int)_tcslen(historyLabel));

        // done custom rendering

        EndPaint(hWnd, &ps);
    }
    break;
    case WM_DESTROY:
        // window is being closed

        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}
