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

#include "extensions/multiplayer/animation/loader.h"
#include "flic.h"
#include "legocharactermanager.h"
#include "misc.h"
#include "misc/legocontainer.h"
#include "mxbitmap.h"
#include "roi/legoroi.h"

#include <SDL3/SDL_stdinc.h>

using namespace Multiplayer::Animation;

// Find the ROI matching a phoneme track's roiName.
// Check actor aliases first (participant ROIs whose names differ from animation actor names),
// then fall back to a direct name search in the roiMap.
static LegoROI* FindTrackROI(
	const std::string& p_roiName,
	LegoROI** p_roiMap,
	MxU32 p_roiMapSize,
	const std::vector<std::pair<std::string, LegoROI*>>& p_actorAliases
)
{
	if (p_roiName.empty() || !p_roiMap) {
		return nullptr;
	}

	for (const auto& alias : p_actorAliases) {
		if (!SDL_strcasecmp(p_roiName.c_str(), alias.first.c_str())) {
			return alias.second;
		}
	}

	for (MxU32 i = 1; i < p_roiMapSize; i++) {
		if (p_roiMap[i] && p_roiMap[i]->GetName() && !SDL_strcasecmp(p_roiName.c_str(), p_roiMap[i]->GetName())) {
			return p_roiMap[i];
		}
	}
	return nullptr;
}

void PhonemePlayer::Init(
	const std::vector<SceneAnimData::PhonemeTrack>& p_tracks,
	LegoROI** p_roiMap,
	MxU32 p_roiMapSize,
	const std::vector<std::pair<std::string, LegoROI*>>& p_actorAliases
)
{
	for (size_t trackIdx = 0; trackIdx < p_tracks.size(); trackIdx++) {
		auto& track = p_tracks[trackIdx];
		PhonemeState state;
		state.targetROI = nullptr;
		state.originalTexture = nullptr;
		state.cachedTexture = nullptr;
		state.bitmap = nullptr;
		state.currentFrame = -1;

		// Resolve the target ROI from the track's roiName via aliases or roiMap
		LegoROI* targetROI = FindTrackROI(track.roiName, p_roiMap, p_roiMapSize, p_actorAliases);
		if (!targetROI) {
			m_states.push_back(state);
			continue;
		}
		state.targetROI = targetROI;

		// If a previous track already set up a cached texture for this ROI, reuse it.
		// Otherwise the second track's "original" would be the first track's cached texture,
		// causing a use-after-free during cleanup.
		PhonemeState* existing = nullptr;
		for (size_t j = 0; j < m_states.size(); j++) {
			if (m_states[j].targetROI == targetROI && m_states[j].cachedTexture) {
				existing = &m_states[j];
				break;
			}
		}

		if (existing) {
			state.cachedTexture = existing->cachedTexture;
			state.bitmap = new MxBitmap();
			state.bitmap->SetSize(track.width, track.height, nullptr, FALSE);
			m_states.push_back(state);
			continue;
		}

		LegoROI* head = targetROI->FindChildROI("head", targetROI);
		if (!head) {
			m_states.push_back(state);
			continue;
		}

		LegoTextureInfo* originalInfo = nullptr;
		head->GetTextureInfo(originalInfo);
		if (!originalInfo) {
			m_states.push_back(state);
			continue;
		}
		state.originalTexture = originalInfo;

		LegoTextureInfo* cached = TextureContainer()->GetCached(originalInfo);
		if (!cached) {
			m_states.push_back(state);
			continue;
		}
		state.cachedTexture = cached;

		CharacterManager()->SetHeadTexture(targetROI, cached);

		state.bitmap = new MxBitmap();
		state.bitmap->SetSize(track.width, track.height, nullptr, FALSE);

		m_states.push_back(state);
	}
}

void PhonemePlayer::Tick(float p_elapsedMs, const std::vector<SceneAnimData::PhonemeTrack>& p_tracks)
{
	for (size_t i = 0; i < p_tracks.size() && i < m_states.size(); i++) {
		auto& track = p_tracks[i];
		auto& state = m_states[i];

		if (!state.bitmap || !state.cachedTexture) {
			continue;
		}

		float trackElapsed = p_elapsedMs - (float) track.timeOffset;
		if (trackElapsed < 0.0f) {
			continue;
		}

		if (track.flcHeader->speed == 0) {
			continue;
		}

		int targetFrame = (int) (trackElapsed / (float) track.flcHeader->speed);
		if (targetFrame == state.currentFrame) {
			continue;
		}
		if (targetFrame >= (int) track.frameData.size()) {
			continue;
		}

		int startFrame = state.currentFrame + 1;
		if (startFrame < 0) {
			startFrame = 0;
		}

		for (int f = startFrame; f <= targetFrame; f++) {
			const auto& data = track.frameData[f];
			if (data.size() < sizeof(MxS32)) {
				continue;
			}

			MxS32 rectCount;
			SDL_memcpy(&rectCount, data.data(), sizeof(MxS32));
			size_t headerSize = sizeof(MxS32) + rectCount * sizeof(MxRect32);
			if (data.size() <= headerSize) {
				continue;
			}

			FLIC_FRAME* flcFrame = (FLIC_FRAME*) (data.data() + headerSize);

			BYTE decodedColorMap;
			DecodeFLCFrame(
				&state.bitmap->GetBitmapInfo()->m_bmiHeader,
				state.bitmap->GetImage(),
				track.flcHeader,
				flcFrame,
				&decodedColorMap
			);

			// When the FLC frame updates the palette, apply it to the texture surface
			if (decodedColorMap && state.cachedTexture->m_palette) {
				PALETTEENTRY entries[256];
				RGBQUAD* colors = state.bitmap->GetBitmapInfo()->m_bmiColors;
				for (int c = 0; c < 256; c++) {
					entries[c].peRed = colors[c].rgbRed;
					entries[c].peGreen = colors[c].rgbGreen;
					entries[c].peBlue = colors[c].rgbBlue;
					entries[c].peFlags = PC_NONE;
				}
				state.cachedTexture->m_palette->SetEntries(0, 0, 256, entries);
			}
		}

		state.cachedTexture->LoadBits(state.bitmap->GetImage());
		state.currentFrame = targetFrame;
	}
}

void PhonemePlayer::NotifyROIDestroyed(LegoROI* p_roi)
{
	for (auto& state : m_states) {
		if (state.targetROI == p_roi) {
			state.targetROI = nullptr;
		}
	}
}

void PhonemePlayer::Cleanup()
{
	for (size_t i = 0; i < m_states.size(); i++) {
		auto& state = m_states[i];

		// Only the state that owns the original texture (i.e. performed the initial setup)
		// should restore and erase. Other states sharing the same cachedTexture are secondary.
		if (state.targetROI && state.originalTexture) {
			CharacterManager()->SetHeadTexture(state.targetROI, state.originalTexture);
		}

		if (state.originalTexture && state.cachedTexture) {
			TextureContainer()->EraseCached(state.cachedTexture);
		}

		delete state.bitmap;
	}
	m_states.clear();
}
