#include <SDL.h>
#include <stdio.h>
#include <sys\types.h> 
#include <sys\stat.h>

#include <stdlib.h>
#include <windows.h>
#include <sys\timeb.h>

#include "Constants.h"
#include "Sna.h"
#include "Cpu.h"
#include "CpuUtilities.h"
#include "Io.h"
#include "Memory.h"
#include "Log.h"
#include "Notification.h"
#include "Video.h"

#define __SNA_FILE_SIZE (49179)

#define PATH_COMPONENT_MAX_SIZE (256)
static char _driveBuffer[PATH_COMPONENT_MAX_SIZE];
static char _directoryBuffer[PATH_COMPONENT_MAX_SIZE];
static char _filenameBuffer[PATH_COMPONENT_MAX_SIZE];
static char _extensionBuffer[PATH_COMPONENT_MAX_SIZE];

#define FILENAME_AND_EXTENSION_MAX_SIZE (PATH_COMPONENT_MAX_SIZE*3+1)
static char _filenameAndExtensionBuffer[FILENAME_AND_EXTENSION_MAX_SIZE];
static char _saveStateFilenameBuffer[FILENAME_AND_EXTENSION_MAX_SIZE];

#define ABSOLUTE_PATH_MAX_SIZE (4*FILENAME_AND_EXTENSION_MAX_SIZE)
static char _absolutePathBuffer[ABSOLUTE_PATH_MAX_SIZE];

Uint8 _snaInitialized = 0;

static volatile Uint8 _sna_save_scheduled = 0;

#define _DEBUG_BUFFER_SIZE (ABSOLUTE_PATH_MAX_SIZE+256)
static char _debugBuffer[_DEBUG_BUFFER_SIZE];

Uint8 sna_load(char* filename) {
    log_write_string_string("Starting SNA load from file: %s", filename);

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

    if (info.st_size != __SNA_FILE_SIZE) {
        log_write("Error: SNA file must be exactly 49179 bytes long");
        return 0;
    }

    char* file = (Uint8*)malloc(info.st_size);
    if (file == NULL) {
        log_write("Error: unable to allocate memory for SNA file");
        return 0;
    }
    FILE* fp;
    errno_t result = fopen_s(&fp, filename, "rb");
    if (result) {
        free(file);
        log_write("Error: could not open SNA file");
        return 0;
    }
    // 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) {
        fclose(fp);
        free(file);
        log_write("Error: could not read from SNA file");
        return 0;
    }
    fclose(fp);

    // set up registers
    cpu_regs()->I = *((Uint8*)(file + 0x00));
    *cpu_regs()->HL_alt = *((Uint16*)(file + 0x01));
    *cpu_regs()->DE_alt = *((Uint16*)(file + 0x03));
    *cpu_regs()->BC_alt = *((Uint16*)(file + 0x05));
    *cpu_regs()->AF_alt = *((Uint16*)(file + 0x07));
    *cpu_regs()->HL = *((Uint16*)(file + 0x09));
    *cpu_regs()->DE = *((Uint16*)(file + 0x0B));
    *cpu_regs()->BC = *((Uint16*)(file + 0x0D));
    *cpu_regs()->IY = *((Uint16*)(file + 0x0F));
    *cpu_regs()->IX = *((Uint16*)(file + 0x11));

    cpu_regs()->IFF = 0;
    Uint8 byte0x13 = *((Uint8*)(file + 0x13));
    if (byte0x13 & 4) {
        cpu_regs()->IFF |= CPU_IFF2_BIT;
        // note: the SNA spec does not mention IFF1 because after loading the SNA
        //       execution is resumed by simulating a RETN instruction, which
        //       has the side effect of copying IFF2 into IFF1
    }

    cpu_regs()->R = *((Uint8*)(file + 0x14));
    *cpu_regs()->AF = *((Uint16*)(file + 0x15));
    cpu_regs()->SP = *((Uint16*)(file + 0x17));

    Uint8 interruptMode = *((Uint8*)(file + 0x19));
    switch (interruptMode) {
    case 0:
        cpu_set_interrupt_mode(Mode0);
        break;
    case 1:
        cpu_set_interrupt_mode(Mode1);
        break;
    case 2:
        cpu_set_interrupt_mode(Mode2);
        break;
    default:
        log_write("Error: SNA file must specify 0, 1, or 2 for interrupt mode (byte at offset 0x19)");
        free(file);
        return 0;
    }

    Uint8 borderColour = *((Uint8*)(file + 0x1A));
    io_write8_16bitaddr(ULA_PORT_FE, borderColour);

    memory_load(file + 0x1B, 16384, SPECTRUM_RAM_SIZE);
    free(file);
    log_write("Loaded SNA into ZX Spectrum memory");

    // simulate retn
    Uint16 branchTarget = cpu_pop16();
    cpu_regs()->PC = branchTarget;
    if (cpu_regs()->IFF & CPU_IFF2_BIT) cpu_regs()->IFF |= CPU_IFF1_BIT; else cpu_regs()->IFF &= ~CPU_IFF1_BIT;
    log_write("Performed a retn to resume SNA");

    return 1;
}

// consumer does not own pointer
//
char* _sna_get_filename_for_save() {
    SYSTEMTIME time;
    GetSystemTime(&time);
    sprintf_s(_saveStateFilenameBuffer, FILENAME_AND_EXTENSION_MAX_SIZE - 5, "%s_%04d-%02d-%02d__%02d-%02d-%02d.%03d.sna", 
        _filenameAndExtensionBuffer, 
        time.wYear, 
        time.wMonth, 
        time.wDay, 
        time.wHour, 
        time.wMinute, 
        time.wSecond, 
        time.wMilliseconds);

    return _saveStateFilenameBuffer;
}

Uint8 sna_handle_save() {
    if (!_sna_save_scheduled) {
        return 0;
    }

    _sna_save_scheduled = 0;

    char* filename = _sna_get_filename_for_save();

    log_write("Starting SNA save");
    log_write(filename);
        
    char* file = (Uint8*)malloc(__SNA_FILE_SIZE);
    if (file == NULL) {
        log_write("Error: unable to allocate memory for SNA file");
        return 0;
    }
    FILE* fp;
    errno_t result = fopen_s(&fp, filename, "wb");
    if (result) {
        free(file);
        log_write("Error: could not open SNA file for saving");
        return 0;
    }

    // save existing word immediately above stack
    Uint8 spMinus1 = memory_read8(cpu_regs()->SP - 1);
    Uint8 spMinus2 = memory_read8(cpu_regs()->SP - 2);

    // save PC to stack
    cpu_push_PC();

    // write registers to file buffer
    *((Uint8*)(file + 0x00)) = cpu_regs()->I;
    *((Uint16*)(file + 0x01)) = *cpu_regs()->HL_alt;
    *((Uint16*)(file + 0x03)) = *cpu_regs()->DE_alt;
    *((Uint16*)(file + 0x05)) = *cpu_regs()->BC_alt;
    *((Uint16*)(file + 0x07)) = *cpu_regs()->AF_alt;
    *((Uint16*)(file + 0x09)) = *cpu_regs()->HL;
    *((Uint16*)(file + 0x0B)) = *cpu_regs()->DE;
    *((Uint16*)(file + 0x0D)) = *cpu_regs()->BC;
    *((Uint16*)(file + 0x0F)) = *cpu_regs()->IY;
    *((Uint16*)(file + 0x11)) = *cpu_regs()->IX;

    *((Uint8*)(file + 0x13)) = cpu_regs()->IFF | CPU_IFF2_BIT;
    if (cpu_regs()->IFF & CPU_IFF1_BIT) {
        *((Uint8*)(file + 0x13)) = 4;   // bit 2 of byte 0x13 is set when IFF is set
    }
    else {
        *((Uint8*)(file + 0x13)) = 0;
    }

    *((Uint8*)(file + 0x14)) = cpu_regs()->R;
    *((Uint16*)(file + 0x15)) = *cpu_regs()->AF;
    *((Uint16*)(file + 0x17)) = cpu_regs()->SP;

    *((Uint8*)(file + 0x19)) = cpu_get_interrupt_mode();
    *((Uint8*)(file + 0x1A)) = io_read8_border_colour();

    // dump memory into file bufer
    memory_dump((Uint8*)(file + 0x1B), 16 * 1024, 48 * 1024);

    // restore SP and saved word
    cpu_regs()->SP += 2;
    memory_write8(cpu_regs()->SP - 1, spMinus1);
    memory_write8(cpu_regs()->SP - 2, spMinus2);

    // write file buffer to file
    size_t blocks_written = fwrite(file, __SNA_FILE_SIZE, 1, fp);
    if (blocks_written != 1) {
        fclose(fp);
        free(file);
        log_write("Error: could not write to SNA file");
        return 0;
    }
    fclose(fp);

    free(file);
    log_write("Wrote SNA file");

    sprintf_s(_debugBuffer, _DEBUG_BUFFER_SIZE - 10, "Saved SNA snapshot");
    notification_show(_debugBuffer, 1500, video_force_next_frame_full_render, NULL);

    return 1;
}

void sna_keyup(SDL_Keysym key) {
    if (!_snaInitialized) {
        return;
    }

    switch (key.sym) {
    case SDLK_F11:
        _sna_save_scheduled = 1;
        break;
    }
}

void sna_start(char* path) {
    // does not need to be freed, because it returns input buffer on success
    char* resultPtr = _fullpath(
        _absolutePathBuffer,
        path,
        ABSOLUTE_PATH_MAX_SIZE
    );
    if (!resultPtr) {
        sprintf_s(_debugBuffer, _DEBUG_BUFFER_SIZE - 10, "Warning: SNA saving unavailable: could not resolve absolute path of %s", path);
        log_write(_debugBuffer);
        return;
    }

    errno_t result = _splitpath_s(
        _absolutePathBuffer,
        _driveBuffer,
        PATH_COMPONENT_MAX_SIZE - 5,
        _directoryBuffer,
        PATH_COMPONENT_MAX_SIZE - 5,
        _filenameBuffer,
        PATH_COMPONENT_MAX_SIZE - 5,
        _extensionBuffer,
        PATH_COMPONENT_MAX_SIZE - 5);
    if (result) {
        sprintf_s(_debugBuffer, _DEBUG_BUFFER_SIZE - 10, "Warning: SNA saving unavailable: could not parse path %s", _absolutePathBuffer);
        log_write(_debugBuffer);
        return;
    }

    CreateDirectoryA("saved_SNAs", NULL);

    sprintf_s(_filenameAndExtensionBuffer, FILENAME_AND_EXTENSION_MAX_SIZE - 5, "saved_SNAs\\%s", _filenameBuffer);
    sprintf_s(_debugBuffer, _DEBUG_BUFFER_SIZE - 10, "SNA saving directory: %s", "saved_SNAs");
    log_write(_debugBuffer);

    _snaInitialized = 1;
}

void sna_destroy() {
    if (!_snaInitialized) {
        return;
    }
}