#include "ZRoom.h"
#include <algorithm>
#include <cassert>
#include <chrono>
#include <cinttypes>
#include <string_view>

#include "Commands/EndMarker.h"
#include "Commands/SetActorCutsceneList.h"
#include "Commands/SetActorList.h"
#include "Commands/SetAlternateHeaders.h"
#include "Commands/SetAnimatedMaterialList.h"
#include "Commands/SetCameraSettings.h"
#include "Commands/SetCollisionHeader.h"
#include "Commands/SetCsCamera.h"
#include "Commands/SetCutscenes.h"
#include "Commands/SetEchoSettings.h"
#include "Commands/SetEntranceList.h"
#include "Commands/SetExitList.h"
#include "Commands/SetLightList.h"
#include "Commands/SetLightingSettings.h"
#include "Commands/SetMesh.h"
#include "Commands/SetMinimapChests.h"
#include "Commands/SetMinimapList.h"
#include "Commands/SetObjectList.h"
#include "Commands/SetPathways.h"
#include "Commands/SetRoomBehavior.h"
#include "Commands/SetRoomList.h"
#include "Commands/SetSkyboxModifier.h"
#include "Commands/SetSkyboxSettings.h"
#include "Commands/SetSoundSettings.h"
#include "Commands/SetSpecialObjects.h"
#include "Commands/SetStartPositionList.h"
#include "Commands/SetTimeSettings.h"
#include "Commands/SetTransitionActorList.h"
#include "Commands/SetWind.h"
#include "Commands/SetWorldMapVisited.h"
#include "Commands/Unused09.h"
#include "Commands/Unused1D.h"
#include "Commands/ZRoomCommandUnk.h"
#include "Globals.h"
#include <Utils/DiskFile.h>
#include "Utils/Path.h"
#include "Utils/StringHelper.h"
#include "WarningHandler.h"
#include "ZBlob.h"
#include "ZCutscene.h"
#include "ZFile.h"

REGISTER_ZFILENODE(Room, ZRoom);
REGISTER_ZFILENODE(Scene, ZRoom);
REGISTER_ZFILENODE(AltHeader, ZRoom);

ZRoom::ZRoom(ZFile* nParent) : ZResource(nParent)
{
	roomCount = -1;
	canHaveInner = true;
	RegisterOptionalAttribute("HackMode");
}

ZRoom::~ZRoom()
{
	for (ZRoomCommand* cmd : commands)
		delete cmd;
}

void ZRoom::ExtractFromXML(tinyxml2::XMLElement* reader, uint32_t nRawDataIndex)
{
	ZResource::ExtractFromXML(reader, nRawDataIndex);

	if (hackMode == "syotes_room")
	{
		SyotesRoomHack();
	}
	else
	{
		DeclareVar(name, "");
	}
}

void ZRoom::ExtractFromBinary(uint32_t nRawDataIndex, ZResourceType parentType)
{
	rawDataIndex = nRawDataIndex;
	name = GetDefaultName(parent->GetName());

	zroomType = ZResourceType::AltHeader;
	switch (parentType)
	{
	case ZResourceType::Scene:
	case ZResourceType::Room:
	case ZResourceType::AltHeader:
		parentZroomType = parentType;
		break;

	default:
		// TODO: error message or something
		assert(false);
		break;
	}

	ParseRawData();
	DeclareVar(name, "");
}

void ZRoom::ParseXML(tinyxml2::XMLElement* reader)
{
	ZResource::ParseXML(reader);

	// TODO: HACK: remove this specific check when the repo uses the correct HackMode="syotes_room"
	if (name == "syotes_room_0")
	{
		hackMode = "syotes_room";
	}

	std::string nodeName = std::string(reader->Name());
	if (nodeName == "Scene")
	{
		zroomType = ZResourceType::Scene;
	}
	else if (nodeName == "Room")
		zroomType = ZResourceType::Room;
	else if (nodeName == "AltHeader")
		zroomType = ZResourceType::AltHeader;

	if (reader->Attribute("HackMode") != nullptr)
	{
		hackMode = std::string(reader->Attribute("HackMode"));
		if (hackMode != "syotes_room")
		{
			std::string headerError = StringHelper::Sprintf(
				"invalid value found for 'HackMode' attribute: '%s'", hackMode.c_str());
			HANDLE_ERROR_RESOURCE(WarningType::InvalidAttributeValue, parent, this, rawDataIndex,
			                      headerError, "");
		}
	}
}

void ZRoom::ParseRawData()
{
	if (hackMode == "syotes_room")
		return;

	bool shouldContinue = true;
	uint32_t currentIndex = 0;
	uint32_t currentPtr = rawDataIndex;

	const auto& rawData = parent->GetRawData();
	while (shouldContinue)
	{
		RoomCommand opcode = static_cast<RoomCommand>(rawData.at(currentPtr));

		ZRoomCommand* cmd = nullptr;

		auto start = std::chrono::steady_clock::now();

		switch (opcode)
		{
		case RoomCommand::SetStartPositionList:
			cmd = new SetStartPositionList(parent);
			break;  // 0x00
		case RoomCommand::SetActorList:
			cmd = new SetActorList(parent);
			break;  // 0x01
		case RoomCommand::SetCsCamera:
			cmd = new SetCsCamera(parent);
			break;  // 0x02 (MM-ONLY)
		case RoomCommand::SetCollisionHeader:
			cmd = new SetCollisionHeader(parent);
			break;  // 0x03
		case RoomCommand::SetRoomList:
			cmd = new SetRoomList(parent);
			break;  // 0x04
		case RoomCommand::SetWind:
			cmd = new SetWind(parent);
			break;  // 0x05
		case RoomCommand::SetEntranceList:
			cmd = new SetEntranceList(parent);
			break;  // 0x06
		case RoomCommand::SetSpecialObjects:
			cmd = new SetSpecialObjects(parent);
			break;  // 0x07
		case RoomCommand::SetRoomBehavior:
			cmd = new SetRoomBehavior(parent);
			break;  // 0x08
		case RoomCommand::Unused09:
			cmd = new Unused09(parent);
			break;  // 0x09
		case RoomCommand::SetMesh:
			cmd = new SetMesh(parent);
			break;  // 0x0A
		case RoomCommand::SetObjectList:
			cmd = new SetObjectList(parent);
			break;  // 0x0B
		case RoomCommand::SetLightList:
			cmd = new SetLightList(parent);
			break;  // 0x0C (MM-ONLY)
		case RoomCommand::SetPathways:
			cmd = new SetPathways(parent);
			break;  // 0x0D
		case RoomCommand::SetTransitionActorList:
			cmd = new SetTransitionActorList(parent);
			break;  // 0x0E
		case RoomCommand::SetLightingSettings:
			cmd = new SetLightingSettings(parent);
			break;  // 0x0F
		case RoomCommand::SetTimeSettings:
			cmd = new SetTimeSettings(parent);
			break;  // 0x10
		case RoomCommand::SetSkyboxSettings:
			cmd = new SetSkyboxSettings(parent);
			break;  // 0x11
		case RoomCommand::SetSkyboxModifier:
			cmd = new SetSkyboxModifier(parent);
			break;  // 0x12
		case RoomCommand::SetExitList:
			cmd = new SetExitList(parent);
			break;  // 0x13
		case RoomCommand::EndMarker:
			cmd = new EndMarker(parent);
			break;  // 0x14
		case RoomCommand::SetSoundSettings:
			cmd = new SetSoundSettings(parent);
			break;  // 0x15
		case RoomCommand::SetEchoSettings:
			cmd = new SetEchoSettings(parent);
			break;  // 0x16
		case RoomCommand::SetCutscenes:
			cmd = new SetCutscenes(parent);
			break;  // 0x17
		case RoomCommand::SetAlternateHeaders:
			cmd = new SetAlternateHeaders(parent);
			break;  // 0x18
		case RoomCommand::SetCameraSettings:
			if (Globals::Instance->game == ZGame::MM_RETAIL)
				cmd = new SetWorldMapVisited(parent);
			else
				cmd = new SetCameraSettings(parent);
			break;  // 0x19
		case RoomCommand::SetAnimatedMaterialList:
			cmd = new SetAnimatedMaterialList(parent);
			break;  // 0x1A (MM-ONLY)
		case RoomCommand::SetActorCutsceneList:
			cmd = new SetActorCutsceneList(parent);
			break;  // 0x1B (MM-ONLY)
		case RoomCommand::SetMinimapList:
			cmd = new SetMinimapList(parent);
			break;  // 0x1C (MM-ONLY)
		case RoomCommand::Unused1D:
			cmd = new Unused1D(parent);
			break;  // 0x1D
		case RoomCommand::SetMinimapChests:
			cmd = new SetMinimapChests(parent);
			break;  // 0x1E (MM-ONLY)
		default:
			cmd = new ZRoomCommandUnk(parent);
		}

		cmd->commandSet = rawDataIndex;
		cmd->ExtractCommandFromRoom(this, currentPtr);

		if (Globals::Instance->profile)
		{
			auto end = std::chrono::steady_clock::now();
			int64_t diff =
				std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
			if (diff > 50)
				printf("OP: %s, TIME: %" PRIi64 "ms\n", cmd->GetCommandCName().c_str(), diff);
		}

		cmd->cmdIndex = currentIndex;

		commands.push_back(cmd);

		if (opcode == RoomCommand::EndMarker)
			shouldContinue = false;

		currentPtr += 8;
		currentIndex++;
	}
}

void ZRoom::DeclareReferences(const std::string& prefix)
{
	for (auto& cmd : commands)
		cmd->DeclareReferences(prefix);
}

void ZRoom::ParseRawDataLate()
{
	for (auto& cmd : commands)
		cmd->ParseRawDataLate();
}

void ZRoom::DeclareReferencesLate(const std::string& prefix)
{
	for (auto& cmd : commands)
		cmd->DeclareReferencesLate(prefix);
}

Declaration* ZRoom::DeclareVar(const std::string& prefix, const std::string& body)
{
	std::string auxName = name;
	if (auxName == "")
		auxName = GetDefaultName(prefix);
	if (zroomType == ZResourceType::Scene || zroomType == ZResourceType::Room)
		auxName = StringHelper::Sprintf("%sCommands", name.c_str());

	Declaration* decl =
		parent->AddDeclarationArray(rawDataIndex, GetDeclarationAlignment(), GetRawDataSize(),
	                                GetSourceTypeName(), auxName, commands.size(), body);
	decl->staticConf = staticConf;
	return decl;
}

std::string ZRoom::GetBodySourceCode() const
{
	std::string declaration;

	for (size_t i = 0; i < commands.size(); i++)
	{
		ZRoomCommand* cmd = commands[i];
		declaration += StringHelper::Sprintf("\t%s,", cmd->GetBodySourceCode().c_str());

		if (i + 1 < commands.size())
			declaration += "\n";
	}

	return declaration;
}

std::string ZRoom::GetDefaultName(const std::string& prefix) const
{
	return StringHelper::Sprintf("%sSet_%06X", prefix.c_str(), rawDataIndex);
}

/*
 * There is one room in Ocarina of Time that lacks a header. Room 120, "Syotes", dates
 * back to very early in the game's development. Since this room is a special case,
 * declare automatically the data its contains whitout the need of a header.
 */
void ZRoom::SyotesRoomHack()
{
	PolygonType2 poly(parent, 0, this);

	poly.ParseRawData();
	poly.DeclareReferences(GetName());
	parent->AddDeclaration(0, poly.GetDeclarationAlignment(), poly.GetRawDataSize(),
	                       poly.GetSourceTypeName(), poly.GetDefaultName(GetName()),
	                       poly.GetBodySourceCode());
}

ZRoomCommand* ZRoom::FindCommandOfType(RoomCommand cmdType)
{
	for (size_t i = 0; i < commands.size(); i++)
	{
		if (commands[i]->GetRoomCommand() == cmdType)
			return commands[i];
	}

	return nullptr;
}

size_t ZRoom::GetDeclarationSizeFromNeighbor(uint32_t declarationAddress)
{
	auto currentDecl = parent->declarations.find(declarationAddress);
	if (currentDecl == parent->declarations.end())
		return 0;

	auto nextDecl = currentDecl;
	std::advance(nextDecl, 1);
	if (nextDecl == parent->declarations.end())
		return parent->GetRawData().size() - currentDecl->first;

	return nextDecl->first - currentDecl->first;
}

size_t ZRoom::GetCommandSizeFromNeighbor(ZRoomCommand* cmd)
{
	int32_t cmdIndex = -1;

	for (size_t i = 0; i < commands.size(); i++)
	{
		if (commands[i] == cmd)
		{
			cmdIndex = i;
			break;
		}
	}

	if (cmdIndex != -1)
	{
		if (cmdIndex + 1 < (int32_t)commands.size())
			return commands[cmdIndex + 1]->cmdAddress - commands[cmdIndex]->cmdAddress;
		else
			return parent->GetRawData().size() - commands[cmdIndex]->cmdAddress;
	}

	return 0;
}

void ZRoom::GetSourceOutputCode([[maybe_unused]] const std::string& prefix)
{
	if (hackMode != "syotes_room")
		DeclareVar(prefix, GetBodySourceCode());
}

size_t ZRoom::GetRawDataSize() const
{
	size_t size = 0;

	for (ZRoomCommand* cmd : commands)
		size += cmd->GetRawDataSize();

	return size;
}

std::string ZRoom::GetSourceTypeName() const
{
	return "SceneCmd";
}

ZResourceType ZRoom::GetResourceType() const
{
	assert(zroomType == ZResourceType::Scene || zroomType == ZResourceType::Room ||
	       zroomType == ZResourceType::AltHeader);
	return zroomType;
}
