#include "common.h"
#include "patcher.h"
#include "CutsceneMgr.h"
#include "Directory.h"
#include "Camera.h"
#include "Streaming.h"
#include "FileMgr.h"
#include "main.h"
#include "AnimManager.h"
#include "AnimBlendAssocGroup.h"
#include "AnimBlendClumpData.h"
#include "Pad.h"
#include "DMAudio.h"
#include "World.h"
#include "PlayerPed.h"
#include "CutsceneHead.h"
#include "RpAnimBlend.h"
#include "ModelIndices.h"
#include "TempColModels.h"

const struct {
	const char *szTrackName;
	int iTrackId;
} musicNameIdAssoc[] = {
	{ "JB",			STREAMED_SOUND_NEWS_INTRO },
	{ "BET",		STREAMED_SOUND_BANK_INTRO },
	{ "L1_LG",		STREAMED_SOUND_CUTSCENE_LUIGI1_LG },
	{ "L2_DSB",		STREAMED_SOUND_CUTSCENE_LUIGI2_DSB },
	{ "L3_DM",		STREAMED_SOUND_CUTSCENE_LUIGI3_DM },
	{ "L4_PAP",		STREAMED_SOUND_CUTSCENE_LUIGI4_PAP },
	{ "L5_TFB",		STREAMED_SOUND_CUTSCENE_LUIGI5_TFB },
	{ "J0_DM2",		STREAMED_SOUND_CUTSCENE_JOEY0_DM2 },
	{ "J1_LFL",		STREAMED_SOUND_CUTSCENE_JOEY1_LFL },
	{ "J2_KCL",		STREAMED_SOUND_CUTSCENE_JOEY2_KCL },
	{ "J3_VH",		STREAMED_SOUND_CUTSCENE_JOEY3_VH },
	{ "J4_ETH",		STREAMED_SOUND_CUTSCENE_JOEY4_ETH },
	{ "J5_DST",		STREAMED_SOUND_CUTSCENE_JOEY5_DST },
	{ "J6_TBJ",		STREAMED_SOUND_CUTSCENE_JOEY6_TBJ },
	{ "T1_TOL",		STREAMED_SOUND_CUTSCENE_TONI1_TOL },
	{ "T2_TPU",		STREAMED_SOUND_CUTSCENE_TONI2_TPU },
	{ "T3_MAS",		STREAMED_SOUND_CUTSCENE_TONI3_MAS },
	{ "T4_TAT",		STREAMED_SOUND_CUTSCENE_TONI4_TAT },
	{ "T5_BF",		STREAMED_SOUND_CUTSCENE_TONI5_BF },
	{ "S0_MAS",		STREAMED_SOUND_CUTSCENE_SAL0_MAS },
	{ "S1_PF",		STREAMED_SOUND_CUTSCENE_SAL1_PF },
	{ "S2_CTG",		STREAMED_SOUND_CUTSCENE_SAL2_CTG },
	{ "S3_RTC",		STREAMED_SOUND_CUTSCENE_SAL3_RTC },
	{ "S5_LRQ",		STREAMED_SOUND_CUTSCENE_SAL5_LRQ },
	{ "S4_BDBA",	STREAMED_SOUND_CUTSCENE_SAL4_BDBA },
	{ "S4_BDBB",	STREAMED_SOUND_CUTSCENE_SAL4_BDBB },
	{ "S2_CTG2",	STREAMED_SOUND_CUTSCENE_SAL2_CTG2 },
	{ "S4_BDBD",	STREAMED_SOUND_CUTSCENE_SAL4_BDBD },
	{ "S5_LRQB",	STREAMED_SOUND_CUTSCENE_SAL5_LRQB },
	{ "S5_LRQC",	STREAMED_SOUND_CUTSCENE_SAL5_LRQC },
	{ "A1_SS0",		STREAMED_SOUND_CUTSCENE_ASUKA_1_SSO },
	{ "A2_PP",		STREAMED_SOUND_CUTSCENE_ASUKA_2_PP },
	{ "A3_SS",		STREAMED_SOUND_CUTSCENE_ASUKA_3_SS },
	{ "A4_PDR",		STREAMED_SOUND_CUTSCENE_ASUKA_4_PDR },
	{ "A5_K2FT",	STREAMED_SOUND_CUTSCENE_ASUKA_5_K2FT},
	{ "K1_KBO",		STREAMED_SOUND_CUTSCENE_KENJI1_KBO },
	{ "K2_GIS",		STREAMED_SOUND_CUTSCENE_KENJI2_GIS },
	{ "K3_DS",		STREAMED_SOUND_CUTSCENE_KENJI3_DS },
	{ "K4_SHI",		STREAMED_SOUND_CUTSCENE_KENJI4_SHI },
	{ "K5_SD",		STREAMED_SOUND_CUTSCENE_KENJI5_SD },
	{ "R0_PDR2",	STREAMED_SOUND_CUTSCENE_RAY0_PDR2 },
	{ "R1_SW",		STREAMED_SOUND_CUTSCENE_RAY1_SW },
	{ "R2_AP",		STREAMED_SOUND_CUTSCENE_RAY2_AP },
	{ "R3_ED",		STREAMED_SOUND_CUTSCENE_RAY3_ED },
	{ "R4_GF",		STREAMED_SOUND_CUTSCENE_RAY4_GF },
	{ "R5_PB",		STREAMED_SOUND_CUTSCENE_RAY5_PB },
	{ "R6_MM",		STREAMED_SOUND_CUTSCENE_RAY6_MM },
	{ "D1_STOG",	STREAMED_SOUND_CUTSCENE_DONALD1_STOG },
	{ "D2_KK",		STREAMED_SOUND_CUTSCENE_DONALD2_KK },
	{ "D3_ADO",		STREAMED_SOUND_CUTSCENE_DONALD3_ADO },
	{ "D5_ES",		STREAMED_SOUND_CUTSCENE_DONALD5_ES },
	{ "D7_MLD",		STREAMED_SOUND_CUTSCENE_DONALD7_MLD },
	{ "D4_GTA",		STREAMED_SOUND_CUTSCENE_DONALD4_GTA },
	{ "D4_GTA2",	STREAMED_SOUND_CUTSCENE_DONALD4_GTA2 },
	{ "D6_STS",		STREAMED_SOUND_CUTSCENE_DONALD6_STS },
	{ "A6_BAIT",	STREAMED_SOUND_CUTSCENE_ASUKA6_BAIT },
	{ "A7_ETG",		STREAMED_SOUND_CUTSCENE_ASUKA7_ETG },
	{ "A8_PS",		STREAMED_SOUND_CUTSCENE_ASUKA8_PS },
	{ "A9_ASD",		STREAMED_SOUND_CUTSCENE_ASUKA9_ASD },
	{ "K4_SHI2",	STREAMED_SOUND_CUTSCENE_KENJI4_SHI2 },
	{ "C1_TEX",		STREAMED_SOUND_CUTSCENE_CATALINA1_TEX },
	{ "EL_PH1",		STREAMED_SOUND_CUTSCENE_ELBURRO1_PH1 },
	{ "EL_PH2",		STREAMED_SOUND_CUTSCENE_ELBURRO2_PH2 },
	{ "EL_PH3",		STREAMED_SOUND_CUTSCENE_ELBURRO3_PH3 },
	{ "EL_PH4",		STREAMED_SOUND_CUTSCENE_ELBURRO4_PH4 },
	{ "YD_PH1",		STREAMED_SOUND_CUTSCENE_YARDIE_PH1 },
	{ "YD_PH2",		STREAMED_SOUND_CUTSCENE_YARDIE_PH2 },
	{ "YD_PH3",		STREAMED_SOUND_CUTSCENE_YARDIE_PH3 },
	{ "YD_PH4",		STREAMED_SOUND_CUTSCENE_YARDIE_PH4 },
	{ "HD_PH1",		STREAMED_SOUND_CUTSCENE_HOODS_PH1 },
	{ "HD_PH2",		STREAMED_SOUND_CUTSCENE_HOODS_PH2 },
	{ "HD_PH3",		STREAMED_SOUND_CUTSCENE_HOODS_PH3 },
	{ "HD_PH4",		STREAMED_SOUND_CUTSCENE_HOODS_PH4 },
	{ "HD_PH5",		STREAMED_SOUND_CUTSCENE_HOODS_PH5 },
	{ "MT_PH1",		STREAMED_SOUND_CUTSCENE_MARTY_PH1 },
	{ "MT_PH2",		STREAMED_SOUND_CUTSCENE_MARTY_PH2 },
	{ "MT_PH3",		STREAMED_SOUND_CUTSCENE_MARTY_PH3 },
	{ "MT_PH4",		STREAMED_SOUND_CUTSCENE_MARTY_PH4 },
	{ NULL,			NULL }
};

int
FindCutsceneAudioTrackId(const char *szCutsceneName)
{
	for (int i = 0; musicNameIdAssoc[i].szTrackName; i++) {
		if (!strcmpi(musicNameIdAssoc[i].szTrackName, szCutsceneName))
			return musicNameIdAssoc[i].iTrackId;
	}
	return -1;
}

bool &CCutsceneMgr::ms_running = *(bool*)0x95CCF5;
bool &CCutsceneMgr::ms_cutsceneProcessing = *(bool*)0x95CD9F;
CDirectory *&CCutsceneMgr::ms_pCutsceneDir = *(CDirectory**)0x8F5F88;
CCutsceneObject *(&CCutsceneMgr::ms_pCutsceneObjects)[NUMCUTSCENEOBJECTS] = *(CCutsceneObject*(*)[NUMCUTSCENEOBJECTS]) *(uintptr*) 0x862170;
int32 &CCutsceneMgr::ms_numCutsceneObjs = *(int32*)0x942FA4;
bool &CCutsceneMgr::ms_loaded = *(bool*)0x95CD95;
bool &CCutsceneMgr::ms_animLoaded = *(bool*)0x95CDA0;
bool &CCutsceneMgr::ms_useLodMultiplier = *(bool*)0x95CD74;
char(&CCutsceneMgr::ms_cutsceneName)[CUTSCENENAMESIZE] = *(char(*)[CUTSCENENAMESIZE]) *(uintptr*)0x70D9D0;
CAnimBlendAssocGroup &CCutsceneMgr::ms_cutsceneAssociations = *(CAnimBlendAssocGroup*)0x709C58;
CVector &CCutsceneMgr::ms_cutsceneOffset = *(CVector*)0x8F2C0C;
float &CCutsceneMgr::ms_cutsceneTimer = *(float*)0x941548;
uint32 &CCutsceneMgr::ms_cutsceneLoadStatus = *(uint32*)0x95CB40;

RpAtomic *
CalculateBoundingSphereRadiusCB(RpAtomic *atomic, void *data)
{
	float radius = RpAtomicGetBoundingSphereMacro(atomic)->radius;
	RwV3d center = RpAtomicGetBoundingSphereMacro(atomic)->center;

	for (RwFrame *frame = RpAtomicGetFrame(atomic); RwFrameGetParent(frame); frame = RwFrameGetParent(frame))
		RwV3dTransformPoints(&center, &center, 1, RwFrameGetMatrix(frame));

	float size = RwV3dLength(&center) + radius;
	if (size > *(float *)data)
		*(float *)data = size;
	return atomic;
}

void
CCutsceneMgr::Initialise(void)
{
	ms_numCutsceneObjs = 0;
	ms_loaded = false;
	ms_running = false;
	ms_animLoaded = false;
	ms_cutsceneProcessing = false;
	ms_useLodMultiplier = false;

	ms_pCutsceneDir = new CDirectory(CUTSCENEDIRSIZE);
	ms_pCutsceneDir->ReadDirFile("ANIM\\CUTS.DIR");
}

void
CCutsceneMgr::Shutdown(void)
{
	delete ms_pCutsceneDir;
}

void
CCutsceneMgr::LoadCutsceneData(const char *szCutsceneName)
{
	int file;
	uint32 size;
	uint32 offset;
	CPlayerPed *pPlayerPed;

	ms_cutsceneProcessing = true;
	if (!strcmpi(szCutsceneName, "jb"))
		ms_useLodMultiplier = true;
	CTimer::Stop();

	ms_pCutsceneDir->numEntries = 0;
	ms_pCutsceneDir->ReadDirFile("ANIM\\CUTS.DIR");

	CStreaming::RemoveUnusedModelsInLoadedList();
	CGame::DrasticTidyUpMemory();

	strcpy(ms_cutsceneName, szCutsceneName);
	file = CFileMgr::OpenFile("ANIM\\CUTS.IMG", "rb");

	// Load animations
	sprintf(gString, "%s.IFP", szCutsceneName);
	if (ms_pCutsceneDir->FindItem(gString, offset, size)) {
		CStreaming::MakeSpaceFor(size << 11);
		CStreaming::ImGonnaUseStreamingMemory();
		CFileMgr::Seek(file, offset << 11, SEEK_SET);
		CAnimManager::LoadAnimFile(file, false);
		ms_cutsceneAssociations.CreateAssociations(szCutsceneName);
		CStreaming::IHaveUsedStreamingMemory();
		ms_animLoaded = true;
	} else {
		ms_animLoaded = false;
	}

	// Load camera data
	sprintf(gString, "%s.DAT", szCutsceneName);
	if (ms_pCutsceneDir->FindItem(gString, offset, size)) {
		CFileMgr::Seek(file, offset << 11, SEEK_SET);
		TheCamera.LoadPathSplines(file);
	}

	CFileMgr::CloseFile(file);

	if (strcmpi(ms_cutsceneName, "end")) {
		DMAudio.ChangeMusicMode(MUSICMODE_CUTSCENE);
		int trackId = FindCutsceneAudioTrackId(szCutsceneName);
		if (trackId != -1) {
			printf("Start preload audio %s\n", szCutsceneName);
			DMAudio.PreloadCutSceneMusic(trackId);
			printf("End preload audio %s\n", szCutsceneName);
		}
	}

	ms_cutsceneTimer = 0.0f;
	ms_loaded = true;
	ms_cutsceneOffset = CVector(0.0f, 0.0f, 0.0f);

	pPlayerPed = FindPlayerPed();
	CTimer::Update();

	pPlayerPed->m_pWanted->ClearQdCrimes();
	pPlayerPed->bIsVisible = false;
	pPlayerPed->m_fCurrentStamina = pPlayerPed->m_fMaxStamina;
	CPad::GetPad(0)->DisablePlayerControls |= PLAYERCONTROL_DISABLED_80;
	CWorld::Players[CWorld::PlayerInFocus].MakePlayerSafe(true);
}

void
CCutsceneMgr::SetHeadAnim(const char *animName, CObject *pObject)
{
	CCutsceneHead *pCutsceneHead = (CCutsceneHead*)pObject;
	char szAnim[CUTSCENENAMESIZE * 2];

	sprintf(szAnim, "%s_%s", ms_cutsceneName, animName);
	pCutsceneHead->PlayAnimation(szAnim);
}

void
CCutsceneMgr::FinishCutscene()
{
	CCutsceneMgr::ms_cutsceneTimer = TheCamera.GetCutSceneFinishTime() * 0.001f;
	TheCamera.FinishCutscene();

	FindPlayerPed()->bIsVisible = true;
	CWorld::Players[CWorld::PlayerInFocus].MakePlayerSafe(false);
}

void
CCutsceneMgr::SetupCutsceneToStart(void)
{
	TheCamera.SetCamCutSceneOffSet(ms_cutsceneOffset);
	TheCamera.TakeControlWithSpline(2);
	TheCamera.SetWideScreenOn();

	ms_cutsceneOffset.z++;

	for (int i = ms_numCutsceneObjs - 1; i >= 0; i--) {
		assert(RwObjectGetType(ms_pCutsceneObjects[i]->m_rwObject) == rpCLUMP);
		if (CAnimBlendAssociation *pAnimBlendAssoc = RpAnimBlendClumpGetFirstAssociation((RpClump*)ms_pCutsceneObjects[i]->m_rwObject)) {
			assert(pAnimBlendAssoc->hierarchy->sequences[0].HasTranslation());
			ms_pCutsceneObjects[i]->GetPosition() = ms_cutsceneOffset + ((KeyFrameTrans*)pAnimBlendAssoc->hierarchy->sequences[0].GetKeyFrame(0))->translation;
			CWorld::Add(ms_pCutsceneObjects[i]);
			pAnimBlendAssoc->SetRun();
		} else {
			ms_pCutsceneObjects[i]->GetPosition() = ms_cutsceneOffset;
		}
	}

	CTimer::Update();
	CTimer::Update();
	ms_running = true;
	ms_cutsceneTimer = 0.0f;
}

void
CCutsceneMgr::SetCutsceneAnim(const char *animName, CObject *pObject)
{
	CAnimBlendAssociation *pNewAnim;
	CAnimBlendClumpData *pAnimBlendClumpData;

	assert(RwObjectGetType(pObject->m_rwObject) == rpCLUMP);
	RpAnimBlendClumpRemoveAllAssociations((RpClump*)pObject->m_rwObject);

	pNewAnim = ms_cutsceneAssociations.CopyAnimation(animName);
	pNewAnim->SetCurrentTime(0.0f);
	pNewAnim->flags |= ASSOC_HAS_TRANSLATION;
	pNewAnim->flags &= ~ASSOC_RUNNING;

	pAnimBlendClumpData = *RPANIMBLENDCLUMPDATA(pObject->m_rwObject);
	pAnimBlendClumpData->link.Prepend(&pNewAnim->link);
}

CCutsceneHead *
CCutsceneMgr::AddCutsceneHead(CObject *pObject, int modelId)
{
	CCutsceneHead *pHead = new CCutsceneHead(pObject);
	pHead->SetModelIndex(modelId);
	CWorld::Add(pHead);
	ms_pCutsceneObjects[ms_numCutsceneObjs++] = pHead;
	return pHead;
}

CCutsceneObject *
CCutsceneMgr::CreateCutsceneObject(int modelId)
{
	CBaseModelInfo *pModelInfo;
	CColModel *pColModel;
	float radius;
	RpClump *clump;
	CCutsceneObject *pCutsceneObject;

	if (modelId >= MI_CUTOBJ01 && modelId <= MI_CUTOBJ05) {
		pModelInfo = CModelInfo::GetModelInfo(modelId);
		pColModel = &CTempColModels::ms_colModelCutObj[modelId - MI_CUTOBJ01];
		radius = 0.0f;

		pModelInfo->SetColModel(pColModel);
		clump = (RpClump*)pModelInfo->GetRwObject();
		assert(RwObjectGetType(clump) == rpCLUMP);
		RpClumpForAllAtomics(clump, CalculateBoundingSphereRadiusCB, &radius);

		pColModel->boundingSphere.radius = radius;
		pColModel->boundingBox.min = CVector(-radius, -radius, -radius);
		pColModel->boundingBox.max = CVector(radius, radius, radius);
	}

	pCutsceneObject = new CCutsceneObject();
	pCutsceneObject->SetModelIndex(modelId);
	ms_pCutsceneObjects[ms_numCutsceneObjs++] = pCutsceneObject;
	return pCutsceneObject;
}

void
CCutsceneMgr::DeleteCutsceneData(void)
{
	if (!ms_loaded) return;

	ms_cutsceneProcessing = false;
	ms_useLodMultiplier = false;

	for (--ms_numCutsceneObjs; ms_numCutsceneObjs >= 0; ms_numCutsceneObjs--) {
		CWorld::Remove(ms_pCutsceneObjects[ms_numCutsceneObjs]);
		ms_pCutsceneObjects[ms_numCutsceneObjs]->DeleteRwObject();
		delete ms_pCutsceneObjects[ms_numCutsceneObjs];
	}
	ms_numCutsceneObjs = 0;

	if (ms_animLoaded)
		CAnimManager::RemoveLastAnimFile();

	ms_animLoaded = false;
	TheCamera.RestoreWithJumpCut();
	TheCamera.SetWideScreenOff();
	ms_running = false;
	ms_loaded = false;

	FindPlayerPed()->bIsVisible = true;
	CPad::GetPad(0)->DisablePlayerControls &= ~PLAYERCONTROL_DISABLED_80;
	CWorld::Players[CWorld::PlayerInFocus].MakePlayerSafe(false);

	if (strcmpi(ms_cutsceneName, "end")) {
		DMAudio.StopCutSceneMusic();
		if (strcmpi(ms_cutsceneName, "bet"))
			DMAudio.ChangeMusicMode(MUSICMODE_GAME);
	}
	CTimer::Stop();
	//TheCamera.GetScreenFadeStatus() == 2; // what for??
	CGame::DrasticTidyUpMemory();
	CTimer::Update();
}

void
CCutsceneMgr::Update(void)
{
	enum {
		CUTSCENE_LOADING_0 = 0,
		CUTSCENE_LOADING_AUDIO,
		CUTSCENE_LOADING_2,
		CUTSCENE_LOADING_3,
		CUTSCENE_LOADING_4
	};

	switch (ms_cutsceneLoadStatus) {
	case CUTSCENE_LOADING_AUDIO:
		SetupCutsceneToStart();
		if (strcmpi(ms_cutsceneName, "end"))
			DMAudio.PlayPreloadedCutSceneMusic();
		ms_cutsceneLoadStatus++;
		break;
	case CUTSCENE_LOADING_2:
	case CUTSCENE_LOADING_3:
		ms_cutsceneLoadStatus++;
		break;
	case CUTSCENE_LOADING_4:
		ms_cutsceneLoadStatus = CUTSCENE_LOADING_0;
		break;
	default:
		break;
	}

	if (!ms_running) return;

	ms_cutsceneTimer += CTimer::GetTimeStepNonClipped() * 0.02f;
	if (strcmpi(ms_cutsceneName, "end") && TheCamera.Cams[TheCamera.ActiveCam].Mode == CCam::MODE_FLYBY && ms_cutsceneLoadStatus == CUTSCENE_LOADING_0) {
		if (CPad::GetPad(0)->GetCrossJustDown()
			|| (CGame::playingIntro && CPad::GetPad(0)->GetStartJustDown())
			|| CPad::GetPad(0)->GetLeftMouseJustDown()
			|| CPad::GetPad(0)->GetPadEnterJustDown() || CPad::GetPad(0)->GetEnterJustDown() // NOTE: In original code it's a single CPad method
			|| CPad::GetPad(0)->GetCharJustDown(VK_SPACE))
			FinishCutscene();
	}
}

bool CCutsceneMgr::HasCutsceneFinished(void) { return TheCamera.GetPositionAlongSpline() == 1.0f; }

STARTPATCHES
	InjectHook(0x4045D0, &CCutsceneMgr::Initialise, PATCH_JUMP);
	InjectHook(0x404630, &CCutsceneMgr::Shutdown, PATCH_JUMP);
	InjectHook(0x404650, &CCutsceneMgr::LoadCutsceneData, PATCH_JUMP);
	InjectHook(0x405140, &CCutsceneMgr::FinishCutscene, PATCH_JUMP);
	InjectHook(0x404D80, &CCutsceneMgr::SetHeadAnim, PATCH_JUMP);
	InjectHook(0x404DC0, &CCutsceneMgr::SetupCutsceneToStart, PATCH_JUMP);
	InjectHook(0x404D20, &CCutsceneMgr::SetCutsceneAnim, PATCH_JUMP);
	InjectHook(0x404CD0, &CCutsceneMgr::AddCutsceneHead, PATCH_JUMP);
	InjectHook(0x404BE0, &CCutsceneMgr::CreateCutsceneObject, PATCH_JUMP);
	InjectHook(0x4048E0, &CCutsceneMgr::DeleteCutsceneData, PATCH_JUMP);
	InjectHook(0x404EE0, &CCutsceneMgr::Update, PATCH_JUMP);
	InjectHook(0x4051B0, &CCutsceneMgr::GetCutsceneTimeInMilleseconds, PATCH_JUMP);
	InjectHook(0x4051F0, &CCutsceneMgr::HasCutsceneFinished, PATCH_JUMP);
	InjectHook(0x404B40, &CalculateBoundingSphereRadiusCB, PATCH_JUMP);
ENDPATCHES