//Copyright Paul Reiche, Fred Ford. 1992-2002

/*
 *  This program 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.
 *
 *  This program 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 this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#define COMM_INTERNAL
#include "commanim.h"

#include "comm.h"
#include "element.h"
#include "setup.h"
#include "libs/compiler.h"
#include "libs/graphics/cmap.h"
#include "libs/mathlib.h"


static TimeCount LastTime;
static SEQUENCE Sequences[MAX_ANIMATIONS + 2];
		// 2 extra for Talk and Transition animations
static DWORD ActiveMask;
		// Bit mask of all animations that are currently active.
		// Bit 'i' is set if the animation with index 'i' is active.
static ANIMATION_DESC TalkDesc;
static ANIMATION_DESC TransitDesc;
static SEQUENCE* Talk;
static SEQUENCE* Transit;
static COUNT FirstAmbient;
static COUNT TotalSequences;


static inline DWORD
randomFrameRate (SEQUENCE *pSeq)
{
	ANIMATION_DESC *ADPtr = pSeq->ADPtr;

	return ADPtr->BaseFrameRate	+
			TFB_Random () % (ADPtr->RandomFrameRate + 1);
}

static inline DWORD
randomRestartRate (SEQUENCE *pSeq)
{
	ANIMATION_DESC *ADPtr = pSeq->ADPtr;

	return ADPtr->BaseRestartRate +
			TFB_Random () % (ADPtr->RandomRestartRate + 1);
}

static inline COUNT
randomFrameIndex (SEQUENCE *pSeq, COUNT from)
{
	ANIMATION_DESC *ADPtr = pSeq->ADPtr;

	return from	+ TFB_Random () % (ADPtr->NumFrames - from);
}

static void
SetupAmbientSequences (SEQUENCE *pSeq, COUNT Num)
{
	COUNT i;
	
	for (i = 0; i < Num; ++i, ++pSeq)
	{
		ANIMATION_DESC *ADPtr = &CommData.AlienAmbientArray[i];

		memset (pSeq, 0, sizeof (*pSeq));

		pSeq->ADPtr = ADPtr;
		if (ADPtr->AnimFlags & COLORXFORM_ANIM)
			pSeq->AnimType = COLOR_ANIM;
		else
			pSeq->AnimType = PICTURE_ANIM;
		pSeq->Direction = UP_DIR;
		pSeq->FramesLeft = ADPtr->NumFrames;
		// Default: first frame is neutral
		if (ADPtr->AnimFlags & RANDOM_ANIM)
		{	// Set a random frame/colormap
			pSeq->NextIndex = TFB_Random () % ADPtr->NumFrames;
		}
		else if (ADPtr->AnimFlags & YOYO_ANIM)
		{	// Skip the first frame/colormap (it's neutral)
			pSeq->NextIndex = 1;
			--pSeq->FramesLeft;
		}
		else if (ADPtr->AnimFlags & CIRCULAR_ANIM)
		{	// Exception that makes everything more painful:
			// *Last* frame is neutral
			pSeq->CurIndex = ADPtr->NumFrames - 1;
			pSeq->NextIndex = 0;
		}

		pSeq->Alarm = randomRestartRate (pSeq) + 1;
	}
}

static void
SetupTalkSequence (SEQUENCE *pSeq, ANIMATION_DESC *ADPtr)
{
	memset (pSeq, 0, sizeof (*pSeq));
	// Initially disabled, and until needed
	ADPtr->AnimFlags |= ANIM_DISABLED;
	pSeq->ADPtr = ADPtr;
	pSeq->AnimType = PICTURE_ANIM;
}

static inline BOOLEAN
animAtNeutralIndex (SEQUENCE *pSeq)
{
	ANIMATION_DESC *ADPtr = pSeq->ADPtr;

	if (ADPtr->AnimFlags & CIRCULAR_ANIM)
	{	// CIRCULAR_ANIM's neutral frame is the last
		return pSeq->NextIndex == 0;
	}
	else
	{	// All others, neutral frame is the first
		return pSeq->CurIndex == 0;
	}
}

static inline BOOLEAN
conflictsWithTalkingAnim (SEQUENCE *pSeq)
{
	ANIMATION_DESC *ADPtr = pSeq->ADPtr;

	return ADPtr->AnimFlags & CommData.AlienTalkDesc.AnimFlags & WAIT_TALKING;
}

static void
ProcessColormapAnims (SEQUENCE *pSeq, COUNT Num)
{
	COUNT i;

	for (i = 0; i < Num; ++i, ++pSeq)
	{
		ANIMATION_DESC *ADPtr = pSeq->ADPtr;

		if ((ADPtr->AnimFlags & ANIM_DISABLED)
				|| pSeq->AnimType != COLOR_ANIM
				|| !pSeq->Change)
			continue;

		XFormColorMap (GetColorMapAddress (
				SetAbsColorMapIndex (CommData.AlienColorMap,
				ADPtr->StartIndex + pSeq->CurIndex)),
				pSeq->Alarm - 1);
		pSeq->Change = FALSE;
	}
}

static BOOLEAN
AdvanceAmbientSequence (SEQUENCE *pSeq)
{
	BOOLEAN active;
	ANIMATION_DESC *ADPtr = pSeq->ADPtr;

	--pSeq->FramesLeft;
	// YOYO_ANIM does not actually end until it comes back
	// in reverse direction, even if FramesLeft gets to 0 here
	if (pSeq->FramesLeft
			|| ((ADPtr->AnimFlags & YOYO_ANIM) && pSeq->NextIndex != 0))
	{
		active = TRUE;
		pSeq->Alarm = randomFrameRate (pSeq) + 1;
	}
	else
	{	// last animation frame
		active = FALSE;
		pSeq->Alarm = randomRestartRate (pSeq) + 1;

		// RANDOM_ANIM must end on a neutral frame
		if (ADPtr->AnimFlags & RANDOM_ANIM)
			pSeq->NextIndex = 0;
	}

	// Will draw the next frame or change to next colormap
	pSeq->CurIndex = pSeq->NextIndex;
	pSeq->Change = TRUE;

	if (pSeq->FramesLeft == 0)
	{	// Animation ended
		// Set it up for the next round
		pSeq->FramesLeft = ADPtr->NumFrames;

		if (ADPtr->AnimFlags & YOYO_ANIM)
		{	// YOYO_ANIM never draws the first frame
			// ("first" depends on direction)
			--pSeq->FramesLeft;
			pSeq->Direction = -pSeq->Direction;
		}
		else if (ADPtr->AnimFlags & CIRCULAR_ANIM)
		{	// Rewind the CIRCULAR_ANIM
			// NextIndex will be brought to 0 just below
			pSeq->NextIndex = -1;
		}
		// RANDOM_ANIM is setup just below
	}

	if (ADPtr->AnimFlags & RANDOM_ANIM)
		pSeq->NextIndex = randomFrameIndex (pSeq, 0);
	else
		pSeq->NextIndex += pSeq->Direction;

	return active;
}

static void
ResetSequence (SEQUENCE *pSeq)
{
	// Reset the animation and cause a redraw of the neutral frame,
	// assuming it is not ANIM_DISABLED
	// NOTE: This does not handle CIRCULAR_ANIM properly
	pSeq->Direction = NO_DIR;
	pSeq->CurIndex = 0;
	pSeq->Change = TRUE;
}

static void
AdvanceTalkingSequence (SEQUENCE *pSeq, DWORD ElapsedTicks)
{
	// We use the actual descriptor for flags processing and
	// a copied one for drawing. A copied one is updated only
	// when it is safe to do so.
	ANIMATION_DESC *ADPtr = pSeq->ADPtr;
	
	if (pSeq->Direction == NO_DIR)
	{	// just starting now
		pSeq->Direction = UP_DIR;
		// It's now safe to pick up new Talk descriptor if changed
		// (e.g. Zoq and Pik taking turns to talk)
		if (CommData.AlienTalkDesc.StartIndex != ADPtr->StartIndex)
		{	// copy the new one
			*ADPtr = CommData.AlienTalkDesc;
		}

		assert (pSeq->CurIndex == 0);
		pSeq->Alarm = 0; // now!
		ADPtr->AnimFlags &= ~ANIM_DISABLED;
	}

	if (pSeq->Alarm > ElapsedTicks)
	{	// Not time yet
		pSeq->Alarm -= ElapsedTicks;
		return;
	}

	// Time to start or advance the animation
	pSeq->Alarm = randomFrameRate (pSeq);
	pSeq->Change = TRUE;
	// Talking animation is like RANDOM_ANIM, except that
	// random frames always alternate with the neutral one
	// The animation does not stop until we reset it
	if (pSeq->CurIndex == 0)
	{	// random frame next
		pSeq->CurIndex = randomFrameIndex (pSeq, 1);
		pSeq->Alarm += randomRestartRate (pSeq);
	}
	else
	{	// neutral frame next
		pSeq->CurIndex = 0;
	}
}

static BOOLEAN
AdvanceTransitSequence (SEQUENCE *pSeq, DWORD ElapsedTicks)
{
	BOOLEAN done = FALSE;
	// We use the actual descriptor for flags processing and
	// a copied one for drawing. A copied one is updated only
	// when it is safe to do so.
	ANIMATION_DESC *ADPtr = pSeq->ADPtr;

	if (pSeq->Direction == NO_DIR)
	{	// just starting now
		pSeq->Alarm = 0; // now!
		ADPtr->AnimFlags &= ~ANIM_DISABLED;
	}

	if (pSeq->Alarm > ElapsedTicks)
	{	// Not time yet
		pSeq->Alarm -= ElapsedTicks;
		return FALSE;
	}

	// Time to start or advance the animation
	pSeq->Change = TRUE;

	if (pSeq->Direction == NO_DIR)
	{	// just starting now
		pSeq->FramesLeft = ADPtr->NumFrames;
		// Both INTRO and DONE may be set at the same time,
		// when e.g. Zoq and Pik are taking turns to talk
		// Process the DONE transition first to go into
		// a neutral state before switching over.
		if (CommData.AlienTransitionDesc.AnimFlags & TALK_DONE)
		{
			pSeq->Direction = DOWN_DIR;
			pSeq->CurIndex = ADPtr->NumFrames - 1;
		}
		else if (CommData.AlienTransitionDesc.AnimFlags & TALK_INTRO)
		{
			pSeq->Direction = UP_DIR;
			// It's now safe to pick up new Transition descriptor if changed
			// (e.g. Zoq and Pik taking turns to talk)
			if (CommData.AlienTransitionDesc.StartIndex
					!= ADPtr->StartIndex)
			{	// copy the new one
				*ADPtr = CommData.AlienTransitionDesc;
			}
			
			pSeq->CurIndex = 0;
		}
	}

	--pSeq->FramesLeft;
	if (pSeq->FramesLeft == 0)
	{	// animation is done
		if (pSeq->Direction == UP_DIR)
		{	// done with TALK_INTRO transition
			CommData.AlienTransitionDesc.AnimFlags &= ~TALK_INTRO;
		}
		else if (pSeq->Direction == DOWN_DIR)
		{	// done with TALK_DONE transition
			CommData.AlienTransitionDesc.AnimFlags &= ~TALK_DONE;

			// Done with all transition frames
			ADPtr->AnimFlags |= ANIM_DISABLED;
			done = TRUE;
		}
		pSeq->Direction = NO_DIR;
	}
	else
	{	// next frame
		pSeq->Alarm = randomFrameRate (pSeq);
		pSeq->CurIndex += pSeq->Direction;
	}

	return done;
}

void
InitCommAnimations (void)
{
	ActiveMask = 0;

	TalkDesc = CommData.AlienTalkDesc;
	TransitDesc = CommData.AlienTransitionDesc;

	// Animation sequences have to be drawn in reverse, and
	// talk animations have to be drawn last (so we add them first)
	TotalSequences = 0;
	// Transition animation last
	Transit = Sequences + TotalSequences;
	SetupTalkSequence (Transit, &TransitDesc);
	++TotalSequences;
	// Talk animation second last
	Talk = Sequences + TotalSequences;
	SetupTalkSequence (Talk, &TalkDesc);
	++TotalSequences;
	FirstAmbient = TotalSequences;
	SetupAmbientSequences (Sequences + FirstAmbient, CommData.NumAnimations);
	TotalSequences += CommData.NumAnimations;

	LastTime = GetTimeCounter ();
}

BOOLEAN
ProcessCommAnimations (BOOLEAN FullRedraw, BOOLEAN paused)
{
	if (paused)
	{	// Drive colormap xforms and nothing else
		XFormColorMap_step ();
		return FALSE;
	}
	else
	{
		COUNT i;
		SEQUENCE *pSeq;
		BOOLEAN Change;
		BOOLEAN CanTalk = TRUE;
		TimeCount CurTime;
		DWORD ElapsedTicks;
		DWORD NextActiveMask;

		CurTime = GetTimeCounter ();
		ElapsedTicks = CurTime - LastTime;
		LastTime = CurTime;

		// Process ambient animations
		NextActiveMask = ActiveMask;
		pSeq = Sequences + FirstAmbient;
		for (i = 0; i < CommData.NumAnimations; ++i, ++pSeq)
		{
			ANIMATION_DESC *ADPtr = pSeq->ADPtr;
			DWORD ActiveBit = 1L << i;

			if (ADPtr->AnimFlags & ANIM_DISABLED)
				continue;
			
			if (pSeq->Direction == NO_DIR)
			{	// animation is paused
				if (!conflictsWithTalkingAnim (pSeq))
				{	// start it up
					pSeq->Direction = UP_DIR;
				}
			}
			else if (pSeq->Alarm > ElapsedTicks)
			{	// not time yet
				pSeq->Alarm -= ElapsedTicks;
			}
			else if (ActiveMask & ADPtr->BlockMask)
			{	// animation is blocked
				assert (!(ActiveMask & ActiveBit) &&
						"Check animations' mutual blocking masks");
				assert (animAtNeutralIndex (pSeq));
				// reschedule
				pSeq->Alarm = randomRestartRate (pSeq) + 1;
				continue;
			}
			else
			{	// Time to start or advance the animation
				if (AdvanceAmbientSequence (pSeq))
				{	// Animation is active this frame and the next
					ActiveMask |= ActiveBit;
					NextActiveMask |= ActiveBit;
				}
				else
				{	// Animation remains active this frame but not the next
					// This keeps any conflicting animations (BlockMask)
					// from activating in the same frame and scribbling over
					// our last image.
					NextActiveMask &= ~ActiveBit;
				}
			}

			if (pSeq->AnimType == PICTURE_ANIM && pSeq->Direction != NO_DIR
					&& conflictsWithTalkingAnim (pSeq))
			{
				// We want to talk, but this is a running picture animation
				// which conflicts with the talking animation
				// See if it is safe to stop it now.
				if (animAtNeutralIndex (pSeq))
				{	// pause the animation
					pSeq->Direction = NO_DIR;
					NextActiveMask &= ~ActiveBit;
					// Talk animation is drawn last, so it's not a conflict
					// for this frame. The talk animation will be drawn
					// over the neutral frame.
				}
				else
				{	// Otherwise, let the animation run until it's safe
					CanTalk = FALSE;
				}
			}
		}
		// All ambient animations have been processed. Advance the mask.
		ActiveMask = NextActiveMask;

		// Process the talking and transition animations
		if (CanTalk	&& haveTalkingAnim () && runningTalkingAnim ())
		{
			BOOLEAN done = FALSE;

			if (signaledStopTalkingAnim () && haveTransitionAnim ())
			{	// Run the transition. We will clear everything
				// when it is done
				CommData.AlienTransitionDesc.AnimFlags |= TALK_DONE;
			}

			if (CommData.AlienTransitionDesc.AnimFlags
					& (TALK_INTRO | TALK_DONE))
			{	// Transitioning in or out of talking
				if ((CommData.AlienTransitionDesc.AnimFlags & TALK_DONE)
						&& Transit->Direction == NO_DIR)
				{	// This is needed when switching talking anims
					ResetSequence (Talk);
				}
				done = AdvanceTransitSequence (Transit, ElapsedTicks);
			}
			else if (!signaledStopTalkingAnim ())
			{	// Talking, transition is done
				AdvanceTalkingSequence (Talk, ElapsedTicks);
			}
			else
			{	// Not talking
				ResetSequence (Talk);
				done = TRUE;
			}

			if (signaledStopTalkingAnim () && done)
			{
				clearRunTalkingAnim ();
				clearStopTalkingAnim ();
			}
		}
		else
		{	// Not talking -- disable talking anim if it is done
			if (Talk->Direction == NO_DIR)
				TalkDesc.AnimFlags |= ANIM_DISABLED;
		}

		BatchGraphics ();

		// Draw all animations
		{
			BOOLEAN ColorChange = XFormColorMap_step ();

			if (ColorChange)
				FullRedraw = TRUE;

			// Colormap animations are processed separately
			// from picture anims (see XFormColorMap_step)
			ProcessColormapAnims (Sequences + FirstAmbient,
					CommData.NumAnimations);

			Change = DrawAlienFrame (Sequences, TotalSequences, FullRedraw);
			if (FullRedraw)
				Change = TRUE;
		}
		
		UnbatchGraphics ();

		// Post-process ambient animations
		pSeq = Sequences + FirstAmbient;
		for (i = 0; i < CommData.NumAnimations; ++i, ++pSeq)
		{
			ANIMATION_DESC *ADPtr = pSeq->ADPtr;
			DWORD ActiveBit = 1L << i;

			if (ADPtr->AnimFlags & ANIM_DISABLED)
				continue;

			// We can only disable a one-shot anim here, otherwise the
			// last frame will not be drawn
			if ((ADPtr->AnimFlags & ONE_SHOT_ANIM)
					&& !(NextActiveMask & ActiveBit))
			{	// One-shot animation, inactive next frame
				ADPtr->AnimFlags |= ANIM_DISABLED;
			}
		}

		return Change;
	}
}

BOOLEAN
DrawAlienFrame (SEQUENCE *Sequences, COUNT Num, BOOLEAN fullRedraw)
{
	int i;
	STAMP s;
	BOOLEAN Change = FALSE;

	BatchGraphics ();

	s.origin.x = -SAFE_X;
	s.origin.y = 0;
	
	if (fullRedraw)
	{
		// Draw the main frame
		s.frame = CommData.AlienFrame;
		DrawStamp (&s);

		// Draw any static frames (has to be in reverse)
		for (i = CommData.NumAnimations - 1; i >= 0; --i)
		{
			ANIMATION_DESC *ADPtr = &CommData.AlienAmbientArray[i];

			if (ADPtr->AnimFlags & ANIM_MASK)
				continue;

			ADPtr->AnimFlags |= ANIM_DISABLED;

			if (!(ADPtr->AnimFlags & COLORXFORM_ANIM))
			{	// It's a static frame (e.g. Flagship picture at Starbase)
				s.frame = SetAbsFrameIndex (CommData.AlienFrame,
						ADPtr->StartIndex);
				DrawStamp (&s);
			}
		}
	}

	if (Sequences)
	{	// Draw the animation sequences (has to be in reverse)
		for (i = Num - 1; i >= 0; --i)
		{
			SEQUENCE *pSeq = &Sequences[i];
			ANIMATION_DESC *ADPtr = pSeq->ADPtr;

			if ((ADPtr->AnimFlags & ANIM_DISABLED)
					|| pSeq->AnimType != PICTURE_ANIM)
				continue;

			// Draw current animation frame only if changed
			if (!fullRedraw && !pSeq->Change)
				continue;

			s.frame = SetAbsFrameIndex (CommData.AlienFrame,
					ADPtr->StartIndex + pSeq->CurIndex);
			DrawStamp (&s);
			pSeq->Change = FALSE;

			Change = TRUE;
		}
	}

	UnbatchGraphics ();

	return Change;
}
