#include <Windows.h>
#include <SDL.h>
#include <SDL_timer.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys\types.h> 
#include <sys\stat.h>
#include <sys\timeb.h>
#include <time.h>

#include "Constants.h"
#include "Timing.h"
#include "Video.h"
#include "Memory.h"
#include "Cpu.h"
#include "Tests.h"
#include "Io.h"
#include "Keyboard.h"
#include "Kempston.h"
#include "Log.h"
#include "SoundBuffer.h"
#include "Sound.h"
#include "Statistics.h"
#include "EventProgrammer.h"
#include "SaveStates.h"
#include "Sna.h"
#include "Z80.h"
#include "TapLoader.h"
#include "TapUi.h"
#include "PokeUi.h"
#include "Notification.h"
#include "Screenshots.h"
#include "Rewind.h"
#include "GameController.h"
#include "Contention.h"
#include "Main.h"

static Uint16 _framems = DEFAULT_FRAME_MS;
static Uint8 _zoom = DEFAULT_ZOOM;
static Uint8 _soundOn = 1;

#define DONT_GENERATE_MNEMONICS 0
#define GENERATE_MNEMONICS 1

#define _DEBUG_BUFFER_SIZE 1024
static char _debugBuffer[_DEBUG_BUFFER_SIZE];

Uint8 try_read_int_argument(int argc, char* argv[], char* argName, Sint64* result) {
    for (int i = 0; i < argc - 1; i++) {
        if (!strcmp(argv[i], argName)) {
            *result = atoi(argv[i + 1]);
            return 1;
        }
    }

    return 0;
}

Uint8 try_read_double_argument(int argc, char* argv[], char* argName, double* result) {
    for (int i = 0; i < argc - 1; i++) {
        if (!strcmp(argv[i], argName)) {
            *result = atof(argv[i + 1]);
            return 1;
        }
    }

    return 0;
}

Uint8 try_find_argument(int argc, char* argv[], char* argName) {
    for (int i = 0; i < argc; i++) {
        if (!strcmp(argv[i], argName)) {
            return 1;
        }
    }

    return 0;
}

void rom_load() {
    struct stat info;
    const char* filename = "data\\48k.bin";
    log_write_string_string("Starting ROM load from file: %s", (char*)filename);

    if (stat(filename, &info) != 0) {
        log_write("Error: ROM file not found");
        return;
    }

    char* file = (Uint8*)malloc(info.st_size);
    if (file == NULL) {
        log_write("Error: unable to allocate memory for ROM file");
        return;
    }
    FILE* fp;
    errno_t result = fopen_s(&fp, filename, "rb");
    if (result) {
        log_write("Error: could not open ROM file");
        return;
    }
    // try to read a single block of info.st_size bytes
    size_t blocks_read = fread(file, info.st_size, 1, fp);
    if (blocks_read != 1) {
        log_write("Error: could not read from SNA file");
        return;
    }
    fclose(fp);

    memory_load(file, 0, SPECTRUM_ROM_SIZE);
    free(file);
    log_write("Loaded ROM into ZX Spectrum memory");
    int romHash = memory_compute_hash(0, SPECTRUM_ROM_SIZE);
    log_write_string_int("ROM hash is %d", romHash);
}

void _cpu_on_tstates_elapsed_callback(Uint64 tstatesTotal) {
    sound_add_sample(io_is_speaker_active(), tstatesTotal);
}

void _cpu_on_instruction_executed_callback___stats_and_tap(struct instruction* instruction, Uint64 tstatesTotal) {
    statistics_record(instruction);
    taploader_on_instruction_tstates_elapsed(instruction);
}

void _cpu_on_instruction_executed_callback___tap(struct instruction* instruction, Uint64 tstatesTotal) {
    taploader_on_instruction_tstates_elapsed(instruction);
}

void _cpu_on_instruction_executed_callback___stats_and_border_effect(struct instruction* instruction, Uint64 tstatesTotal) {
    taploader_on_instruction_tstates_elapsed__tap_independent_border_effect(instruction);
    statistics_record(instruction);
}

void _cpu_on_instruction_executed_callback___border_effect_only(struct instruction* instruction, Uint64 tstatesTotal) {
    taploader_on_instruction_tstates_elapsed__tap_independent_border_effect(instruction);
}

Uint8 _cpu_start_tape_notification_cancel_callback() {
    // hide notification as soon as the user starts tape
    struct tapRunState state = taploader_get_state();
    if (state.type != Stopped) {
        return 1;
    }

    return 0;
}

void _initialize_after_tape_loaded(char* tapFile, Uint8 autoplayTape) {
    // even when we're loading a tape, and not a snapshot,
    // we still initialize snapshot modules because they might
    // have to save a snapshot
    savestates_start(tapFile);
    sna_start(tapFile);

    // z80_start is called with the file name of the tap file because any downstream
    // files created (e.g. z80 snapshots via keystrokes) by the z80 module must be based 
    // on the name of the tap file, and not of the tape initializer snapshot
    z80_load("data\\tapeinit.z80");
    z80_start(tapFile);

    int romHash = memory_compute_hash(0, SPECTRUM_ROM_SIZE);
    if (romHash == SINCLAIR_48K_ROM__CHECKSUM) {
        // when we're using a known ROM
        // poke BASIC system variables to behave as if a random amount of time passed 
        // before LOAD "" was issued and that RANDOMIZE was executed at some point

        // "SEED" system variable (2 bytes)
        Uint16 seed = rand() % 65536;
        memory_write16(0x5c76, seed);

        // "FRAMES" system variable (3 bytes)
        Uint16 frames = 300 + rand() % 1000; // between 6 and 26 seconds' worth of "user delay" after power-on
        memory_write16(0x5c78, frames);
        memory_write8(0x5c78 + 2, 0);   // MSbyte of 3 bytes
    }

    screenshots_start(tapFile);

    if (autoplayTape) {
        taploader_press_play(1);
    }
    else {
        notification_show("Start tape via controls at the bottom", 5000, NULL, _cpu_start_tape_notification_cancel_callback);
    }
}

int main(int argc, char* argv[]) {
    char* snaFile = NULL;
    char* tapFile = NULL;
    Uint8 snaFound = 0;
    Uint8 tapFound = 0;
    srand((unsigned int)time(NULL));

    // since we seed based on current system time, the first random number
    // we generate is predictably increasing each second
    // by generating (and throwing away) this first number, we can no longer use
    // the predictable first number in any capacity
    Uint16 throwaway = rand();

    SDL_Log("");
    SDL_Log("zxian v34 - a ZX Spectrum emulator written by Sebastian Mihai");

#ifdef _DEBUG
    HWND windowHandle = GetConsoleWindow();
    ShowWindow(windowHandle, SW_SHOW);
#endif // _DEBUG

    SDL_Log("Usage:");
    SDL_Log("         zxian -switch value");
    SDL_Log("");
    SDL_Log("Switches:");
    SDL_Log("  -sna <snapshot>     loads the specified snapshot file");
    SDL_Log("                      example:    -sna manicminer.z80");
    SDL_Log("                      example:    -sna manicminer.sna");
    SDL_Log("  -tap <TAP file>     loads the specified TAP file");
    SDL_Log("                      example:    -tap rastan.tap");
    SDL_Log("  -autoplaytape       plays loaded tape on startup automatically");
    SDL_Log("  -zoom <integer>     enlarges window by an integer factor");
    SDL_Log("                      example:    -zoom 2");
    SDL_Log("  -silence            silences sounds");
    SDL_Log("  -soundvolume        set sound volume (0 to 255)");
    SDL_Log("  -noframeskip        disables automatic frame skipping");
    SDL_Log("  -scanlines          simulates a CRT-like scanline effect");
    SDL_Log("  -autohideui         automatically hides UIs until mouse hovers over");
    SDL_Log("  -displaymode <integer>");
    SDL_Log("                      sets the display mode; exit fullscreen via ALT+F4");
    SDL_Log("                      0=windowed, 1=desktop fullscreen, 2=fullscreen");
    SDL_Log("                      example:    -displaymode 1");
    SDL_Log("                      Note: fullscreen modes may not work correctly");
    SDL_Log("                            if zoom is too high for the supported");
    SDL_Log("                            monitor resolutions");
    SDL_Log("  -poke               enables the poke UI, which can modify memory");
    SDL_Log("  -pokeaddr <integer> sets initial address and enables the poke UI");
    SDL_Log("  -pokeval <integer>  sets initial value and enables the poke UI");
    SDL_Log("  -framems <integer>  sets the duration of a video frame in milliseconds");
    SDL_Log("                      default is 20 (since PAL video standard is 50fps)");
    SDL_Log("                      minimum is 1, maximum is 5000");
    SDL_Log("                      example:    -framems 15");
    SDL_Log("                      for values lower than 20, you should also specify");
    SDL_Log("                          -videomode 1");
    SDL_Log("                      for values higher than 20, lower sound sample rate");
    SDL_Log("  -controllerconfig <config file>");
    SDL_Log("                      loads the specified controller config file");
    SDL_Log("                      example:");
    SDL_Log("                            -controllerconfig .\\controller\\kempston.cfg");
    SDL_Log("");
    SDL_Log("Advanced switches, video:");
    SDL_Log("  -frameskipallowance <integer>");
    SDL_Log("                      percent of frame duration which triggers frameskip");
    SDL_Log("                      example:    -frameskipallowance 3");
    SDL_Log("                      (waits for 103%% frame duration before skipping)");
    SDL_Log("  -videomode <integer>");
    SDL_Log("                      sets method of driving video");
    SDL_Log("                      0=QueueTimer-based, 1=SDL_Timer-based");
    SDL_Log("                      example:    -videomode 0");
    SDL_Log("                      QueueTimer is default and is very precise");
    SDL_Log("                      SDL_Timer for very short video frame durations");
    SDL_Log("  -renderer <integer>");
    SDL_Log("                      selects the SDL renderer to be used");
    SDL_Log("                      0=software, 1=accelerated");
    SDL_Log("                      example:    -renderer 0");
    SDL_Log("");
    SDL_Log("Advanced switches, sound:");
    SDL_Log("  -soundmode <integer>");
    SDL_Log("                      how sound is sampled with respect to frame length");
    SDL_Log("                      0=auto, 1=static, 2=dynamic");
    SDL_Log("                      example:    -soundmode 0");
    SDL_Log("  -soundsamples <integer>");
    SDL_Log("                      sample rate per second");
    SDL_Log("                      example:    -soundsamples 96000");
    SDL_Log("  -soundlogresyncs    logs a message every time a resync occurs");
    SDL_Log("");
    SDL_Log("Advanced switches, sound sync:");
    SDL_Log("Note: many of these are inter-dependent; changing one will likely");
    SDL_Log("      require you to change others");
    SDL_Log("  -soundbuffer <float>");
    SDL_Log("                      buffer size in milliseconds (ms)");
    SDL_Log("                      example:    -soundbuffer 200.0");
    SDL_Log("  -soundsyncmindistance <float>");
    SDL_Log("                      min distance for read head behind write head (ms)");
    SDL_Log("                      example:    -soundsyncmindistance 5.0");
    SDL_Log("  -soundsyncmaxdistance <float>");
    SDL_Log("                      max distance for read head behind write head (ms)");
    SDL_Log("                      example:    -soundsyncmaxdistance 150.0");
    SDL_Log("  -soundsyncrewindamount <float>");
    SDL_Log("                      adjustment amount for when read is too fast (ms)");
    SDL_Log("                      example:    -soundsyncrewindamount 3.0");
    SDL_Log("  -soundsyncfastforwardamount <float>");
    SDL_Log("                      adjustment amount for when read is too slow (ms)");
    SDL_Log("                      example:    -soundsyncfastforwardamount 4.0");
    SDL_Log("");
    SDL_Log("Advanced switches, miscellaneous:");
    SDL_Log("  -instructionlog     logs every instruction executed by");
    SDL_Log("                      the CPU; produces VERY large log files");

#ifdef _____DISABLE_DEBUG_FUNCTIONALITY
    SDL_Log(" (SWITCH UNAVAILABLE, COMPILE WITHOUT _____DISABLE_DEBUG_FUNCTIONALITY)");
#endif

    SDL_Log("  -instructionstats   logs run-time instruction statistics");
    SDL_Log("                      such as instruction frequency, etc.");

#ifdef _____DISABLE_DEBUG_FUNCTIONALITY
    SDL_Log(" (SWITCH UNAVAILABLE, COMPILE WITHOUT _____DISABLE_DEBUG_FUNCTIONALITY)");
#endif

    log_start();

#ifdef _____DISABLE_DEBUG_FUNCTIONALITY
    log_write("Compiled with _____DISABLE_DEBUG_FUNCTIONALITY");
#endif

    // log all arguments
    _debugBuffer[0] = 0;    // empty string
    strcat_s(_debugBuffer, _DEBUG_BUFFER_SIZE - 10, "Command line: ");
    for (int i = 0; i < argc; i++) {
        strcat_s(_debugBuffer, _DEBUG_BUFFER_SIZE - 10, argv[i]);
        strcat_s(_debugBuffer, _DEBUG_BUFFER_SIZE - 10, " ");
    }
    log_write(_debugBuffer);

    log_write("Version: v34");

    // find SNA
    for (int i = 0; i < argc - 1; i++) {
        if (!strcmp(argv[i], "-sna")) {
            snaFile = argv[i + 1];
            snaFound = 1;
        }
    }
    if (!snaFound) {
        // find TAP only when sna was not found
        for (int i = 0; i < argc - 1; i++) {
            if (!strcmp(argv[i], "-tap")) {
                tapFile = argv[i + 1];
                tapFound = 1;
            }
        }
    }

    // find -poke
    Uint8 pokeFound = try_find_argument(argc, argv, "-poke");

    // find -pokeaddr
    Sint64 pokeAddr;
    Uint8 pokeAddrFound = try_read_int_argument(argc, argv, "-pokeaddr", &pokeAddr);
    if (pokeAddrFound) {
        pokeui_set_address((Uint16)pokeAddr);
    }

    // find -pokeval
    Sint64 pokeVal;
    Uint8 pokeValFound = try_read_int_argument(argc, argv, "-pokeval", &pokeVal);
    if (pokeValFound) {
        pokeui_set_value((Uint8)pokeVal);
    }

    Uint8 enablePokeUi = pokeFound || pokeAddrFound || pokeValFound;

    // find -scanlines
    Uint8 scanlinesFound = try_find_argument(argc, argv, "-scanlines");
    if (scanlinesFound) {
        video_enable_scanline_effect(1);
    }

    // find -autohideui
    Uint8 autohideUiIsFound = try_find_argument(argc, argv, "-autohideui");

    // find zoom
    for (int i = 0; i < argc - 1; i++) {
        if (!strcmp(argv[i], "-zoom")) {
            _zoom = atoi(argv[i + 1]);
            if (_zoom < 1 || _zoom > MAX_ZOOM) {
                _zoom = DEFAULT_ZOOM;
            }
        }
    }

    // find -soundvolume
    Uint8 volume = DEFAULT_VOLUME;
    Sint64 soundvolumeVal;
    Uint8 soundvolumeValFound = try_read_int_argument(argc, argv, "-soundvolume", &soundvolumeVal);
    if (soundvolumeValFound && soundvolumeVal >= 0 && soundvolumeVal <= 255) {
        volume = (Uint8)soundvolumeVal;
    }

    // find framems
    for (int i = 0; i < argc - 1; i++) {
        if (!strcmp(argv[i], "-framems")) {
            _framems = atoi(argv[i + 1]);
            if (_framems < 1 || _framems > 5000) {
                log_write("Warning: -framems must be between 1 and 5000. Using default.");
                _framems = DEFAULT_FRAME_MS;
            }
        }
    }

    // find silence
    for (int i = 0; i < argc; i++) {
        if (!strcmp(argv[i], "-silence")) {
            _soundOn = 0;
        }
    }

    Uint8 instructionLog = 0;
    // find instructionlog
    for (int i = 0; i < argc; i++) {
        if (!strcmp(argv[i], "-instructionlog")) {
#ifndef _____DISABLE_DEBUG_FUNCTIONALITY
            instructionLog = 1;
#endif
        }
    }

    Uint8 instructionStats = 0;
    // find instructionStats
    for (int i = 0; i < argc; i++) {
        if (!strcmp(argv[i], "-instructionstats")) {
#ifndef _____DISABLE_DEBUG_FUNCTIONALITY
            instructionStats = 1;
#endif
        }
    }

    Uint8 noFrameskip = 0;
    // find noframeskip
    for (int i = 0; i < argc; i++) {
        if (!strcmp(argv[i], "-noframeskip")) {
            noFrameskip = 1;
        }
    }

    // find frameskipallowance
    Uint8 lagAllowanceBeforeFrameskip;
    Uint8 lagAllowanceBeforeFrameskipFound = 0;
    for (int i = 0; i < argc - 1; i++) {
        if (!strcmp(argv[i], "-frameskipallowance")) {
            lagAllowanceBeforeFrameskip = atoi(argv[i + 1]);
            if (lagAllowanceBeforeFrameskip >= 0) {
                lagAllowanceBeforeFrameskipFound = 1;
            }
        }
    }

    // find soundmode
    enum soundMode soundMode;
    Uint8 soundModeFound = 0;
    for (int i = 0; i < argc - 1; i++) {
        if (!strcmp(argv[i], "-soundmode")) {
            Uint8 tmp = atoi(argv[i + 1]);

            switch (tmp) {
            case Auto:
            case Static:
            case Dynamic:
                soundMode = tmp;
                soundModeFound = 1;
                break;
            }
        }
    }

    // find videomode
    enum videoMode videoMode = Video_viaQueueTimer;  // default
    for (int i = 0; i < argc - 1; i++) {
        if (!strcmp(argv[i], "-videomode")) {
            Uint8 tmp = atoi(argv[i + 1]);

            switch (tmp) {
            case Video_viaQueueTimer:
            case Video_viaSDLTimer:
                videoMode = tmp;
                break;
            }
        }
    }

    // find displaymode
    enum videoDisplayMode displayMode = Video_DisplayMode_Windowed;  // default
    for (int i = 0; i < argc - 1; i++) {
        if (!strcmp(argv[i], "-displaymode")) {
            Uint8 tmp = atoi(argv[i + 1]);

            switch (tmp) {
            case Video_DisplayMode_Windowed:
            case Video_DisplayMode_DesktopFullscreen:
            case Video_DisplayMode_Fullscreen:
                displayMode = tmp;
                break;
            }
        }
    }

    soundbuffer_set_buffer_default(_framems);

    // find -soundbuffer
    double soundBufferSizeMs;
    Uint8 soundBufferSizeFound = try_read_double_argument(argc, argv, "-soundbuffer", &soundBufferSizeMs);
    if (soundBufferSizeFound) {
        soundbuffer_override_buffer_size_MS(soundBufferSizeMs);
    }

    // find soundsamples
    Uint32 soundSamplesPerSecond = SOUND_DEFAULT_SAMPLE_RATE_PER_SECOND;
    for (int i = 0; i < argc - 1; i++) {
        if (!strcmp(argv[i], "-soundsamples")) {
            Sint64 tmp = atoi(argv[i + 1]);
            if (tmp >= 0) {
                soundSamplesPerSecond = (Uint32)tmp;
            }
        }
    }
    soundbuffer_preinit(soundSamplesPerSecond);

    // find -soundsyncmindistance
    double soundSyncMinDistance;
    Uint8 soundSyncMinDistanceFound = try_read_double_argument(argc, argv, "-soundsyncmindistance", &soundSyncMinDistance);
    if (soundSyncMinDistanceFound) {
        soundbuffer_override_min_trailing_distance_MS(soundSyncMinDistance);
    }

    // find -soundsyncmaxdistance
    double soundSyncMaxDistance;
    Uint8 soundSyncMaxDistanceFound = try_read_double_argument(argc, argv, "-soundsyncmaxdistance", &soundSyncMaxDistance);
    if (soundSyncMaxDistanceFound) {
        soundbuffer_override_max_trailing_distance_MS(soundSyncMaxDistance);
    }

    // find -soundsyncrewindamount
    double soundSyncRewindAmount;
    Uint8 soundSyncRewindAmountFound = try_read_double_argument(argc, argv, "-soundsyncrewindamount", &soundSyncRewindAmount);
    if (soundSyncRewindAmountFound) {
        soundbuffer_override_rewind_amount_MS(soundSyncRewindAmount);
    }

    // find -soundsyncfastforwardamount
    double soundSyncFastForwardAmount;
    Uint8 soundSyncFastForwardAmountFound = try_read_double_argument(argc, argv, "-soundsyncfastforwardamount", &soundSyncFastForwardAmount);
    if (soundSyncFastForwardAmountFound) {
        soundbuffer_override_fastforward_amount_MS(soundSyncFastForwardAmount);
    }

    // find -soundlogresyncs
    Uint8 soundLogResyncs = try_find_argument(argc, argv, "-soundlogresyncs");

    // find -autoplaytape
    Uint8 autoPlayTapeFound = try_find_argument(argc, argv, "-autoplaytape");

    // find -renderer
    Sint64 renderer;
    Uint8 rendererFound = try_read_int_argument(argc, argv, "-renderer", &renderer);
    if (rendererFound) {
        video_set_renderer_type((Uint8)renderer);
    }

    // find -controllerconfig
    char* controllerConfigFile = NULL;
    for (int i = 0; i < argc - 1; i++) {
        if (!strcmp(argv[i], "-controllerconfig")) {
            controllerConfigFile = argv[i + 1];
            game_controller_configure_overrides_from_file(controllerConfigFile);
        }
    }

    timing_start(_framems);

    statistics_start();
    keyboard_start();
    kempston_start();
    io_start();
    memory_start(video_handle_memory_write);
    // ROM must be loaded immediately after memory start
    rom_load();

    taploader_start();
    game_controller_start();

    // these are used to test video rendering by loading a screen and then entering
    // an infinite loop
    //tests_screen_load();
    //tests_rom_load();

    // the unit tests exercise parsing of instruction opcodes and the execution
    // tests are used to test one or few instructions at a time
    //tests_run_unit_tests();
    //tests_run_execution_tests();

    if (_soundOn) {
        if (soundLogResyncs) {
            soundbuffer_enable_resync_logging();
        }
        sound_start(soundSamplesPerSecond, volume);
        if (soundModeFound) {
            sound_set_user_mode(soundMode);
        }
    }

#ifdef _____DISABLE_DEBUG_FUNCTIONALITY
#define CPU_STATISTICS_CALLBACK_WITH_TAP _cpu_on_instruction_executed_callback___tap
#define CPU_STATISTICS_CALLBACK_WITHOUT_TAP _cpu_on_instruction_executed_callback___border_effect_only
#else
#define CPU_STATISTICS_CALLBACK_WITH_TAP _cpu_on_instruction_executed_callback___stats_and_tap
#define CPU_STATISTICS_CALLBACK_WITHOUT_TAP _cpu_on_instruction_executed_callback___stats_and_border_effect
#endif

    func_cpu_instruction_executed_callback* callback = CPU_STATISTICS_CALLBACK_WITHOUT_TAP;
    if (tapFound) {
        callback = CPU_STATISTICS_CALLBACK_WITH_TAP;
    }

    contention_start();
    cpu_start(
        instructionLog, 
        callback, 
        _cpu_on_tstates_elapsed_callback, 
        contention_get_contention_for_inflight_instruction, 
        DONT_GENERATE_MNEMONICS);

    if (snaFound) {
        Uint8 snaLoadedSuccessfully = 0;
        char* extension = strrchr(snaFile, '.');
        if (extension) {
            // make lowercase
            char* p = extension;
            while (*p = tolower(*p)) ++p;
            if (!strcmp(extension, ".sna")) {
                snaLoadedSuccessfully = sna_load(snaFile);
            }
            else if (!strcmp(extension, ".z80")) {
                snaLoadedSuccessfully = z80_load(snaFile);
            }
        }

        if (snaLoadedSuccessfully) {
            savestates_start(snaFile);
            sna_start(snaFile);
            z80_start(snaFile);
            screenshots_start(snaFile);
        }
        else {
            savestates_start("default");
            sna_start("default");
            z80_start("default");
            screenshots_start("default");
        }
    }

    SDL_Init(SDL_INIT_VIDEO);

    int tapeControlsHeight = 0;
    if (tapFound) {
        tapeControlsHeight = tapui_get_UI_height(_zoom);
    }

    int pokeControlsHeight = 0;
    if (enablePokeUi) {
        pokeControlsHeight = pokeui_get_UI_height(_zoom);
    }

    int playAreaHeight = VISIBLE_SPECTRUM_SCREEN_HEIGHT * _zoom;

    int windowWidth = VISIBLE_SPECTRUM_SCREEN_WIDTH * _zoom;
    int windowHeight = playAreaHeight;

    Uint32 windowFlags = 0;
    SDL_Window* window = SDL_CreateWindow("zxian v34 (by Sebastian Mihai)", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, windowWidth, windowHeight, windowFlags);
    log_write("Created SDL window");

    Uint8 initializationComplete = 1;

    if (!video_start(videoMode, window, _zoom, windowWidth, windowHeight, displayMode)) {
        initializationComplete = 0;
    }
    
    if (initializationComplete) {
        if ( noFrameskip ) {
            video_disable_frameskip();
        }
        if (lagAllowanceBeforeFrameskipFound) {
            video_set_lag_allowance_before_frameskip(lagAllowanceBeforeFrameskip);
        }

        // since video_start() might change the video card display mode, this is the earliest when
        // we can know if we have to offset from the left or top - to keep everything centered
        // during fullscreen, for example
        int displayLeft, displayTop;
        video_get_display_coordinates(&displayLeft, &displayTop);

        notification_start(windowWidth, displayTop, displayLeft, _zoom);
        eventprogrammer_start(_framems);

        if (tapFound) {
            Uint8 tapLoadedSuccessfully = taploader_load(tapFile);
            if (!tapLoadedSuccessfully) {
                savestates_start("default");
                sna_start("default");
                z80_start("default");
                screenshots_start("default");
            }

            // we start the UI irrespective of whether we loaded the tape file
            int tapeUiTop = displayTop + playAreaHeight - tapeControlsHeight;
            tapui_start(window, video_get_renderer(), windowWidth, tapeControlsHeight, tapeUiTop, displayLeft, _zoom, autohideUiIsFound);

            if (tapLoadedSuccessfully) {
                _initialize_after_tape_loaded(tapFile, autoPlayTapeFound);
            }
        }

        if (!tapFound && !snaFound) {
            savestates_start("default");
            sna_start("default");
            z80_start("default");
            screenshots_start("default");
        }

        // savestates have started by now
        rewind_start();

        if (enablePokeUi) {
            int pokeUiTop = displayTop + playAreaHeight - tapeControlsHeight - pokeControlsHeight;
            pokeui_start(window, video_get_renderer(), windowWidth, pokeControlsHeight, pokeUiTop, displayLeft, _zoom, autohideUiIsFound);
        }

        SDL_Event event;

        Uint8 needKeyDownBeforeKeyUp = 0;
        Uint8 isMouseClicked = 0;
        Uint8 firstMouseRepeatTookPlace = 0;
        
        struct timeb lastMouseClickTime;
        ftime(&lastMouseClickTime);

        Uint8 quitRequested = 0;
        while (!quitRequested) {
            SDL_Delay(5);
            while (SDL_PollEvent(&event)) {

                if (event.type == SDL_QUIT) {
                    quitRequested = 1;
                    break;
                }

                if (event.type == SDL_KEYDOWN) {
                    keyboard_keydown(event.key.keysym);
                    kempston_keydown(event.key.keysym);
                    sound_keydown(event.key.keysym);

                    needKeyDownBeforeKeyUp = 0;
                }

                if (event.type == SDL_KEYUP) {
                    keyboard_keyup(event.key.keysym);
                    kempston_keyup(event.key.keysym);

                    // the reason why we require a keydown before we do any state actions
                    // is because SDL will continuously spam the last SDL_KEYUP event even after
                    // the key is no longer pressed, resulting in an infinitely-repeating 
                    // actions such as "load state"
                    if (!needKeyDownBeforeKeyUp) {
                        savestates_keyup(event.key.keysym);
                        sna_keyup(event.key.keysym);
                        z80_keyup(event.key.keysym);
                        screenshots_keyup(event.key.keysym);
                        rewind_keyup(event.key.keysym);

                        needKeyDownBeforeKeyUp = 1;
                    }
                }

                if (event.type == SDL_MOUSEBUTTONUP && event.button.button == SDL_BUTTON_LEFT) {
                    if (isMouseClicked) {
                        tapui_mouseclick();
                        pokeui_mouseclick();
                    }
                    isMouseClicked = 0;
                    firstMouseRepeatTookPlace = 0;
                }

                if (event.type == SDL_MOUSEBUTTONDOWN && event.button.button == SDL_BUTTON_LEFT) {
                    isMouseClicked = 1;
                    pokeui_mousedown();
                    ftime(&lastMouseClickTime);
                }

                if (event.type == SDL_CONTROLLERBUTTONUP) {
                    game_controller_on_button_up(event);
                }

                if (event.type == SDL_CONTROLLERBUTTONDOWN) {
                    game_controller_on_button_down(event);
                }

                if (event.type == SDL_CONTROLLERAXISMOTION) {
                    game_controller_on_controller_axis_motion(event);
                }

                //log_write_string_int("---------------- event.type: %d", event.type);

                /*printf_s((char* const)_debugBuffer, _DEBUG_BUFFER_SIZE - 10,
                    "SDL Event: type=0x%x  %d", event.type, event.window.data1);
                log_write(_debugBuffer);*/
            }

            // handle held down mouse click
            if (isMouseClicked) {
                struct timeb now;
                ftime(&now);
                Uint64 deltaMs = (Uint64)(1000.0 * (now.time - lastMouseClickTime.time)
                    + (now.millitm - lastMouseClickTime.millitm));
                Uint64 threshold = firstMouseRepeatTookPlace ?  MOUSE_CLICK_HELD_DOWN_REPEAT_PERIOD_MS : MOUSE_CLICK_HELD_DOWN_REPEAT_PERIOD_INITIAL_MS;

                if (deltaMs >= threshold) {
                    firstMouseRepeatTookPlace = 1;

                    // simulate mouse click
                    SDL_Event e;
                    e.type = SDL_MOUSEBUTTONDOWN;
                    e.button.button = SDL_BUTTON_LEFT;
                    SDL_PushEvent(&e);
                }
            }
        }
    }

    timing_prepare_destroy();
    log_write("Exit was requested");
    log_write("");
    log_write("RUN REPORTS (all entries pertain to CPU at normal speed, unlesss stated)");
    log_write("");

    // delay these
    sound_destroy();
    SDL_Delay(250);
    video_destroy();
    SDL_Delay(250);
    
    tapui_destroy();
    pokeui_destroy();
    notification_destroy();
    
    eventprogrammer_destroy();

    SDL_DestroyWindow(window);
    SDL_Quit();

    cpu_destroy();
    contention_destroy();
    memory_destroy();
    io_destroy();
    kempston_destroy();
    keyboard_destroy();

    if (instructionStats) {
        statistics_log_all();
    }
    statistics_destroy();

    screenshots_destroy();
    taploader_destroy();
    rewind_destroy();
    savestates_destroy();

    timing_destroy();
    log_destroy();

    return 0;
}
