#include "extensions/multiplayer/animation/sceneplayer.h"

#include "3dmanager/lego3dmanager.h"
#include "anim/legoanim.h"
#include "extensions/common/animutils.h"
#include "extensions/common/charactercloner.h"
#include "extensions/multiplayer/mputils.h"
#include "legoactors.h"
#include "legoanimationmanager.h"
#include "legocameracontroller.h"
#include "legocharactermanager.h"
#include "legovideomanager.h"
#include "legoworld.h"
#include "misc.h"
#include "misc/legotree.h"
#include "mxbackgroundaudiomanager.h"
#include "mxgeometry/mxgeometry3d.h"
#include "realtime/realtime.h"
#include "roi/legoroi.h"
#include "viewmanager/viewmanager.h"

#include <SDL3/SDL_stdinc.h>
#include <SDL3/SDL_timer.h>
#include <algorithm>
#include <cmath>
#include <deque>
#include <functional>
#include <vector>

using namespace Multiplayer::Animation;
namespace AnimUtils = Extensions::Common::AnimUtils;
using Extensions::Common::CharacterCloner;

static bool MatchesCharacter(const std::string& p_actorName, int8_t p_charIndex)
{
	if (p_charIndex < 0 || p_charIndex >= (int8_t) sizeOfArray(g_actorInfoInit)) {
		return false;
	}
	return !SDL_strcasecmp(p_actorName.c_str(), g_actorInfoInit[p_charIndex].m_name);
}

ScenePlayer::ScenePlayer()
	: m_loader(nullptr), m_playing(false), m_rebaseComputed(false), m_startTime(0), m_currentData(nullptr),
	  m_category(e_npcAnim), m_animRootROI(nullptr), m_vehicleROI(nullptr), m_hiddenVehicleROI(nullptr),
	  m_roiMap(nullptr), m_roiMapSize(0), m_hasCamAnim(false), m_observerMode(false), m_hideOnStop(false)
{
}

ScenePlayer::~ScenePlayer()
{
	if (m_playing) {
		Stop();
	}
}

void ScenePlayer::SetupROIs(const AnimInfo* p_animInfo)
{
	LegoU32 numActors = m_currentData->anim->GetNumActors();
	std::vector<LegoROI*> createdROIs;
	std::vector<AnimUtils::ROIAlias> aliases;
	std::deque<std::string> aliasNames;

	std::vector<bool> participantMatched(m_participants.size(), false);

	static uint32_t s_roiCounter = 0;

	// Generate a unique ROI name to avoid collisions in the character manager
	// and ViewManager. The counter is global so names stay unique across calls.
	auto makeUniqueName = [](char* p_buf, size_t p_size, const char* p_prefix, const char* p_name) {
		SDL_snprintf(p_buf, p_size, "%s_%s_%u", p_prefix, p_name, s_roiCounter++);
	};

	// Register an alias mapping an animation actor name to an ROI whose actual
	// name differs (e.g. a participant's unique name, or a cloned scene ROI).
	auto addAlias = [&](const std::string& p_name, LegoROI* p_roi) {
		aliasNames.push_back(p_name);
		aliases.push_back({aliasNames.back().c_str(), p_roi});
		m_actorAliases.push_back({p_name, p_roi});
	};

	// Create a prop ROI from a registered LOD name. Returns nullptr if the
	// LOD isn't in the ViewLODListManager.
	auto createProp = [&](const std::string& p_name, const char* p_lodName) -> LegoROI* {
		char uniqueName[64];
		makeUniqueName(uniqueName, sizeof(uniqueName), "npc_prop", p_name.c_str());
		LegoROI* roi = CharacterManager()->CreateAutoROI(uniqueName, p_lodName, FALSE);
		if (roi) {
			roi->SetName(p_name.c_str());
			createdROIs.push_back(roi);
		}
		return roi;
	};

	// Clone a scene ROI by name. Creates an independent deep copy (shared LOD
	// geometry via refcount) with a unique name and an alias for the ROI map.
	auto cloneSceneROI = [&](const std::string& p_name) -> LegoROI* {
		const CompoundObject& sceneROIs = VideoManager()->Get3DManager()->GetLego3DView()->GetViewManager()->GetROIs();
		for (CompoundObject::const_iterator it = sceneROIs.begin(); it != sceneROIs.end(); it++) {
			LegoROI* source = (LegoROI*) *it;
			if (!source->GetName() || SDL_strcasecmp(source->GetName(), p_name.c_str())) {
				continue;
			}

			char uniqueName[64];
			makeUniqueName(uniqueName, sizeof(uniqueName), "npc_scene", p_name.c_str());

			LegoROI* clone = Multiplayer::DeepCloneROI(source, uniqueName);
			if (clone) {
				clone->SetVisibility(FALSE);
				VideoManager()->Get3DManager()->Add(*clone);
				m_clonedSceneROIs.push_back(clone);
				addAlias(p_name, clone);
			}
			return clone;
		}
		return nullptr;
	};

	for (LegoU32 i = 0; i < numActors; i++) {
		const char* actorName = m_currentData->anim->GetActorName(i);
		LegoU32 actorType = m_currentData->anim->GetActorType(i);

		if (!actorName || *actorName == '\0') {
			continue;
		}

		const char* lookupName = (*actorName == '*') ? actorName + 1 : actorName;
		std::string lowered(lookupName);
		std::transform(lowered.begin(), lowered.end(), lowered.begin(), ::tolower);

		if (actorType == LegoAnimActorEntry::e_managedLegoActor) {
			// Character actor: match to a participant or clone as NPC
			bool matched = false;

			for (size_t p = 0; p < m_participants.size(); p++) {
				if (participantMatched[p] || m_participants[p].IsSpectator()) {
					continue;
				}

				if (MatchesCharacter(lowered, m_participants[p].charIndex)) {
					participantMatched[p] = true;
					matched = true;
					addAlias(lowered, m_participants[p].roi);
					break;
				}
			}

			if (!matched) {
				char uniqueName[64];
				makeUniqueName(uniqueName, sizeof(uniqueName), "npc_char", lowered.c_str());
				LegoROI* roi = CharacterCloner::Clone(CharacterManager(), uniqueName, lowered.c_str());
				if (roi) {
					addAlias(lowered, roi);
					VideoManager()->Get3DManager()->Add(*roi);
					createdROIs.push_back(roi);
				}
			}
		}
		else if (actorType == LegoAnimActorEntry::e_managedInvisibleRoiTrimmed || actorType == LegoAnimActorEntry::e_sceneRoi1 || actorType == LegoAnimActorEntry::e_sceneRoi2) {
			createProp(lowered, Multiplayer::TrimLODSuffix(lowered).c_str());
		}
		else if (actorType == LegoAnimActorEntry::e_managedInvisibleRoi) {
			createProp(lowered, lowered.c_str());
		}
		else {
			// Type 0/1: scene actor, vehicle, or prop
			LegoROI* roi = nullptr;

			// Check if this is a vehicle actor via ModelInfo flag
			bool isVehicleActor = false;
			for (uint8_t m = 0; m < p_animInfo->m_modelCount; m++) {
				if (p_animInfo->m_models[m].m_name &&
					!SDL_strcasecmp(lowered.c_str(), p_animInfo->m_models[m].m_name) &&
					p_animInfo->m_models[m].m_unk0x2c) {
					isVehicleActor = true;
					break;
				}
			}

			// Try matching a participant's vehicle by category
			if (isVehicleActor && !m_vehicleROI) {
				MxU32 animVehicleIdx;
				if (AnimationManager()->FindVehicle(lowered.c_str(), animVehicleIdx)) {
					for (size_t p = 0; p < m_participants.size(); p++) {
						if (!m_participants[p].vehicleROI) {
							continue;
						}

						MxU32 perfVehicleIdx;
						if (AnimationManager()->FindVehicle(m_participants[p].vehicleROI->GetName(), perfVehicleIdx)) {
							if (Catalog::GetVehicleCategory((int8_t) animVehicleIdx) ==
								Catalog::GetVehicleCategory((int8_t) perfVehicleIdx)) {
								m_vehicleROI = m_participants[p].vehicleROI;
								addAlias(lowered, m_vehicleROI);
								roi = m_vehicleROI;
								break;
							}
						}
					}
				}
			}

			// Try creating from a registered LOD
			if (!roi) {
				roi = createProp(lowered, Multiplayer::TrimLODSuffix(lowered).c_str());
			}

			// Fallback: clone an existing scene ROI (for models like BIRD
			// whose LOD data is embedded in the world, not registered separately)
			if (!roi) {
				roi = cloneSceneROI(lowered);
			}

			// Final fallback: borrow local player's vehicle via alias
			if (!roi && m_participants[0].vehicleROI && !m_vehicleROI) {
				m_vehicleROI = m_participants[0].vehicleROI;
				addAlias(lowered, m_vehicleROI);
			}
		}
	}

	m_propROIs = std::move(createdROIs);

	// Find root ROI: first non-spectator participant matched to an animation actor
	LegoROI* rootROI = nullptr;
	for (size_t p = 0; p < m_participants.size(); p++) {
		if (!m_participants[p].IsSpectator() && participantMatched[p]) {
			rootROI = m_participants[p].roi;
			break;
		}
	}

	if (!rootROI && !m_participants.empty()) {
		rootROI = m_participants[0].roi;
	}

	if (!rootROI) {
		return;
	}

	m_animRootROI = rootROI;

	// Collect extra ROIs (other matched participants + props + vehicle)
	std::vector<LegoROI*> extras;
	for (size_t p = 0; p < m_participants.size(); p++) {
		if (m_participants[p].roi != rootROI && participantMatched[p]) {
			extras.push_back(m_participants[p].roi);
		}
	}
	for (auto* propROI : m_propROIs) {
		extras.push_back(propROI);
	}
	for (auto* clonedROI : m_clonedSceneROIs) {
		extras.push_back(clonedROI);
	}
	if (m_vehicleROI) {
		extras.push_back(m_vehicleROI);
	}

	delete[] m_roiMap;
	m_roiMap = nullptr;
	m_roiMapSize = 0;

	AnimUtils::BuildROIMap(
		m_currentData->anim,
		rootROI,
		extras.empty() ? nullptr : extras.data(),
		(int) extras.size(),
		m_roiMap,
		m_roiMapSize,
		aliases.empty() ? nullptr : aliases.data(),
		(int) aliases.size()
	);
}

void ScenePlayer::Play(
	const AnimInfo* p_animInfo,
	int8_t p_worldId,
	AnimCategory p_category,
	const ParticipantROI* p_participants,
	uint8_t p_participantCount,
	bool p_observerMode
)
{
	if (m_playing) {
		Stop();
	}

	if (p_participantCount == 0 || !p_participants[0].roi || !p_animInfo) {
		return;
	}

	SceneAnimData* data = m_loader->EnsureCached(p_worldId, p_animInfo->m_objectId);
	if (!data || !data->anim) {
		return;
	}

	m_currentData = data;
	m_category = p_category;
	m_hideOnStop = data->hideOnStop;
	m_observerMode = p_observerMode;

	// Build participant list with saved transforms for restoration
	for (uint8_t i = 0; i < p_participantCount; i++) {
		ParticipantROI participant;
		participant.roi = p_participants[i].roi;
		participant.vehicleROI = p_participants[i].vehicleROI;
		participant.savedTransform = p_participants[i].roi->GetLocal2World();
		participant.savedName = p_participants[i].roi->GetName();
		participant.charIndex = p_participants[i].charIndex;
		m_participants.push_back(participant);
	}

	SetupROIs(p_animInfo);

	if (!m_roiMap) {
		m_currentData = nullptr;
		m_participants.clear();
		return;
	}

	ResolvePtAtCamROIs();
	m_phonemePlayer.Init(data->phonemeTracks, m_roiMap, m_roiMapSize, m_actorAliases);
	m_audioPlayer.Init(data->audioTracks);

	// Observers and spectators don't get camera control — they watch the animation from their own viewpoint
	m_hasCamAnim =
		(!m_observerMode && !m_participants[0].IsSpectator() && m_category == e_camAnim &&
		 m_currentData->anim->GetCamAnim() != nullptr);

	if (m_category == e_camAnim && !m_observerMode && !m_participants[0].IsSpectator()) {
		// Hide the player's ride vehicle — it would remain visible at the
		// pre-animation position while the player is teleported
		LegoROI* localVehicle = m_participants[0].vehicleROI;
		if (localVehicle && localVehicle != m_vehicleROI) {
			localVehicle->SetVisibility(FALSE);
			m_hiddenVehicleROI = localVehicle;
		}
	}

	m_startTime = 0;
	m_playing = true;

	BackgroundAudioManager()->LowerVolume();
}

void ScenePlayer::ComputeRebaseMatrix()
{
	if (!m_animRootROI) {
		m_rebaseMatrix.SetIdentity();
		m_rebaseComputed = true;
		return;
	}

	// Use the root performer's saved position as the rebase anchor
	MxMatrix targetTransform;
	targetTransform.SetIdentity();
	for (const auto& p : m_participants) {
		if (p.roi == m_animRootROI) {
			targetTransform = p.savedTransform;
			break;
		}
	}

	// Find the root ROI's world transform at time 0 by walking the animation tree
	std::function<bool(LegoTreeNode*, MxMatrix&)> findOrigin = [&](LegoTreeNode* node, MxMatrix& parentWorld) -> bool {
		LegoAnimNodeData* data = (LegoAnimNodeData*) node->GetData();
		MxU32 roiIdx = data ? data->GetROIIndex() : 0;

		MxMatrix localMat;
		LegoROI::CreateLocalTransform(data, 0, localMat);
		MxMatrix worldMat;
		worldMat.Product(localMat, parentWorld);

		if (roiIdx != 0 && m_roiMap[roiIdx] == m_animRootROI) {
			m_animPose0 = worldMat;
			return true;
		}
		for (LegoU32 i = 0; i < node->GetNumChildren(); i++) {
			if (findOrigin(node->GetChild(i), worldMat)) {
				return true;
			}
		}
		return false;
	};
	MxMatrix identity;
	identity.SetIdentity();
	findOrigin(m_currentData->anim->GetRoot(), identity);

	// Inverse of animPose0 (rigid body: transpose rotation, negate translated position)
	MxMatrix invAnimPose0;
	invAnimPose0.SetIdentity();
	for (int r = 0; r < 3; r++) {
		for (int c = 0; c < 3; c++) {
			invAnimPose0[r][c] = m_animPose0[c][r];
		}
	}
	for (int r = 0; r < 3; r++) {
		invAnimPose0[3][r] =
			-(invAnimPose0[0][r] * m_animPose0[3][0] + invAnimPose0[1][r] * m_animPose0[3][1] +
			  invAnimPose0[2][r] * m_animPose0[3][2]);
	}

	m_rebaseMatrix.Product(invAnimPose0, targetTransform);
	m_rebaseComputed = true;
}

void ScenePlayer::ResolvePtAtCamROIs()
{
	m_ptAtCamROIs.clear();
	if (!m_currentData || m_currentData->ptAtCamNames.empty() || !m_roiMap) {
		return;
	}

	for (const auto& name : m_currentData->ptAtCamNames) {
		for (MxU32 i = 1; i < m_roiMapSize; i++) {
			if (m_roiMap[i] && m_roiMap[i]->GetName() && !SDL_strcasecmp(name.c_str(), m_roiMap[i]->GetName())) {
				m_ptAtCamROIs.push_back(m_roiMap[i]);
				break;
			}
		}
	}
}

void ScenePlayer::ApplyPtAtCam()
{
	if (m_ptAtCamROIs.empty()) {
		return;
	}

	// Find the spectator participant — PTATCAM tracks the spectator's position
	// instead of the camera so the actor faces the same target for all players.
	LegoROI* spectatorROI = nullptr;
	for (const auto& p : m_participants) {
		if (p.IsSpectator() && p.roi) {
			spectatorROI = p.roi;
			break;
		}
	}

	if (!spectatorROI) {
		return;
	}

	// Same math as LegoAnimPresenter::PutFrame
	for (LegoROI* roi : m_ptAtCamROIs) {
		if (!roi) {
			continue;
		}

		MxMatrix mat(roi->GetLocal2World());

		Vector3 pos(mat[0]);
		Vector3 dir(mat[1]);
		Vector3 up(mat[2]);
		Vector3 und(mat[3]);

		float possqr = sqrt(pos.LenSquared());
		float dirsqr = sqrt(dir.LenSquared());
		float upsqr = sqrt(up.LenSquared());

		up = und;
		up -= spectatorROI->GetWorldPosition();
		dir /= dirsqr;
		pos.EqualsCross(dir, up);
		pos.Unitize();
		up.EqualsCross(pos, dir);
		pos *= possqr;
		dir *= dirsqr;
		up *= upsqr;

		roi->SetLocal2World(mat);
		roi->WrappedUpdateWorldData();
	}
}

void ScenePlayer::Tick()
{
	if (!m_playing || !m_currentData || m_participants.empty()) {
		return;
	}

	if (m_startTime == 0) {
		m_startTime = SDL_GetTicks();
	}

	if (m_category == e_npcAnim && m_roiMap) {
		AnimUtils::EnsureROIMapVisibility(m_roiMap, m_roiMapSize);
	}

	float elapsed = (float) (SDL_GetTicks() - m_startTime);

	if (elapsed >= m_currentData->duration) {
		Stop();
		return;
	}

	// 1. Skeletal animation
	if (m_currentData->anim && m_roiMap) {
		if (!m_rebaseComputed) {
			if (m_category == e_camAnim) {
				// cam_anims use the action transform directly (keyframes are in world space)
				if (m_currentData->actionTransform.valid) {
					Mx3DPointFloat loc(
						m_currentData->actionTransform.location[0],
						m_currentData->actionTransform.location[1],
						m_currentData->actionTransform.location[2]
					);
					Mx3DPointFloat dir(
						m_currentData->actionTransform.direction[0],
						m_currentData->actionTransform.direction[1],
						m_currentData->actionTransform.direction[2]
					);
					Mx3DPointFloat up(
						m_currentData->actionTransform.up[0],
						m_currentData->actionTransform.up[1],
						m_currentData->actionTransform.up[2]
					);
					CalcLocalTransform(loc, dir, up, m_rebaseMatrix);
				}
				else {
					m_rebaseMatrix.SetIdentity();
				}
				m_rebaseComputed = true;
			}
			else {
				ComputeRebaseMatrix();
			}
		}

		AnimUtils::ApplyTree(m_currentData->anim, m_rebaseMatrix, (LegoTime) elapsed, m_roiMap);
	}

	// 2. Camera animation (cam_anim only)
	if (m_hasCamAnim) {
		MxMatrix camTransform(m_rebaseMatrix);
		m_currentData->anim->GetCamAnim()->CalculateCameraTransform((LegoFloat) elapsed, camTransform);

		LegoWorld* world = CurrentWorld();
		if (world && world->GetCameraController()) {
			world->GetCameraController()->TransformPointOfView(camTransform, FALSE);
		}
	}

	// 3. PTATCAM post-processing
	ApplyPtAtCam();

	// 4. Audio
	const char* audioROIName = m_animRootROI ? m_animRootROI->GetName() : nullptr;
	m_audioPlayer.Tick(elapsed, audioROIName);

	// 5. Phoneme frames
	m_phonemePlayer.Tick(elapsed, m_currentData->phonemeTracks);
}

void ScenePlayer::Stop()
{
	if (!m_playing) {
		return;
	}

	m_audioPlayer.Cleanup();
	m_phonemePlayer.Cleanup();

	if (m_hideOnStop && m_roiMap) {
		for (MxU32 i = 1; i < m_roiMapSize; i++) {
			if (m_roiMap[i]) {
				m_roiMap[i]->SetVisibility(FALSE);
			}
		}
	}

	if (m_hiddenVehicleROI) {
		m_hiddenVehicleROI->SetVisibility(TRUE);
		m_hiddenVehicleROI = nullptr;
	}

	CleanupProps();
	m_vehicleROI = nullptr;

	delete[] m_roiMap;
	m_roiMap = nullptr;
	m_roiMapSize = 0;

	for (auto& p : m_participants) {
		if (p.roi) {
			p.roi->WrappedSetLocal2WorldWithWorldDataUpdate(p.savedTransform);
			p.roi->SetVisibility(TRUE);
		}
	}
	m_participants.clear();

	BackgroundAudioManager()->RaiseVolume();

	m_ptAtCamROIs.clear();
	m_actorAliases.clear();
	m_playing = false;
	m_rebaseComputed = false;
	m_currentData = nullptr;
	m_animRootROI = nullptr;
	m_hasCamAnim = false;
	m_observerMode = false;
	m_startTime = 0;
	m_hideOnStop = false;
}

void ScenePlayer::NotifyROIDestroyed(LegoROI* p_roi)
{
	if (!m_playing || !p_roi) {
		return;
	}

	// Walk the m_roiMap once to find p_roi and all its descendants (child ROIs
	// are destroyed together with their parent). Collect them so every other
	// field can be cleaned with simple pointer equality — the ancestor walk
	// happens in exactly one place.
	std::vector<LegoROI*> destroyed;
	destroyed.push_back(p_roi);

	if (m_roiMap) {
		for (MxU32 i = 0; i < m_roiMapSize; i++) {
			if (!m_roiMap[i]) {
				continue;
			}

			for (OrientableROI* cur = m_roiMap[i]; cur != nullptr; cur = cur->GetParentROI()) {
				if (cur == p_roi) {
					if (m_roiMap[i] != p_roi) {
						destroyed.push_back(m_roiMap[i]);
					}
					m_roiMap[i] = nullptr;
					break;
				}
			}
		}
	}

	auto isDestroyed = [&destroyed](LegoROI* roi) {
		for (LegoROI* d : destroyed) {
			if (roi == d) {
				return true;
			}
		}
		return false;
	};

	for (auto& p : m_participants) {
		if (p.roi && isDestroyed(p.roi)) {
			p.roi = nullptr;
		}
		if (p.vehicleROI && isDestroyed(p.vehicleROI)) {
			p.vehicleROI = nullptr;
		}
	}

	for (auto& roi : m_ptAtCamROIs) {
		if (roi && isDestroyed(roi)) {
			roi = nullptr;
		}
	}

	if (m_animRootROI && isDestroyed(m_animRootROI)) {
		m_animRootROI = nullptr;
	}

	if (m_vehicleROI && isDestroyed(m_vehicleROI)) {
		m_vehicleROI = nullptr;
	}

	for (LegoROI* d : destroyed) {
		m_phonemePlayer.NotifyROIDestroyed(d);
	}
}

void ScenePlayer::CleanupProps()
{
	for (auto* propROI : m_propROIs) {
		if (propROI) {
			CharacterManager()->ReleaseAutoROI(propROI);
		}
	}
	m_propROIs.clear();

	for (auto* clonedROI : m_clonedSceneROIs) {
		if (clonedROI) {
			VideoManager()->Get3DManager()->Remove(*clonedROI);
			delete clonedROI;
		}
	}
	m_clonedSceneROIs.clear();
}
