#include <windows.h>
#include <sys\timeb.h>
#include <time.h>
#include <sys/stat.h>

#include "Rewind.h"
#include "Log.h"
#include "SaveStates.h"
#include "Notification.h"

Uint8 _rewindInitialized = 0;

// thread safety: this is modified by both the video thread,
// as well as the UI thread
static volatile Uint8 _rewind_load_scheduled = 0;

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

struct timeb _rewindModuleStartTime;
Uint8 _isPastApplicationInitialization = 0;

// interval between each saved chunk
#define REWIND_CHUNK_INTERVAL_MS 500

// how many chunks back we go when we perform a rewind
#define REWIND_CHUNK_COUNT 8

// time at the start of the application when we do nothing
#define REWIND_APP_INIT_TIME_MS (REWIND_CHUNK_INTERVAL_MS*4)

// length of our rewind history
#define REWIND_CHUNK_TOTAL REWIND_CHUNK_COUNT*6
Uint8* _rewindChunks[REWIND_CHUNK_TOTAL];

static volatile Sint64 _rewindChunkIndex = 0;

// time of last chunk capture
struct timeb _rewindLastChunkTimestamp;

void _rewind_try_save_chunk() {
	struct timeb now;
	ftime(&now);

	Uint64 timeSinceLastChunkMs = (Uint64)(1000.0 * (now.time - _rewindLastChunkTimestamp.time)
		+ (now.millitm - _rewindLastChunkTimestamp.millitm));
	if (timeSinceLastChunkMs < REWIND_CHUNK_INTERVAL_MS) {
		// not enough time has passed, so we do not capture another chunk
		return;
	}

	// we must capture another chunk

	// create new chunk, if it's still NULL
	Uint8* buf = _rewindChunks[_rewindChunkIndex];
	if (!buf) {
		// we need to allocate it
		buf = (Uint8*)malloc(STATE_SIZE * sizeof(Uint8));

		if (!buf) {
			log_write("Warning: could not allocate rewind chunk buffer");
			return;
		}
		_rewindChunks[_rewindChunkIndex] = buf;
	}

	// dump state into chunk buffer
	savestates_save_to_buffer(_rewindChunks[_rewindChunkIndex]);

	// advance index
	_rewindChunkIndex++;
	if (_rewindChunkIndex >= REWIND_CHUNK_TOTAL) {
		_rewindChunkIndex = 0;
	}

	// save timestamp
	_rewindLastChunkTimestamp = now;
}

void _rewind_handle_load() {
	if (!_rewind_load_scheduled) {
		return;
	}

	struct timeb now;
	ftime(&now);

	// find index of suitable chunk to load
	Sint64 indexToLoad = _rewindChunkIndex - REWIND_CHUNK_COUNT;
	if (indexToLoad < 0) {
		// wrap around
		indexToLoad += REWIND_CHUNK_TOTAL;
	}

	Uint8* chunk = _rewindChunks[indexToLoad];
	if (!chunk) {
		// we don't yet have a chunk that's that old

		// a rewind is no longer scheduled
		_rewind_load_scheduled = 0;
		return;
	}

	// perform the load
	savestates_load_from_buffer(chunk);

	// destroy all chunks skipped over
	// we do this because we might wrap around after repeated rewinds, which can 
	// cause a false rewind into the future
	while (_rewindChunkIndex != indexToLoad && _rewindChunkIndex >= 0 && _rewindChunkIndex < REWIND_CHUNK_TOTAL) {
		Uint8* chunkToDeallocate = _rewindChunks[_rewindChunkIndex];
		if (chunkToDeallocate) {
			free(chunkToDeallocate);
			_rewindChunks[_rewindChunkIndex] = NULL;
		}

		_rewindChunkIndex--;
		if (_rewindChunkIndex < 0) {
			_rewindChunkIndex = REWIND_CHUNK_TOTAL - 1;
		}
	}
	_rewindChunkIndex = indexToLoad;

	notification_show("Rewind", 1000, NULL, NULL);

	// a rewind is no longer scheduled
	_rewind_load_scheduled = 0;
}

void _rewind_clear_all_chunks() {
	for (int i = 0; i < REWIND_CHUNK_TOTAL; i++) {
		if (_rewindChunks[i]) {
			free(_rewindChunks[i]);
			_rewindChunks[i] = NULL;
		}
	}
}

void rewind_notify_start_of_frame() {
	if (!_rewindInitialized) {
		return;
	}

	struct timeb now;

	if (!_isPastApplicationInitialization) {
		// we're not past the initialization yet

		ftime(&now);

		Uint64 runTimeMs = (Uint64)(1000.0 * (now.time - _rewindModuleStartTime.time)
			+ (now.millitm - _rewindModuleStartTime.millitm));

		if (runTimeMs <= REWIND_APP_INIT_TIME_MS) {
			// do nothing if not enough time has passed
			return;
		}
		else {
			// we are past application initialization
			_isPastApplicationInitialization = 1;
		}
	}

	// we are past application initialization

	_rewind_try_save_chunk();

	// load, if necessary
	_rewind_handle_load();
}

void rewind_schedule_load() {
	if (!_rewindInitialized) {
		return;
	}

	if (!_isPastApplicationInitialization) {
		return;
	}

	_rewind_load_scheduled = 1;
}

void rewind_keyup(SDL_Keysym key) {
	switch (key.sym) {
	case SDLK_BACKSPACE:
		rewind_schedule_load();
		break;
	}
}

void rewind_start() {
	ftime(&_rewindModuleStartTime);
	_rewindLastChunkTimestamp = _rewindModuleStartTime;

	_rewind_clear_all_chunks();
	_rewindChunkIndex = 0;

	_rewindInitialized = 1;
}

void rewind_destroy() {
	if (!_rewindInitialized) {
		return;
	}

	_rewindInitialized = 0;
	_rewind_clear_all_chunks();
}
