#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 "Z80.h"
#include "Cpu.h"
#include "CpuUtilities.h"
#include "Io.h"
#include "Memory.h"
#include "Log.h"
#include "Notification.h"
#include "Video.h"


// we always save uncompressed, v1 Z80 files, by dumping the entire RAM
// this means that the file size will always be the same
#define _Z80_V1_HEADER_SIZE (30)
#define _Z80_V1_FILE_SIZE_FOR_SAVING (_Z80_V1_HEADER_SIZE + 48*1024)

#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 _z80Initialized = 0;

static volatile Uint8 _z80_save_scheduled = 0;

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

struct z80FilePropertiesFromV1Header {
    Uint8 isDataBlockCompressed;
};

struct z80FilePropertiesFromV2V3Header {
    Uint16 lengthOfV2V3Header_afterBytes30And31;
};

/// <summary>
///     Get the version number of the Z80 format of the file
///     whose contents are in the input buffer
/// </summary>
/// <returns>
///     0 when version could not be identified
///     1, 2, 3 otherwise
/// </returns>
Uint8 z80_get_z80_file_version(char* file, size_t size) {
    // try to see if it's version 1
    if (size < 30) {
        log_write("Z80: warning - file too small to contain a version 1 header");
        return 0;
    }

    Uint16 pcValueAtOffset6 = *((Uint16*)(file + 6));
    if (pcValueAtOffset6 != 0) {
        return 1;
    }

    // file is not version 1

    // try to see if it's version 2 or 3
    if (size < 32) {
        log_write("Z80: warning - file is too small to contain version 2/3 \"additional header length\" at offset 30-31");
        return 0;
    }

    Uint16 additionalHeaderLength = *((Uint16*)(file + 30));
    switch (additionalHeaderLength)
    {
    case 23:
        // the +2 comes from the fact that bytes 30-31 are not included in the "additional header" length
        if (size < 30 + 23 + 2) {
            // too small
            log_write("Z80: warning - file looks like version 2 (offset 30=23), but is too short");
            return 0;
        }
        // it's version 2
        return 2;
    case 54:
        // the +2 comes from the fact that bytes 30-31 are not included in the "additional header" length
        if (size < 30 + 54 + 2) {
            // too small
            log_write("Z80: warning - file looks like version 3 (offset 30-31 is 54), but is too short");
            return 0;
        }
        // it's version 3
        return 3;
    case 55:
        // the +2 comes from the fact that bytes 30-31 are not included in the "additional header" length
        if (size < 30 + 55 + 2) {
            // too small
            log_write("Z80: warning - file looks like version 3 (offset 30-31 is 55), but is too short");
            return 0;
        }
        // it's version 3
        return 3;
    default:
        log_write("Z80: warning - could not determine file version");
        return 0;
    }
}

// loads the v1 header chunk, which is also
// common to v2 and v3
// input buffer is guaranteed to be long enough to contain a v1 header
struct z80FilePropertiesFromV1Header _z80_load_v1_header_chunk(char* file) {
    struct z80FilePropertiesFromV1Header result = { 
        .isDataBlockCompressed = 0
    };

    cpu_regs()->A = *((Uint8*)(file + 0));
    cpu_regs()->F = *((Uint8*)(file + 1));
    *cpu_regs()->BC = *((Uint16*)(file + 2));
    *cpu_regs()->HL = *((Uint16*)(file + 4));
    cpu_regs()->PC = *((Uint16*)(file + 6));
    cpu_regs()->SP = *((Uint16*)(file + 8));
    cpu_regs()->I = *((Uint8*)(file + 10));    
    cpu_regs()->R = *((Uint8*)(file + 11));

    // when byte 12 is 255, it counts as 1
    Uint8 byte12 = *((Uint8*)(file + 12));
    if (byte12 == 255) {
        byte12 = 1;
    }
    // bit 0: bit 7 of R register
    if (byte12 & (1 << 0)) {
        cpu_regs()->R |= 0b10000000;
    }
    else {
        cpu_regs()->R &= 0b01111111;
    }
    // bit 1-3: border colour
    Uint8 borderColour = (byte12 >> 1) & 0b00000111;
    io_write8_16bitaddr(ULA_PORT_FE, borderColour);
    // bit 5: data is compressed
    if (byte12 & (1 << 5)) {
        result.isDataBlockCompressed = 1;
    }

    *cpu_regs()->DE = *((Uint16*)(file + 13));
    *cpu_regs()->BC_alt = *((Uint16*)(file + 15));
    *cpu_regs()->DE_alt = *((Uint16*)(file + 17));
    *cpu_regs()->HL_alt = *((Uint16*)(file + 19));
    cpu_regs()->A_alt = *((Uint8*)(file + 21));
    cpu_regs()->F_alt = *((Uint8*)(file + 22));
    *cpu_regs()->IY = *((Uint16*)(file + 23));
    *cpu_regs()->IX = *((Uint16*)(file + 25));

    Uint8 iff = *((Uint8*)(file + 27));
    if (iff == 0) {
        cpu_regs()->IFF = 0;
    }
    else {
        cpu_regs()->IFF = CPU_IFF1_BIT | CPU_IFF2_BIT;
    }

    Uint8 iff2 = *((Uint8*)(file + 28));
    if (iff2) {
        cpu_regs()->IFF |= CPU_IFF2_BIT;
    }

    Uint8 byte29 = *((Uint8*)(file + 29));
    Uint8 interruptMode = byte29 & 0b00000011;
    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("Z80: warning - file must specify 0, 1, or 2 for interrupt mode (bits 0-1 of byte at offset 29)");
    }

    return result;
}

// loads the v2/v3 header chunk
// input buffer is guaranteed to be long enough to contain a v2/v3 header
struct z80FilePropertiesFromV2V3Header _z80_load_v2_v3_header_chunk(char* file) {
    struct z80FilePropertiesFromV2V3Header result;

    result.lengthOfV2V3Header_afterBytes30And31 = *((Uint16*)(file + 30));

    cpu_regs()->PC = *((Uint16*)(file + 32));

    return result;
}

// loads data into memory for a v1 Z80 file
Uint8 _z80_load_v1_data_block(char* file, size_t size, struct z80FilePropertiesFromV1Header header) {
    if (!header.isDataBlockCompressed) {
        log_write("Z80: data block is not compressed");
        memory_load(file + 30, 16384, (Uint16)size - 30);
        return 1;
    }
    
    log_write("Z80: data block is compressed");
    Uint16 zxMemoryOffset = 16384;
    size_t offset = 30; // start immediately after v1 header
    while (offset < size) {
        if (offset == size - 4) {
            Uint8 first = file[offset];
            Uint8 second = file[offset + 1];
            Uint8 third = file[offset + 2];
            Uint8 fourth = file[offset + 3];
            // expect the end marker 00 ED ED 00 at the end of data block
            if (first == 0x00 && second == 0xed && third == 0xed && fourth == 0x00) {
                // data block ends with end marker, so we're done
                break;
            }
            else {
                log_write("Z80: warning - data block is compressed but does not end with 00 ED ED 00");
            }
        }

        // ED ED xx yy    expands to    yy yy yy ... yy yy
        //                               \___xx times___/
        if (offset < size - 4) {
            Uint8 first = file[offset];
            Uint8 second = file[offset + 1];
            if (first == 0xed && second == 0xed) {
                // this is an expandable fragment
                Uint8 xx = file[offset + 2];
                for (Uint8 i = 0; i < xx; i++) {
                    // expand it in ZX memory
                    memory_load(file + offset + 3, zxMemoryOffset, 1);
                    zxMemoryOffset++;
                }
                // skip over the 4 bytes of the expandable fragment
                offset += 4;
                continue;
            }
        }

        // we're not at the start of an expandable fragment
        // so this is a normal byte
        memory_load(file + offset, zxMemoryOffset, 1);
        zxMemoryOffset++;
        offset++;
    }

    return 1;
}

/// <returns>0 if lookup failed</returns>
Uint8 _z80_get_48k_page_destination(Uint8 pageNumber, Uint16* pageDestinationOUT) {
    // currently-supported pages are just those ones needed for the ZX Spectrum 48k
    switch (pageNumber)
    {
    case 0:
        *pageDestinationOUT = 0;
        return 1;
    case 4:
        *pageDestinationOUT = 0x8000;
        return 1;
    case 5:
        *pageDestinationOUT = 0xc000;
        return 1;
    case 8:
        *pageDestinationOUT = 0x4000;
        return 1;
    default:
        return 0;
    }
}

/// <summary>
/// Loads a data page into ZX Spectrum memory
/// </summary>
/// <param name="pageData">pointer page data (i.e. after page header)</param>
/// <param name="size">length of page data in file</param>
/// <param name="destination">address in ZX Spectrum where it will be loaded</param>
_z80_load_v2v3_data_page(char* pageData, Uint16 size, Uint16 destination) {
    if (size == 0xffff) {
        // page is not compressed
        memory_load(pageData, destination, size);
    }

    Uint16 zxMemoryOffset = destination;
    size_t offset = 0;
    while (offset < size) {
        // ED ED xx yy    expands to    yy yy yy ... yy yy
        //                               \___xx times___/
        if (offset < size - 4) {
            Uint8 first = pageData[offset];
            Uint8 second = pageData[offset + 1];
            if (first == 0xed && second == 0xed) {
                // this is an expandable fragment
                Uint8 xx = pageData[offset + 2];
                for (Uint8 i = 0; i < xx; i++) {
                    // expand it in ZX memory
                    memory_load(pageData + offset + 3, zxMemoryOffset, 1);
                    zxMemoryOffset++;
                }
                // skip over the 4 bytes of the expandable fragment
                offset += 4;
                continue;
            }
        }

        // we're not at the start of an expandable fragment
        // so this is a normal byte
        memory_load(pageData + offset, zxMemoryOffset, 1);
        zxMemoryOffset++;
        offset++;
    }
}

// loads data into memory for a v2/v3 Z80 file
Uint8 _z80_load_v2v3_data_block(char* file, size_t size, struct z80FilePropertiesFromV2V3Header header) {
    size_t offset = 30; // start immediately after v1 header
    offset += 2;        // skip over bytes 30-31
    offset += header.lengthOfV2V3Header_afterBytes30And31; // skip to immediately after header end

    Uint16 skippedCount = 0;

    log_write("\nZ80: enumerating v2/v3 data pages...");
    log_write("      Offset in File: of where the header begins in the Z80 file");
    log_write("      Length in File: compressed length, when compressed");
    log_write("         Destination: address in ZX Spectrum memory that will host the page");
    log_write("");
    log_write("Offset in File\tPage Number\tDestination\tLength in File\tIs Compressed?");
    log_write("--------------\t-----------\t-----------\t--------------\t--------------");
    while (offset < size) {
        // we are now at the start of a data page entry
        Uint16 lengthOfCompressedData = *((Uint16*)(file + offset));
        Uint8 pageNumber = *((Uint8*)(file + offset + 2));

        Uint16 pageDestination;
        Uint8 pageDestinationLookupSuccess = _z80_get_48k_page_destination(pageNumber, &pageDestination);

        if (pageDestinationLookupSuccess) {
            // since offset points to the start of a data page entry (i.e. its header)
            // the +3 skips over the header, to immediately at the start of page data
            _z80_load_v2v3_data_page(file + offset + 3, lengthOfCompressedData, pageDestination);

            sprintf_s(_debugBuffer, _DEBUG_BUFFER_SIZE - 10,
                "0x%08X\t\t0x%08X\t0x%08X\t0x%08X\t\t%s",
                (Uint32)offset,
                pageNumber,
                pageDestination,
                lengthOfCompressedData,
                lengthOfCompressedData == 0xffff ? "No" : "Yes");
        }
        else {
            sprintf_s(_debugBuffer, _DEBUG_BUFFER_SIZE - 10,
                "0x%08X\t\t0x%08X\t%s\t0x%08X\t\t%s",
                (Uint32)offset,
                pageNumber,
                "[SKIPPED]",
                lengthOfCompressedData,
                lengthOfCompressedData == 0xffff ? "No" : "Yes");
            skippedCount++;
        }
        log_write(_debugBuffer);

        // move to next data page entry
        offset += 3;    // skip over data page header
        offset += lengthOfCompressedData; // skip to immediately after data page
    }

    log_write("");
    if (skippedCount > 0) {
        log_write("Z80: warning - some data pages were skipped and not loaded");
        log_write("");
    }

    return 1;
}

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

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

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

    Uint8 version = z80_get_z80_file_version(file, info.st_size);
    if (version == 0) {
        free(file);
        return 0;
    }

    // we've successfully determined the file version, and the file
    // is long enough for the version it claims to be
    log_write_string_int("Z80: file is version %d", version);

    // we start by loading the v1 header chunk, because it is also common
    // to v2 and v3
    // if the file is v2 or v3, then the correct value of the PC will be
    // populated later below
    struct z80FilePropertiesFromV1Header propertiesV1 = _z80_load_v1_header_chunk(file);
    
    struct z80FilePropertiesFromV2V3Header propertiesV2V3;

    Uint8 dataBlockLoadResult = 0;
    switch (version)
    {
    case 1:
        dataBlockLoadResult = _z80_load_v1_data_block(file, info.st_size, propertiesV1);
        break;
    case 2:
        propertiesV2V3 = _z80_load_v2_v3_header_chunk(file);
        dataBlockLoadResult = _z80_load_v2v3_data_block(file, info.st_size, propertiesV2V3);
        break;
    case 3:
        propertiesV2V3 = _z80_load_v2_v3_header_chunk(file);
        dataBlockLoadResult = _z80_load_v2v3_data_block(file, info.st_size, propertiesV2V3);
        break;
    default:
        break;
    }

    free(file);

    if (dataBlockLoadResult != 0) {
        log_write("Loaded Z80 into ZX Spectrum memory");
    }

    return 1;
}

// consumer does not own pointer
//
char* _z80_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.z80",
        _filenameAndExtensionBuffer,
        time.wYear,
        time.wMonth,
        time.wDay,
        time.wHour,
        time.wMinute,
        time.wSecond,
        time.wMilliseconds);

    return _saveStateFilenameBuffer;
}

Uint8 z80_handle_save() {
    if (!_z80_save_scheduled) {
        return 0;
    }

    _z80_save_scheduled = 0;

    char* filename = _z80_get_filename_for_save();

    log_write("Starting Z80 save");
    log_write(filename);

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

    // write header
    *((Uint8*)(file + 0)) = cpu_regs()->A;
    *((Uint8*)(file + 1)) = cpu_regs()->F;
    *((Uint16*)(file + 2)) = *cpu_regs()->BC;
    *((Uint16*)(file + 4)) = *cpu_regs()->HL;
    *((Uint16*)(file + 6)) = cpu_regs()->PC;
    *((Uint16*)(file + 8)) = cpu_regs()->SP;
    *((Uint8*)(file + 10)) = cpu_regs()->I;
    *((Uint8*)(file + 11)) = cpu_regs()->R;
    
    Uint8 byte12 = cpu_regs()->R >> 7;          //    bit 0: bit 7 of R register
    byte12 |= io_read8_border_colour() << 1;    // bits 1-3: border colour
                                                // bit 5: 0 when data block is not compressed
    *((Uint8*)(file + 12)) = byte12;
    
    *((Uint16*)(file + 13)) = *cpu_regs()->DE;
    *((Uint16*)(file + 15)) = *cpu_regs()->BC_alt;
    *((Uint16*)(file + 17)) = *cpu_regs()->DE_alt;
    *((Uint16*)(file + 19)) = *cpu_regs()->HL_alt;
    *((Uint8*)(file + 21)) = cpu_regs()->A_alt;
    *((Uint8*)(file + 22)) = cpu_regs()->F_alt;
    *((Uint16*)(file + 23)) = *cpu_regs()->IY;
    *((Uint16*)(file + 25)) = *cpu_regs()->IX;
    *((Uint8*)(file + 27)) = cpu_regs()->IFF;
    *((Uint8*)(file + 28)) = cpu_regs()->IFF & CPU_IFF2_BIT;
    
    Uint8 byte29 = cpu_get_interrupt_mode();    // bits 0-1: interrupt mode
    *((Uint8*)(file + 29)) = byte29;

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

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

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

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

    return 1;
}

void z80_keyup(SDL_Keysym key) {
    if (!_z80Initialized) {
        return;
    }

    switch (key.sym) {
    case SDLK_F12:
        _z80_save_scheduled = 1;
        break;
    }
}

void z80_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: Z80 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: Z80 saving unavailable: could not parse path %s", _absolutePathBuffer);
        log_write(_debugBuffer);
        return;
    }

    CreateDirectoryA("saved_Z80s", NULL);

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

    _z80Initialized = 1;
}

void z80_destroy() {
    if (!_z80Initialized) {
        return;
    }
}