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

#include "SaveStates.h"
#include "Log.h"
#include "Cpu.h"
#include "Io.h"
#include "Constants.h"
#include "Memory.h"
#include "Notification.h"
#include "Video.h"
#include "TapLoader.h"

/*
  Format of a zxian state file closely resembles the format of a 48k SNA file, with 
  additional bytes from offset 0xC01B on.

		Offset		    Contents
		0x00		    I
		0x01		    HL'
		0x03		    DE'
		0x05		    BC'
		0x07		    AF'
		0x09		    HL
		0x0B		    DE
		0x0D		    BC
		0x0F		    IY
		0x11		    IX
		0x13		    IFF
		0x14		    R
		0x15		    AF
		0x17		    SP
		0x19		    Interrupt mode: 0, 1 or 2
		0x1A		    Border colour
		0x1B-0xC01A     48 kilobytes of RAM

		now follow zxian-custom fields

		0xC01B		2b  PC
		0xC01D		    halted state of CPU (1=halted, 0=not halted)
		0xC01E          interrupt requested state of CPU (1=requested, 0=not requested)
		0xC01F      2b  tape loader: current tape block, or 0xFFFF if a tape is not loaded
		                this is useful when a user saves state in a multi-load game - he can then
						return to the exact same tape block by loading state

		Total 0xC021 (49185) bytes
*/

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

static volatile Uint8 _savestates_load_scheduled = 0;
static volatile Uint8 _savestates_save_scheduled = 0;
static volatile Uint8 _savestates_load_slot = 255;
static volatile Uint8 _savestates_save_slot = 255;

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

// consumer does not own pointer
//
char* _savestates_get_filename_by_slot(Uint8 slot) {
	if (slot > 9) {
		return NULL;
	}

	char slotChar = '0' + slot;
	sprintf_s(_saveStateFilenameBuffer, FILENAME_AND_EXTENSION_MAX_SIZE - 5, "%s\\%s_%c.sav",
		_filenameAndExtensionBuffer,
		_filenameBuffer,
		slotChar);
	return _saveStateFilenameBuffer;
}

void savestates_save_to_buffer(Uint8* buffer) {
	*((Uint8*)(buffer + 0x00)) = cpu_regs()->I;

	*((Uint16*)(buffer + 0x01)) = *cpu_regs()->HL_alt;
	*((Uint16*)(buffer + 0x03)) = *cpu_regs()->DE_alt;
	*((Uint16*)(buffer + 0x05)) = *cpu_regs()->BC_alt;
	*((Uint16*)(buffer + 0x07)) = *cpu_regs()->AF_alt;

	*((Uint16*)(buffer + 0x09)) = *cpu_regs()->HL;
	*((Uint16*)(buffer + 0x0B)) = *cpu_regs()->DE;
	*((Uint16*)(buffer + 0x0D)) = *cpu_regs()->BC;
	*((Uint16*)(buffer + 0x0F)) = *cpu_regs()->IY;
	*((Uint16*)(buffer + 0x11)) = *cpu_regs()->IX;

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

	Uint8 interruptMode = 0;
	enum interruptMode mode = cpu_get_interrupt_mode();
	switch (mode) {
	case Mode0:
		break;
	case Mode1:
		interruptMode = 1;
		break;
	case Mode2:
		interruptMode = 2;
		break;
	default:
		break;
	}
	*((Uint8*)(buffer + 0x19)) = interruptMode;

	Uint8 borderColour = io_read8_border_colour();
	*((Uint8*)(buffer + 0x1A)) = borderColour;

	// write RAM
	for (int i = 0; i < 48 * 1024; i++) {
		Uint8 byte = memory_read8(16384 + i);
		*((Uint8*)(buffer + 0x1B + i)) = byte;
	}

	// write PC
	*((Uint16*)(buffer + 0xC01B)) = cpu_regs()->PC;

	*((Uint8*)(buffer + 0xC01D)) = cpu_is_halted() ? 1 : 0;
	*((Uint8*)(buffer + 0xC01E)) = cpu_is_interrupt_requested() ? 1 : 0;

	Uint16 tapeBlock = 0xFFFF;
	if (taploader_is_file_loaded()) {
		tapeBlock = taploader_get_state().block->blockIndex;
	}
	*((Uint16*)(buffer + 0xC01F)) = tapeBlock;
}

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

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

	Uint8 interruptMode = *((Uint8*)(buffer + 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;
	}

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

	memory_load(buffer + 0x1B, 16384, SPECTRUM_RAM_SIZE);

	cpu_regs()->PC = *((Uint16*)(buffer + 0xC01B));

	Uint8 isHalted = *((Uint8*)(buffer + 0xC01D));
	Uint8 isInterruptRequested = *((Uint8*)(buffer + 0xC01E));
	cpu_prepare_after_state_load(isHalted, isInterruptRequested);

	Uint16 tapeBlock = *((Uint16*)(buffer + 0xC01F));
	if (tapeBlock != 0xFFFF) {
		if (!taploader_is_file_loaded()) {
			log_write("Warning: state load - state contains a tape block, but no tape is loaded");
		}
		else {
			if (tapeBlock >= taploader_get_block_count()) {
				log_write("Warning: state load - tape block in loaded state has index beyond tape end");
			}
			else {
				// we don't store tape position, so we stop tape upon loading
				taploader_press_stop();

				// seek to the loaded tape block index
				struct tapRunState tapeState = taploader_get_state();
				while (tapeBlock != tapeState.block->blockIndex) {
					if (tapeBlock < tapeState.block->blockIndex) {
						taploader_seek_previous();
					}
					else {
						taploader_seek_next();
					}
					tapeState = taploader_get_state();
				}
			}
		}
	}

	video_force_next_frame_full_render();
}

void _savestates_load(Uint8 slot) {
	if (!_saveStatesInitialized) {
		return;
	}

	char* filename = _savestates_get_filename_by_slot(slot);
	if (!filename) {
		sprintf_s(_debugBuffer, _DEBUG_BUFFER_SIZE - 10, "Warning: could not resolve state file name for slot %u", slot);
		log_write(_debugBuffer);
		return;
	}

	struct stat info;
	if (stat(filename, &info) != 0) {
		// no file in specified state slot, so we do nothing
		sprintf_s(_debugBuffer, _DEBUG_BUFFER_SIZE - 10, "Cannot load: no state in slot %u", slot);
		notification_show(_debugBuffer, 1500, video_force_next_frame_full_render, NULL);
		return;
	}

	if (info.st_size != STATE_SIZE) {
		sprintf_s(_debugBuffer, _DEBUG_BUFFER_SIZE - 10, "Warning: state file %s has size %u instead of %u", 
			filename,
			info.st_size,
			STATE_SIZE);
		log_write(_debugBuffer);
		return;
	}

	Uint8* buffer = (Uint8*)malloc(STATE_SIZE * sizeof(Uint8));
	if (!buffer) {
		log_write("Warning: could not allocate buffer to load state");
		return;
	}

	FILE* fp;
	errno_t result = fopen_s(&fp, filename, "rb");
	if (result) {
		sprintf_s(_debugBuffer, _DEBUG_BUFFER_SIZE - 10, "Warning: could not open state file %s",
			filename);
		log_write(_debugBuffer);
		free(buffer);
		return;
	}
	// try to read a single block of info.st_size bytes
	size_t blocks_read = fread(buffer, info.st_size, 1, fp);
	if (blocks_read != 1) {
		sprintf_s(_debugBuffer, _DEBUG_BUFFER_SIZE - 10, "Warning: could not read from state file %s",
			filename);
		log_write(_debugBuffer);
		fclose(fp);
		free(buffer);
		return;
	}
	fclose(fp);

	savestates_load_from_buffer(buffer);
	free(buffer);

	sprintf_s(_debugBuffer, _DEBUG_BUFFER_SIZE - 10, "Loaded state from slot %u (file %s)", slot, filename);
	log_write(_debugBuffer);

	sprintf_s(_debugBuffer, _DEBUG_BUFFER_SIZE - 10, "Loaded state from slot %u", slot);
	notification_show(_debugBuffer, 1500, video_force_next_frame_full_render, NULL);
}

void _savestates_save(Uint8 slot) {
	if (!_saveStatesInitialized) {
		return;
	}

	char* filename = _savestates_get_filename_by_slot(slot);
	if (!filename) {
		sprintf_s(_debugBuffer, _DEBUG_BUFFER_SIZE - 10, "Warning: could not resolve state file name for slot %u", slot);
		log_write(_debugBuffer);
		return;
	}

	Uint8* buffer = (Uint8*)malloc(STATE_SIZE * sizeof(Uint8));
	if (!buffer) {
		log_write("Warning: could not allocate buffer to save state");
		return;
	}

	savestates_save_to_buffer(buffer);

	// write populated buffer to file

	FILE* file;
	errno_t result = fopen_s(&file, filename, "wb");
	if (result) {
		log_write("Warning: could not create save state file");
		free(buffer);
		return;
	}

	size_t written = fwrite(buffer, 1, STATE_SIZE, file);
	if (written != STATE_SIZE) {
		log_write("Warning: wrote fewer bytes than expected to save state file");
		fclose(file);
		free(buffer);
		return;
	}

	sprintf_s(_debugBuffer, _DEBUG_BUFFER_SIZE - 10, "Saved state to slot %u (file %s)", slot, filename);
	log_write(_debugBuffer);

	sprintf_s(_debugBuffer, _DEBUG_BUFFER_SIZE - 10, "Saved state to slot %u", slot);
	notification_show(_debugBuffer, 1500, video_force_next_frame_full_render, NULL);

	fclose(file);
	free(buffer);
}

void savestates_keyup(SDL_Keysym key) {
	if (!_saveStatesInitialized) {
		return;
	}

	switch (key.sym) {
	case SDLK_F5:
		_savestates_save_slot = 0;
		_savestates_save_scheduled = 1;
		break;
	case SDLK_F6:
		_savestates_save_slot = 1;
		_savestates_save_scheduled = 1;
		break;
	case SDLK_F7:
		_savestates_save_slot = 2;
		_savestates_save_scheduled = 1;
		break;
	case SDLK_F8:
		_savestates_save_slot = 3;
		_savestates_save_scheduled = 1;
		break;

	case SDLK_F1:
		_savestates_load_slot = 0;
		_savestates_load_scheduled = 1;
		break;
	case SDLK_F2:
		_savestates_load_slot = 1;
		_savestates_load_scheduled = 1;
		break;
	case SDLK_F3:
		_savestates_load_slot = 2;
		_savestates_load_scheduled = 1;
		break;
	case SDLK_F4:
		_savestates_load_slot = 3;
		_savestates_load_scheduled = 1;
		break;
	}
}

void savestates_handle_save() {
	if (!_savestates_save_scheduled) {
		return;
	}
	
	_savestates_save(_savestates_save_slot);
	_savestates_save_scheduled = 0;
}

void savestates_handle_load() {
	if (!_savestates_load_scheduled) {
		return;
	}

	_savestates_load(_savestates_load_slot);
	_savestates_load_scheduled = 0;
}

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

	CreateDirectoryA("states", NULL);

	sprintf_s(_filenameAndExtensionBuffer, FILENAME_AND_EXTENSION_MAX_SIZE - 5, "states\\%s%s", _filenameBuffer, _extensionBuffer);
	sprintf_s(_debugBuffer, _DEBUG_BUFFER_SIZE-10, "Save states root directory: %s", _filenameAndExtensionBuffer);
	log_write(_debugBuffer);

	CreateDirectoryA(_filenameAndExtensionBuffer, NULL);
	_saveStatesInitialized = 1;
}

void savestates_destroy() {
	if (!_saveStatesInitialized) {
		return;
	}
}
