/* Copyright (C) 2012 Wildfire Games.
 * This file is part of 0 A.D.
 *
 * 0 A.D. is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * 0 A.D. is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with 0 A.D.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "precompiled.h"

#include "UnitAnimation.h"

#include "graphics/Model.h"
#include "graphics/ObjectEntry.h"
#include "graphics/SkeletonAnim.h"
#include "graphics/SkeletonAnimDef.h"
#include "graphics/Unit.h"
#include "lib/rand.h"
#include "ps/CStr.h"
#include "ps/Game.h"
#include "simulation2/Simulation2.h"
#include "simulation2/components/ICmpSoundManager.h"

// Randomly modify the speed, so that units won't stay perfectly
// synchronised if they're playing animations of the same length
static float DesyncSpeed(float speed, float desync)
{
	if (desync == 0.0f)
		return speed;

	return speed * (1.f - desync + 2.f*desync*(rand(0, 256)/255.f));
}

CUnitAnimation::CUnitAnimation(entity_id_t ent, CModel* model, CObjectEntry* object)
	: m_Entity(ent), m_State("idle"), m_Looping(true),
	  m_Speed(1.f), m_SyncRepeatTime(0.f), m_OriginalSpeed(1.f), m_Desync(0.f)
{
	ReloadUnit(model, object);
}

void CUnitAnimation::SetEntityID(entity_id_t ent)
{
	m_Entity = ent;
}

void CUnitAnimation::AddModel(CModel* model, const CObjectEntry* object)
{
	SModelAnimState state;

	state.anims = object->GetAnimations(m_State);

	if (state.anims.empty())
		state.anims = object->GetAnimations("idle");
	ENSURE(!state.anims.empty()); // there must always be an idle animation

	state.model = model;
	state.animIdx = rand(0, state.anims.size());
	state.time = 0.f;
	state.pastLoadPos = false;
	state.pastActionPos = false;

	m_AnimStates.push_back(state);

	model->SetAnimation(state.anims[state.animIdx], !m_Looping);

	// Recursively add all props
	const std::vector<CModel::Prop>& props = model->GetProps();
	for (std::vector<CModel::Prop>::const_iterator it = props.begin(); it != props.end(); ++it)
	{
		CModel* propModel = it->m_Model->ToCModel();
		if (propModel)
			AddModel(propModel, it->m_ObjectEntry);
	}
}

void CUnitAnimation::ReloadUnit(CModel* model, const CObjectEntry* object)
{
	m_Model = model;
	m_Object = object;

	m_AnimStates.clear();
	AddModel(m_Model, m_Object);
}

void CUnitAnimation::SetAnimationState(const CStr& name, bool once, float speed, float desync, const CStrW& actionSound)
{
	m_Looping = !once;
	m_OriginalSpeed = speed;
	m_Desync = desync;
	m_ActionSound = actionSound;

	m_Speed = DesyncSpeed(m_OriginalSpeed, m_Desync);
	m_SyncRepeatTime = 0.f;

	if (name != m_State)
	{
		m_State = name;

		ReloadUnit(m_Model, m_Object);
	}
}

void CUnitAnimation::SetAnimationSyncRepeat(float repeatTime)
{
	m_SyncRepeatTime = repeatTime;
}

void CUnitAnimation::SetAnimationSyncOffset(float actionTime)
{
	// Update all the synced prop models to each coincide with actionTime
	for (std::vector<SModelAnimState>::iterator it = m_AnimStates.begin(); it != m_AnimStates.end(); ++it)
	{
		CSkeletonAnimDef* animDef = it->anims[it->animIdx]->m_AnimDef;
		if (animDef == NULL)
			continue; // ignore static animations

		float duration = animDef->GetDuration();

		float actionPos = it->anims[it->animIdx]->m_ActionPos;
		bool hasActionPos = (actionPos != -1.f);

		if (!hasActionPos)
			continue;

		float speed = duration / m_SyncRepeatTime;

		// Need to offset so that start+actionTime*speed = actionPos
		float start = actionPos - actionTime*speed;
		// Wrap it so that it's within the animation
		start = fmodf(start, duration);
		if (start < 0)
			start += duration;

		it->time = start;
	}
}

void CUnitAnimation::Update(float time)
{
	// Advance all of the prop models independently
	for (std::vector<SModelAnimState>::iterator it = m_AnimStates.begin(); it != m_AnimStates.end(); ++it)
	{
		CSkeletonAnimDef* animDef = it->anims[it->animIdx]->m_AnimDef;
		if (animDef == NULL)
			continue; // ignore static animations

		float duration = animDef->GetDuration();

		float actionPos = it->anims[it->animIdx]->m_ActionPos;
		float loadPos = it->anims[it->animIdx]->m_ActionPos2;
		bool hasActionPos = (actionPos != -1.f);
		bool hasLoadPos = (loadPos != -1.f);

		// Find the current animation speed
		float speed;
		if (m_SyncRepeatTime && hasActionPos)
			speed = duration / m_SyncRepeatTime;
		else
			speed = m_Speed * it->anims[it->animIdx]->m_Speed;

		// Convert from real time to scaled animation time
		float advance = time * speed;

		// If we're going to advance past the load point in this update, then load the ammo
		if (hasLoadPos && !it->pastLoadPos && it->time + advance >= loadPos)
		{
			it->model->ShowAmmoProp();
			it->pastLoadPos = true;
		}

		// If we're going to advance past the action point in this update, then perform the action
		if (hasActionPos && !it->pastActionPos && it->time + advance >= actionPos)
		{
			if (hasLoadPos)
				it->model->HideAmmoProp();

			if (!m_ActionSound.empty())
			{
				CmpPtr<ICmpSoundManager> cmpSoundManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY);
				if (cmpSoundManager)
					cmpSoundManager->PlaySoundGroup(m_ActionSound, m_Entity);
			}

			it->pastActionPos = true;
		}

		if (it->time + advance < duration)
		{
			// If we're still within the current animation, then simply update it
			it->time += advance;
			it->model->UpdateTo(it->time);
		}
		else if (m_Looping)
		{
			// If we've finished the current animation and want to loop...

			// Wrap the timer around
			it->time = fmod(it->time + advance, duration);

			// If there's a choice of multiple animations, pick a new random one
			if (it->anims.size() > 1)
			{
				size_t newAnimIdx = rand(0, it->anims.size());
				if (newAnimIdx != it->animIdx)
				{
					it->animIdx = newAnimIdx;
					it->model->SetAnimation(it->anims[it->animIdx], !m_Looping);
				}
			}

			it->pastActionPos = false;
			it->pastLoadPos = false;

			it->model->UpdateTo(it->time);
		}
		else
		{
			// If we've finished the current animation and don't want to loop...

			// Update to very nearly the end of the last frame (but not quite the end else we'll wrap around when skinning)
			float nearlyEnd = duration - 1.f;
			if (fabs(it->time - nearlyEnd) > 1.f)
			{
				it->time = nearlyEnd;
				it->model->UpdateTo(it->time);
			}
		}
	}
}
