// blatantly stolen from target-libretro

#include <heuristics/heuristics.hpp>
#include <heuristics/heuristics.cpp>
#include <heuristics/super-famicom.cpp>
#include <heuristics/game-boy.cpp>
#include <heuristics/bs-memory.cpp>

#include "resources.hpp"

static Emulator::Interface *emulator;

struct Program : Emulator::Platform
{
	Program();

	auto open(uint id, string name, vfs::file::mode mode, bool required) -> shared_pointer<vfs::file> override;
	auto load(uint id, string name, string type, vector<string> options = {}) -> Emulator::Platform::Load override;
	auto videoFrame(const uint16* data, uint pitch, uint width, uint height, uint scale) -> void override;
	auto audioFrame(const double* samples, uint channels) -> void override;
	auto inputPoll(uint port, uint device, uint input) -> int16 override;
	auto inputRumble(uint port, uint device, uint input, bool enable) -> void override;
	auto notify(string text) -> void override;
	auto getBackdropColor() -> uint16 override;
	auto cpuTrace(vector<string>) -> void override;

	auto load() -> void;
	auto loadFile(string location) -> vector<uint8_t>;
	auto loadSuperFamicom() -> bool;
	auto loadGameBoy() -> bool;
	auto loadBSMemory() -> bool;

	auto save() -> void;

	auto openFileSuperFamicom(string name, vfs::file::mode mode, bool required) -> shared_pointer<vfs::file>;
	auto openFileGameBoy(string name, vfs::file::mode mode, bool required) -> shared_pointer<vfs::file>;

	auto hackPatchMemory(vector<uint8_t>& data) -> void;

	bool overscan = false;
	uint16_t backdropColor;

public:
	struct Game {
		explicit operator bool() const { return (bool)location; }

		string option;
		string location;
		string manifest;
		Markup::Node document;
		boolean patched;
		boolean verified;
	};

	struct SuperFamicom : Game {
		vector<uint8_t> raw_data;
		string title;
		string region;
		vector<uint8_t> program;
		vector<uint8_t> data;
		vector<uint8_t> expansion;
		vector<uint8_t> firmware;
	} superFamicom;

	struct GameBoy : Game {
		vector<uint8_t> program;
	} gameBoy;

	struct BSMemory : Game {
		vector<uint8_t> program;
	} bsMemory;
};

static Program *program = nullptr;

Program::Program()
{
	platform = this;
}

auto Program::save() -> void
{
	if(!emulator->loaded()) return;
	emulator->save();
}

auto Program::open(uint id, string name, vfs::file::mode mode, bool required) -> shared_pointer<vfs::file>
{
	fprintf(stderr, "name \"%s\" was requested\n", name.data());

	shared_pointer<vfs::file> result;

	if (name == "ipl.rom" && mode == vfs::file::mode::read) {
		result = vfs::memory::file::open(iplrom, sizeof(iplrom));
	}

	if (name == "boards.bml" && mode == vfs::file::mode::read) {
		result = vfs::memory::file::open(Boards, sizeof(Boards));
	}

	if (id == 1) { //Super Famicom
		if (name == "manifest.bml" && mode == vfs::file::mode::read) {
			result = vfs::memory::file::open(superFamicom.manifest.data<uint8_t>(), superFamicom.manifest.size());
		}
		else if (name == "program.rom" && mode == vfs::file::mode::read) {
			result = vfs::memory::file::open(superFamicom.program.data(), superFamicom.program.size());
		}
		else if (name == "data.rom" && mode == vfs::file::mode::read) {
			result = vfs::memory::file::open(superFamicom.data.data(), superFamicom.data.size());
		}
		else if (name == "expansion.rom" && mode == vfs::file::mode::read) {
			result = vfs::memory::file::open(superFamicom.expansion.data(), superFamicom.expansion.size());
		}
		else {
			result = openFileSuperFamicom(name, mode, required);
		}
	}
	else if (id == 2) { //Game Boy
		if (name == "manifest.bml" && mode == vfs::file::mode::read) {
			result = vfs::memory::file::open(gameBoy.manifest.data<uint8_t>(), gameBoy.manifest.size());
		}
		else if (name == "program.rom" && mode == vfs::file::mode::read) {
			result = vfs::memory::file::open(gameBoy.program.data(), gameBoy.program.size());
		}
		else {
			result = openFileGameBoy(name, mode, required);
		}
	}
	else if (id == 3) {  //BS Memory
		if (name == "manifest.bml" && mode == vfs::file::mode::read) {
			result = vfs::memory::file::open(bsMemory.manifest.data<uint8_t>(), bsMemory.manifest.size());
		}
		else if (name == "program.rom" && mode == vfs::file::mode::read) {
			result = vfs::memory::file::open(bsMemory.program.data(), bsMemory.program.size());
		}
		else if(name == "program.flash") {
			//writes are not flushed to disk in bsnes
			result = vfs::memory::file::open(bsMemory.program.data(), bsMemory.program.size());
		}
		else {
			result = {};
		}
		// sufami turbo would be id 4 and 5 and is ignored for reasons? do we support it in bizhawk? TODO check this
	}
	return result;
}

auto Program::openFileSuperFamicom(string name, vfs::file::mode mode, bool required) -> shared_pointer<vfs::file>
{
	// TODO: the original bsnes code handles a lot more paths; *.data.ram, time.rtc and download.ram
	// I believe none of these can currently be correctly served by bizhawk and I therefor ignore them here
	// This should probably be changed? Not sure how much can break from not having them
	if(name == "msu1/data.rom" || name.match("msu1/track*.pcm") || name == "save.ram")
	{
		return vfs::fs::file::open(snesCallbacks.snes_path_request(ID::SuperFamicom, name, required), mode);
	}

	if(name == "arm6.program.rom" && mode == vfs::file::mode::read) {
		if(superFamicom.firmware.size() == 0x28000) {
			return vfs::memory::file::open(&superFamicom.firmware.data()[0x00000], 0x20000);
		}
		if(auto memory = superFamicom.document["game/board/memory(type=ROM,content=Program,architecture=ARM6)"]) {
			return vfs::fs::file::open(snesCallbacks.snes_path_request(ID::SuperFamicom, memory["identifier"].text().downcase(), required), mode);
		}
	}

	if(name == "arm6.data.rom" && mode == vfs::file::mode::read) {
		if(superFamicom.firmware.size() == 0x28000) {
			return vfs::memory::file::open(&superFamicom.firmware.data()[0x20000], 0x08000);
		}
		if(auto memory = superFamicom.document["game/board/memory(type=ROM,content=Data,architecture=ARM6)"]) {
			auto file = vfs::fs::file::open(snesCallbacks.snes_path_request(ID::SuperFamicom, memory["identifier"].text().downcase(), required), mode);
			if (file) file->seek(0x20000, vfs::file::index::absolute);
			return file;
		}
	}

	if(name == "hg51bs169.data.rom" && mode == vfs::file::mode::read) {
		if(superFamicom.firmware.size() == 0xc00) {
			return vfs::memory::file::open(superFamicom.firmware.data(), superFamicom.firmware.size());
		}
		if(auto memory = superFamicom.document["game/board/memory(type=ROM,content=Data,architecture=HG51BS169)"]) {
			return vfs::fs::file::open(snesCallbacks.snes_path_request(ID::SuperFamicom, memory["identifier"].text().downcase(), required), mode);
		}
	}

	if(name == "lr35902.boot.rom" && mode == vfs::file::mode::read) {
		if(superFamicom.firmware.size() == 0x100) {
			return vfs::memory::file::open(superFamicom.firmware.data(), superFamicom.firmware.size());
		}
		if(auto memory = superFamicom.document["game/board/memory(type=ROM,content=Boot,architecture=LR35902)"]) {
			return vfs::fs::file::open(snesCallbacks.snes_path_request(ID::SuperFamicom, memory["identifier"].text().downcase(), required), mode);
		}
	}

	if(name == "upd7725.program.rom" && mode == vfs::file::mode::read) {
		if(superFamicom.firmware.size() == 0x2000) {
			return vfs::memory::file::open(&superFamicom.firmware.data()[0x0000], 0x1800);
		}
		if(auto memory = superFamicom.document["game/board/memory(type=ROM,content=Program,architecture=uPD7725)"]) {
			auto file = vfs::fs::file::open(snesCallbacks.snes_path_request(ID::SuperFamicom, memory["identifier"].text().downcase(), required), mode);
			return file;
		}
	}

	if(name == "upd7725.data.rom" && mode == vfs::file::mode::read) {
		if(superFamicom.firmware.size() == 0x2000) {
			return vfs::memory::file::open(&superFamicom.firmware.data()[0x1800], 0x0800);
		}
		if(auto memory = superFamicom.document["game/board/memory(type=ROM,content=Data,architecture=uPD7725)"]) {
			auto file = vfs::fs::file::open(snesCallbacks.snes_path_request(ID::SuperFamicom, memory["identifier"].text().downcase(), required), mode);
			if (file) file->seek(0x1800, vfs::file::index::absolute);
			return file;
		}
	}

	if(name == "upd96050.program.rom" && mode == vfs::file::mode::read) {
		if(superFamicom.firmware.size() == 0xd000) {
			return vfs::memory::file::open(&superFamicom.firmware.data()[0x0000], 0xc000);
		}
		if(auto memory = superFamicom.document["game/board/memory(type=ROM,content=Program,architecture=uPD96050)"]) {
			auto file = vfs::fs::file::open(snesCallbacks.snes_path_request(ID::SuperFamicom, memory["identifier"].text().downcase(), required), mode);
			return file;
		}
	}

	if(name == "upd96050.data.rom" && mode == vfs::file::mode::read) {
		if(superFamicom.firmware.size() == 0xd000) {
			return vfs::memory::file::open(&superFamicom.firmware.data()[0xc000], 0x1000);
		}
		if(auto memory = superFamicom.document["game/board/memory(type=ROM,content=Data,architecture=uPD96050)"]) {
			auto file = vfs::fs::file::open(snesCallbacks.snes_path_request(ID::SuperFamicom, memory["identifier"].text().downcase(), required), mode);
			if (file) file->seek(0xc000, vfs::file::index::absolute);
			return file;
		}
	}

	return {};
}

auto Program::openFileGameBoy(string name, vfs::file::mode mode, bool required) -> shared_pointer<vfs::file>
{
	if(name == "save.ram")
	{
		return vfs::fs::file::open(snesCallbacks.snes_path_request(ID::GameBoy, name, required), mode);
	}

	if(name == "time.rtc")
	{
		return vfs::fs::file::open(snesCallbacks.snes_path_request(ID::GameBoy, name, required), mode);
	}

	if(name == "sgb")
	{
		return vfs::fs::file::open(snesCallbacks.snes_path_request(ID::GameBoy, name, required), mode);
	}

	if(name == "sgb2")
	{
		return vfs::fs::file::open(snesCallbacks.snes_path_request(ID::GameBoy, name, required), mode);
	}

	return {};
}

auto Program::load() -> void {
	emulator->unload();
	emulator->load();

	// per-game hack overrides
	auto title = superFamicom.title;
	auto region = superFamicom.region;

	//relies on mid-scanline rendering techniques
	if(title == "AIR STRIKE PATROL" || title == "DESERT FIGHTER") emulator->configure("Hacks/PPU/Fast", false);

	//the dialogue text is blurry due to an issue in the scanline-based renderer's color math support
	if(title == "マーヴェラス") emulator->configure("Hacks/PPU/Fast", false);

	//stage 2 uses pseudo-hires in a way that's not compatible with the scanline-based renderer
	if(title == "SFC クレヨンシンチャン") emulator->configure("Hacks/PPU/Fast", false);

	//title screen game select (after choosing a game) changes OAM tiledata address mid-frame
	//this is only supported by the cycle-based PPU renderer
	if(title == "Winter olympics") emulator->configure("Hacks/PPU/Fast", false);

	//title screen shows remnants of the flag after choosing a language with the scanline-based renderer
	if(title == "WORLD CUP STRIKER") emulator->configure("Hacks/PPU/Fast", false);

	//relies on cycle-accurate writes to the echo buffer
	if(title == "KOUSHIEN_2") emulator->configure("Hacks/DSP/Fast", false);

	//will hang immediately
	if(title == "RENDERING RANGER R2") emulator->configure("Hacks/DSP/Fast", false);

	//will hang sometimes in the "Bach in Time" stage
	if(title == "BUBSY II" && region == "PAL") emulator->configure("Hacks/DSP/Fast", false);

	//fixes an errant scanline on the title screen due to writing to PPU registers too late
	if(title == "ADVENTURES OF FRANKEN" && region == "PAL") emulator->configure("Hacks/PPU/RenderCycle", 32);

	//fixes an errant scanline on the title screen due to writing to PPU registers too late
	if(title == "FIREPOWER 2000" || title == "SUPER SWIV") emulator->configure("Hacks/PPU/RenderCycle", 32);

	//fixes an errant scanline on the title screen due to writing to PPU registers too late
	if(title == "NHL '94" || title == "NHL PROHOCKEY'94") emulator->configure("Hacks/PPU/RenderCycle", 32);

	//fixes an errant scanline on the title screen due to writing to PPU registers too late
	if(title == "Sugoro Quest++") emulator->configure("Hacks/PPU/RenderCycle", 128);

	if (emulator->configuration("Hacks/Hotfixes")) {
		//this game transfers uninitialized memory into video RAM: this can cause a row of invalid tiles
		//to appear in the background of stage 12. this one is a bug in the original game, so only enable
		//it if the hotfixes option has been enabled.
		if(title == "The Hurricanes") emulator->configure("Hacks/Entropy", "None");

		//Frisky Tom attract sequence sometimes hangs when WRAM is initialized to pseudo-random patterns
		if (title == "ニチブツ・アーケード・クラシックス") emulator->configure("Hacks/Entropy", "None");
	}

	emulator->power();
}

auto Program::load(uint id, string name, string type, vector<string> options) -> Emulator::Platform::Load {

	if (id == 1)
	{
		if (loadSuperFamicom())
		{
			return {id, superFamicom.region};
		}
	}
	else if (id == 2)
	{
		if (loadGameBoy())
		{
			return { id, NULL };
		}
	}
	else if (id == 3)
	{
		if (loadBSMemory())
		{
			return { id, NULL };
		}
	}
	return { id, options(0) };
}

auto Program::loadSuperFamicom() -> bool
{
	vector<uint8_t>& rom = superFamicom.raw_data;
	fprintf(stderr, "location: \"%s\"\n", superFamicom.location.data());
	fprintf(stderr, "rom size: %ld\n", rom.size());

	if(rom.size() < 0x8000) return false;

	auto heuristics = Heuristics::SuperFamicom(rom, superFamicom.location);

	superFamicom.title = heuristics.title();
	superFamicom.region = heuristics.videoRegion();
	superFamicom.manifest = heuristics.manifest();

	hackPatchMemory(rom);
	superFamicom.document = BML::unserialize(superFamicom.manifest);
	fprintf(stderr, "loaded game manifest: \"\n%s\"\n", superFamicom.manifest.data());

	uint offset = 0;
	if(auto size = heuristics.programRomSize()) {
		superFamicom.program.acquire(rom.data() + offset, size);
		offset += size;
	}
	if(auto size = heuristics.dataRomSize()) {
		superFamicom.data.acquire(rom.data() + offset, size);
		offset += size;
	}
	if(auto size = heuristics.expansionRomSize()) {
		superFamicom.expansion.acquire(rom.data() + offset, size);
		offset += size;
	}
	if(auto size = heuristics.firmwareRomSize()) {
		superFamicom.firmware.acquire(rom.data() + offset, size);
		offset += size;
	}
	return true;
}

auto Program::loadGameBoy() -> bool {
	if (gameBoy.program.size() < 0x4000) return false;

	auto heuristics = Heuristics::GameBoy(gameBoy.program, gameBoy.location);

	gameBoy.manifest = heuristics.manifest();
	gameBoy.document = BML::unserialize(gameBoy.manifest);

	return true;
}

auto Program::loadBSMemory() -> bool {
	if (bsMemory.program.size() < 0x8000) return false;

	auto heuristics = Heuristics::BSMemory(bsMemory.program, gameBoy.location);

	bsMemory.manifest = heuristics.manifest();
	bsMemory.document = BML::unserialize(bsMemory.manifest);

	return true;
}

auto Program::videoFrame(const uint16* data, uint pitch, uint width, uint height, uint scale) -> void {

	// note: scale is not used currently, but as bsnes has builtin scaling support (something something mode 7)
	// we might actually wanna make use of that? also overscan might always be false rn, will need to check
	pitch >>= 1;
	if (!overscan)
	{
		uint multiplier = height / 240;
		data += 8 * pitch * multiplier;
		height -= 16 * multiplier;
	}

	fprintf(stderr, "got a video frame with dimensions h: %d, w: %d, p: %d, overscan: %d, scale: %d\n", height, width, pitch, overscan, scale);

 	snesCallbacks.snes_video_frame(data, width, height, pitch);
}

// Double the fun!
static int16_t d2i16(double v)
{
	v *= 0x8000;
	if (v > 0x7fff)
		v = 0x7fff;
	else if (v < -0x8000)
		v = -0x8000;
	return int16_t(floor(v + 0.5));
}

auto Program::audioFrame(const double* samples, uint channels) -> void
{
	int16_t left = d2i16(samples[0]);
	int16_t right = d2i16(samples[1]);
	return snesCallbacks.snes_audio_sample(left, right);
}

auto Program::notify(string message) -> void
{
	if (message == "NOTIFY NO_LAG");
		snesCallbacks.snes_no_lag();
}

auto Program::cpuTrace(vector<string> parts) -> void
{
	snesCallbacks.snes_trace(parts[0], parts[1]);
}

auto Program::getBackdropColor() -> uint16
{
	return backdropColor;
}

auto Program::inputPoll(uint port, uint device, uint input) -> int16
{
	int index = 0;
	int id = input;
	if (device == ID::Device::SuperMultitap) {
		index = input / 12;
		id = input % 12;
	} else if (device == ID::Device::Payload) {
		index = input / 16;
		id = input % 16;
	} else if (device == ID::Device::Justifiers) {
		index = input / 4;
		id = input % 4;
	}

	return snesCallbacks.snes_input_state(port, index, id);
}

auto Program::inputRumble(uint port, uint device, uint input, bool enable) -> void
{
}


auto Program::hackPatchMemory(vector<uint8_t>& data) -> void
{
	auto title = superFamicom.title;

	if(title == "Satellaview BS-X" && data.size() >= 0x100000) {
		//BS-X: Sore wa Namae o Nusumareta Machi no Monogatari (JPN) (1.1)
		//disable limited play check for BS Memory flash cartridges
		//benefit: allow locked out BS Memory flash games to play without manual header patching
		//detriment: BS Memory ROM cartridges will cause the game to hang in the load menu
		if(data[0x4a9b] == 0x10) data[0x4a9b] = 0x80;
		if(data[0x4d6d] == 0x10) data[0x4d6d] = 0x80;
		if(data[0x4ded] == 0x10) data[0x4ded] = 0x80;
		if(data[0x4e9a] == 0x10) data[0x4e9a] = 0x80;
	}
}
