/* Copyright (c) 2013-2014 Jeffrey Pfau
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "gba-rr.h"

#include "gba.h"
#include "gba-serialize.h"
#include "util/vfs.h"

#define BINARY_EXT ".dat"
#define BINARY_MAGIC "GBAb"
#define METADATA_FILENAME "metadata" BINARY_EXT

enum {
	INVALID_INPUT = 0x8000
};

static bool _emitMagic(struct GBARRContext* rr, struct VFile* vf);
static bool _verifyMagic(struct GBARRContext* rr, struct VFile* vf);
static enum GBARRTag _readTag(struct GBARRContext* rr, struct VFile* vf);
static bool _seekTag(struct GBARRContext* rr, struct VFile* vf, enum GBARRTag tag);
static bool _emitTag(struct GBARRContext* rr, struct VFile* vf, uint8_t tag);
static bool _emitEnd(struct GBARRContext* rr, struct VFile* vf);

static bool _parseMetadata(struct GBARRContext* rr, struct VFile* vf);

static bool _markStreamNext(struct GBARRContext* rr, uint32_t newStreamId, bool recursive);
static void _streamEndReached(struct GBARRContext* rr);

static struct VFile* _openSavedata(struct GBARRContext* rr, int flags);
static struct VFile* _openSavestate(struct GBARRContext* rr, int flags);

void GBARRContextCreate(struct GBA* gba) {
	if (gba->rr) {
		return;
	}

	gba->rr = calloc(1, sizeof(*gba->rr));
}

void GBARRContextDestroy(struct GBA* gba) {
	if (!gba->rr) {
		return;
	}

	if (GBARRIsPlaying(gba->rr)) {
		GBARRStopPlaying(gba->rr);
	}
	if (GBARRIsRecording(gba->rr)) {
		GBARRStopRecording(gba->rr);
	}
	if (gba->rr->metadataFile) {
		gba->rr->metadataFile->close(gba->rr->metadataFile);
	}
	if (gba->rr->savedata) {
		gba->rr->savedata->close(gba->rr->savedata);
	}

	free(gba->rr);
	gba->rr = 0;
}

void GBARRSaveState(struct GBA* gba) {
	if (!gba || !gba->rr) {
		return;
	}

	if (gba->rr->initFrom & INIT_FROM_SAVEGAME) {
		if (gba->rr->savedata) {
			gba->rr->savedata->close(gba->rr->savedata);
		}
		gba->rr->savedata = _openSavedata(gba->rr, O_TRUNC | O_CREAT | O_WRONLY);
		GBASavedataClone(&gba->memory.savedata, gba->rr->savedata);
		gba->rr->savedata->close(gba->rr->savedata);
		gba->rr->savedata = _openSavedata(gba->rr, O_RDONLY);
		GBASavedataMask(&gba->memory.savedata, gba->rr->savedata);
	} else {
		GBASavedataMask(&gba->memory.savedata, 0);
	}

	if (gba->rr->initFrom & INIT_FROM_SAVESTATE) {
		struct VFile* vf = _openSavestate(gba->rr, O_TRUNC | O_CREAT | O_RDWR);
		GBASaveStateNamed(gba, vf, false);
		vf->close(vf);
	} else {
		ARMReset(gba->cpu);
	}
}

void GBARRLoadState(struct GBA* gba) {
	if (!gba || !gba->rr) {
		return;
	}

	if (gba->rr->initFrom & INIT_FROM_SAVEGAME) {
		if (gba->rr->savedata) {
			gba->rr->savedata->close(gba->rr->savedata);
		}
		gba->rr->savedata = _openSavedata(gba->rr, O_RDONLY);
		GBASavedataMask(&gba->memory.savedata, gba->rr->savedata);
	} else {
		GBASavedataMask(&gba->memory.savedata, 0);
	}

	if (gba->rr->initFrom & INIT_FROM_SAVESTATE) {
		struct VFile* vf = _openSavestate(gba->rr, O_RDONLY);
		GBALoadStateNamed(gba, vf);
		vf->close(vf);
	} else {
		ARMReset(gba->cpu);
	}
}

bool GBARRInitStream(struct GBARRContext* rr, struct VDir* stream) {
	if (rr->movieStream && !rr->movieStream->close(rr->movieStream)) {
		return false;
	}

	if (rr->metadataFile && !rr->metadataFile->close(rr->metadataFile)) {
		return false;
	}

	rr->streamDir = stream;
	rr->metadataFile = rr->streamDir->openFile(rr->streamDir, METADATA_FILENAME, O_CREAT | O_RDWR);
	rr->currentInput = INVALID_INPUT;
	if (!_parseMetadata(rr, rr->metadataFile)) {
		rr->metadataFile->close(rr->metadataFile);
		rr->metadataFile = 0;
		rr->maxStreamId = 0;
	}
	rr->streamId = 1;
	rr->movieStream = 0;
	return true;
}

bool GBARRReinitStream(struct GBARRContext* rr, enum GBARRInitFrom initFrom) {
	if (!rr) {
		return false;
	}

	if (rr->metadataFile) {
		rr->metadataFile->truncate(rr->metadataFile, 0);
	} else {
		rr->metadataFile = rr->streamDir->openFile(rr->streamDir, METADATA_FILENAME, O_CREAT | O_TRUNC | O_RDWR);
	}
	_emitMagic(rr, rr->metadataFile);

	rr->initFrom = initFrom;
	rr->initFromOffset = rr->metadataFile->seek(rr->metadataFile, 0, SEEK_CUR);
	_emitTag(rr, rr->metadataFile, TAG_INIT | initFrom);

	rr->streamId = 0;
	rr->maxStreamId = 0;
	_emitTag(rr, rr->metadataFile, TAG_MAX_STREAM);
	rr->maxStreamIdOffset = rr->metadataFile->seek(rr->metadataFile, 0, SEEK_CUR);
	rr->metadataFile->write(rr->metadataFile, &rr->maxStreamId, sizeof(rr->maxStreamId));

	rr->rrCount = 0;
	_emitTag(rr, rr->metadataFile, TAG_RR_COUNT);
	rr->rrCountOffset = rr->metadataFile->seek(rr->metadataFile, 0, SEEK_CUR);
	rr->metadataFile->write(rr->metadataFile, &rr->rrCount, sizeof(rr->rrCount));
	return true;
}

bool GBARRLoadStream(struct GBARRContext* rr, uint32_t streamId) {
	if (rr->movieStream && !rr->movieStream->close(rr->movieStream)) {
		return false;
	}
	rr->movieStream = 0;
	rr->streamId = streamId;
	rr->currentInput = INVALID_INPUT;
	char buffer[14];
	snprintf(buffer, sizeof(buffer), "%u" BINARY_EXT, streamId);
	if (GBARRIsRecording(rr)) {
		int flags = O_CREAT | O_RDWR;
		if (streamId > rr->maxStreamId) {
			flags |= O_TRUNC;
		}
		rr->movieStream = rr->streamDir->openFile(rr->streamDir, buffer, flags);
	} else if (GBARRIsPlaying(rr)) {
		rr->movieStream = rr->streamDir->openFile(rr->streamDir, buffer, O_RDONLY);
		rr->peekedTag = TAG_INVALID;
		if (!rr->movieStream || !_verifyMagic(rr, rr->movieStream) || !_seekTag(rr, rr->movieStream, TAG_BEGIN)) {
			GBARRStopPlaying(rr);
		}
	}
	GBALog(0, GBA_LOG_DEBUG, "[RR] Loading segment: %u", streamId);
	rr->frames = 0;
	rr->lagFrames = 0;
	return true;
}

bool GBARRIncrementStream(struct GBARRContext* rr, bool recursive) {
	uint32_t newStreamId = rr->maxStreamId + 1;
	uint32_t oldStreamId = rr->streamId;
	if (GBARRIsRecording(rr) && rr->movieStream) {
		if (!_markStreamNext(rr, newStreamId, recursive)) {
			return false;
		}
	}
	if (!GBARRLoadStream(rr, newStreamId)) {
		return false;
	}
	GBALog(0, GBA_LOG_DEBUG, "[RR] New segment: %u", newStreamId);
	_emitMagic(rr, rr->movieStream);
	rr->maxStreamId = newStreamId;
	_emitTag(rr, rr->movieStream, TAG_PREVIOUSLY);
	rr->movieStream->write(rr->movieStream, &oldStreamId, sizeof(oldStreamId));
	_emitTag(rr, rr->movieStream, TAG_BEGIN);

	rr->metadataFile->seek(rr->metadataFile, rr->maxStreamIdOffset, SEEK_SET);
	rr->metadataFile->write(rr->metadataFile, &rr->maxStreamId, sizeof(rr->maxStreamId));
	rr->previously = oldStreamId;
	return true;
}

bool GBARRStartPlaying(struct GBARRContext* rr, bool autorecord) {
	if (GBARRIsRecording(rr) || GBARRIsPlaying(rr)) {
		return false;
	}

	rr->isPlaying = true;
	if (!GBARRLoadStream(rr, 1)) {
		rr->isPlaying = false;
		return false;
	}
	rr->autorecord = autorecord;
	return true;
}

void GBARRStopPlaying(struct GBARRContext* rr) {
	if (!GBARRIsPlaying(rr)) {
		return;
	}
	rr->isPlaying = false;
	if (rr->movieStream) {
		rr->movieStream->close(rr->movieStream);
		rr->movieStream = 0;
	}
}

bool GBARRStartRecording(struct GBARRContext* rr) {
	if (GBARRIsRecording(rr) || GBARRIsPlaying(rr)) {
		return false;
	}

	if (!rr->maxStreamIdOffset) {
		_emitTag(rr, rr->metadataFile, TAG_MAX_STREAM);
		rr->maxStreamIdOffset = rr->metadataFile->seek(rr->metadataFile, 0, SEEK_CUR);
		rr->metadataFile->write(rr->metadataFile, &rr->maxStreamId, sizeof(rr->maxStreamId));
	}

	rr->isRecording = true;
	return GBARRIncrementStream(rr, false);
}

void GBARRStopRecording(struct GBARRContext* rr) {
	if (!GBARRIsRecording(rr)) {
		return;
	}
	rr->isRecording = false;
	if (rr->movieStream) {
		_emitEnd(rr, rr->movieStream);
		rr->movieStream->close(rr->movieStream);
		rr->movieStream = 0;
	}
}

bool GBARRIsPlaying(struct GBARRContext* rr) {
	return rr && rr->isPlaying;
}

bool GBARRIsRecording(struct GBARRContext* rr) {
	return rr && rr->isRecording;
}

void GBARRNextFrame(struct GBARRContext* rr) {
	if (!GBARRIsRecording(rr) && !GBARRIsPlaying(rr)) {
		return;
	}

	if (GBARRIsPlaying(rr)) {
		while (rr->peekedTag == TAG_INPUT) {
			_readTag(rr, rr->movieStream);
			GBALog(0, GBA_LOG_WARN, "[RR] Desync detected!");
		}
		if (rr->peekedTag == TAG_LAG) {
			GBALog(0, GBA_LOG_DEBUG, "[RR] Lag frame marked in stream");
			if (rr->inputThisFrame) {
				GBALog(0, GBA_LOG_WARN, "[RR] Lag frame in stream does not match movie");
			}
		}
	}

	++rr->frames;
	GBALog(0, GBA_LOG_DEBUG, "[RR] Frame: %u", rr->frames);
	if (!rr->inputThisFrame) {
		++rr->lagFrames;
		GBALog(0, GBA_LOG_DEBUG, "[RR] Lag frame: %u", rr->lagFrames);
	}

	if (GBARRIsRecording(rr)) {
		if (!rr->inputThisFrame) {
			_emitTag(rr, rr->movieStream, TAG_LAG);
		}
		_emitTag(rr, rr->movieStream, TAG_FRAME);
		rr->inputThisFrame = false;
	} else {
		if (!_seekTag(rr, rr->movieStream, TAG_FRAME)) {
			_streamEndReached(rr);
		}
	}
}

void GBARRLogInput(struct GBARRContext* rr, uint16_t keys) {
	if (!GBARRIsRecording(rr)) {
		return;
	}

	if (keys != rr->currentInput) {
		_emitTag(rr, rr->movieStream, TAG_INPUT);
		rr->movieStream->write(rr->movieStream, &keys, sizeof(keys));
		rr->currentInput = keys;
	}
	GBALog(0, GBA_LOG_DEBUG, "[RR] Input log: %03X", rr->currentInput);
	rr->inputThisFrame = true;
}

uint16_t GBARRQueryInput(struct GBARRContext* rr) {
	if (!GBARRIsPlaying(rr)) {
		return 0;
	}

	if (rr->peekedTag == TAG_INPUT) {
		_readTag(rr, rr->movieStream);
	}
	rr->inputThisFrame = true;
	if (rr->currentInput == INVALID_INPUT) {
		GBALog(0, GBA_LOG_WARN, "[RR] Stream did not specify input");
	}
	GBALog(0, GBA_LOG_DEBUG, "[RR] Input replay: %03X", rr->currentInput);
	return rr->currentInput;
}

bool GBARRFinishSegment(struct GBARRContext* rr) {
	if (rr->movieStream) {
		if (!_emitEnd(rr, rr->movieStream)) {
			return false;
		}
	}
	return GBARRIncrementStream(rr, false);
}

bool GBARRSkipSegment(struct GBARRContext* rr) {
	rr->nextTime = 0;
	while (_readTag(rr, rr->movieStream) != TAG_EOF);
	if (!rr->nextTime || !GBARRLoadStream(rr, rr->nextTime)) {
		_streamEndReached(rr);
		return false;
	}
	return true;
}

bool GBARRMarkRerecord(struct GBARRContext* rr) {
	++rr->rrCount;
	rr->metadataFile->seek(rr->metadataFile, rr->rrCountOffset, SEEK_SET);
	rr->metadataFile->write(rr->metadataFile, &rr->rrCount, sizeof(rr->rrCount));
	return true;
}

bool _emitMagic(struct GBARRContext* rr, struct VFile* vf) {
	UNUSED(rr);
	return vf->write(vf, BINARY_MAGIC, 4) == 4;
}

bool _verifyMagic(struct GBARRContext* rr, struct VFile* vf) {
	UNUSED(rr);
	char buffer[4];
	if (vf->read(vf, buffer, sizeof(buffer)) != sizeof(buffer)) {
		return false;
	}
	if (memcmp(buffer, BINARY_MAGIC, sizeof(buffer)) != 0) {
		return false;
	}
	return true;
}

enum GBARRTag _readTag(struct GBARRContext* rr, struct VFile* vf) {
	if (!rr || !vf) {
		return TAG_EOF;
	}

	enum GBARRTag tag = rr->peekedTag;
	switch (tag) {
	case TAG_INPUT:
		vf->read(vf, &rr->currentInput, sizeof(uint16_t));
		break;
	case TAG_PREVIOUSLY:
		vf->read(vf, &rr->previously, sizeof(rr->previously));
		break;
	case TAG_NEXT_TIME:
		vf->read(vf, &rr->nextTime, sizeof(rr->nextTime));
		break;
	case TAG_MAX_STREAM:
		vf->read(vf, &rr->maxStreamId, sizeof(rr->maxStreamId));
		break;
	case TAG_FRAME_COUNT:
		vf->read(vf, &rr->frames, sizeof(rr->frames));
		break;
	case TAG_LAG_COUNT:
		vf->read(vf, &rr->lagFrames, sizeof(rr->lagFrames));
		break;
	case TAG_RR_COUNT:
		vf->read(vf, &rr->rrCount, sizeof(rr->rrCount));
		break;

	case TAG_INIT_EX_NIHILO:
		rr->initFrom = INIT_EX_NIHILO;
		break;
	case TAG_INIT_FROM_SAVEGAME:
		rr->initFrom = INIT_FROM_SAVEGAME;
		break;
	case TAG_INIT_FROM_SAVESTATE:
		rr->initFrom = INIT_FROM_SAVESTATE;
	case TAG_INIT_FROM_BOTH:
		rr->initFrom = INIT_FROM_BOTH;
		break;

	// To be spec'd
	case TAG_AUTHOR:
	case TAG_COMMENT:
		break;

	// Empty markers
	case TAG_FRAME:
	case TAG_LAG:
	case TAG_BEGIN:
	case TAG_END:
	case TAG_INVALID:
	case TAG_EOF:
		break;
	}

	uint8_t tagBuffer;
	if (vf->read(vf, &tagBuffer, 1) != 1) {
		rr->peekedTag = TAG_EOF;
	} else {
		rr->peekedTag = tagBuffer;
	}

	if (rr->peekedTag == TAG_END) {
		GBARRSkipSegment(rr);
	}
	return tag;
}

bool _seekTag(struct GBARRContext* rr, struct VFile* vf, enum GBARRTag tag) {
	enum GBARRTag readTag;
	while ((readTag = _readTag(rr, vf)) != tag) {
		if (readTag == TAG_EOF) {
			return false;
		}
	}
	return true;
}

bool _emitTag(struct GBARRContext* rr, struct VFile* vf, uint8_t tag) {
	UNUSED(rr);
	return vf->write(vf, &tag, sizeof(tag)) == sizeof(tag);
}

bool _parseMetadata(struct GBARRContext* rr, struct VFile* vf) {
	if (!_verifyMagic(rr, vf)) {
		return false;
	}
	while (_readTag(rr, vf) != TAG_EOF) {
		switch (rr->peekedTag) {
		case TAG_MAX_STREAM:
			rr->maxStreamIdOffset = vf->seek(vf, 0, SEEK_CUR);
			break;
		case TAG_INIT_EX_NIHILO:
		case TAG_INIT_FROM_SAVEGAME:
		case TAG_INIT_FROM_SAVESTATE:
		case TAG_INIT_FROM_BOTH:
			rr->initFromOffset = vf->seek(vf, 0, SEEK_CUR);
			break;
		case TAG_RR_COUNT:
			rr->rrCountOffset = vf->seek(vf, 0, SEEK_CUR);
			break;
		default:
			break;
		}
	}
	return true;
}

bool _emitEnd(struct GBARRContext* rr, struct VFile* vf) {
	// TODO: Error check
	_emitTag(rr, vf, TAG_END);
	_emitTag(rr, vf, TAG_FRAME_COUNT);
	vf->write(vf, &rr->frames, sizeof(rr->frames));
	_emitTag(rr, vf, TAG_LAG_COUNT);
	vf->write(vf, &rr->lagFrames, sizeof(rr->lagFrames));
	_emitTag(rr, vf, TAG_NEXT_TIME);

	uint32_t newStreamId = 0;
	vf->write(vf, &newStreamId, sizeof(newStreamId));
	return true;
}

bool _markStreamNext(struct GBARRContext* rr, uint32_t newStreamId, bool recursive) {
	if (rr->movieStream->seek(rr->movieStream, -sizeof(newStreamId) - 1, SEEK_END) < 0) {
		return false;
	}

	uint8_t tagBuffer;
	if (rr->movieStream->read(rr->movieStream, &tagBuffer, 1) != 1) {
		return false;
	}
	if (tagBuffer != TAG_NEXT_TIME) {
		return false;
	}
	if (rr->movieStream->write(rr->movieStream, &newStreamId, sizeof(newStreamId)) != sizeof(newStreamId)) {
		return false;
	}
	if (recursive) {
		if (rr->movieStream->seek(rr->movieStream, 0, SEEK_SET) < 0) {
			return false;
		}
		if (!_verifyMagic(rr, rr->movieStream)) {
			return false;
		}
		_readTag(rr, rr->movieStream);
		if (_readTag(rr, rr->movieStream) != TAG_PREVIOUSLY) {
			return false;
		}
		if (rr->previously == 0) {
			return true;
		}
		uint32_t currentStreamId = rr->streamId;
		if (!GBARRLoadStream(rr, rr->previously)) {
			return false;
		}
		return _markStreamNext(rr, currentStreamId, rr->previously);
	}
	return true;
}

void _streamEndReached(struct GBARRContext* rr) {
	if (!GBARRIsPlaying(rr)) {
		return;
	}

	uint32_t endStreamId = rr->streamId;
	GBARRStopPlaying(rr);
	if (rr->autorecord) {
		rr->isRecording = true;
		GBARRLoadStream(rr, endStreamId);
		GBARRIncrementStream(rr, false);
	}
}

struct VFile* _openSavedata(struct GBARRContext* rr, int flags) {
	return rr->streamDir->openFile(rr->streamDir, "movie.sav", flags);
}

struct VFile* _openSavestate(struct GBARRContext* rr, int flags) {
	return rr->streamDir->openFile(rr->streamDir, "movie.ssm", flags);
}