//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.
 */

#include "../ship.h"
#include "shofixti.h"
#include "resinst.h"

#include "uqm/globdata.h"
#include "uqm/tactrans.h"
#include "libs/mathlib.h"

// Core characteristics
#define MAX_CREW 6
#define MAX_ENERGY 4
#define ENERGY_REGENERATION 1
#define ENERGY_WAIT 9
#define MAX_THRUST 35
#define THRUST_INCREMENT 5
#define TURN_WAIT 1
#define THRUST_WAIT 0
#define WEAPON_WAIT 3
#define SPECIAL_WAIT 0
#define SHIP_MASS 1

// Dart Gun
#define WEAPON_ENERGY_COST 1
#define SHOFIXTI_OFFSET 15
#define MISSILE_OFFSET 1
#define MISSILE_SPEED DISPLAY_TO_WORLD (24)
#define MISSILE_LIFE 10
#define MISSILE_HITS 1
#define MISSILE_DAMAGE 1

// Glory Device
#define SPECIAL_ENERGY_COST 0
#define DESTRUCT_RANGE 180
#define MAX_DESTRUCTION (DESTRUCT_RANGE / 10)

// Full game: Tanaka/Katana's damaged ships
#define NUM_LIMPETS 3

static RACE_DESC shofixti_desc =
{
	{ /* SHIP_INFO */
		FIRES_FORE,
		5, /* Super Melee cost */
		MAX_CREW, MAX_CREW,
		MAX_ENERGY, MAX_ENERGY,
		SHOFIXTI_RACE_STRINGS,
		SHOFIXTI_ICON_MASK_PMAP_ANIM,
		SHOFIXTI_MICON_MASK_PMAP_ANIM,
		NULL, NULL, NULL
	},
	{ /* FLEET_STUFF */
		0, /* Initial sphere of influence radius */
		{ /* Known location (center of SoI) */
			0, 0,
		},
	},
	{
		MAX_THRUST,
		THRUST_INCREMENT,
		ENERGY_REGENERATION,
		WEAPON_ENERGY_COST,
		SPECIAL_ENERGY_COST,
		ENERGY_WAIT,
		TURN_WAIT,
		THRUST_WAIT,
		WEAPON_WAIT,
		SPECIAL_WAIT,
		SHIP_MASS,
	},
	{
		{
			SHOFIXTI_BIG_MASK_PMAP_ANIM,
			SHOFIXTI_MED_MASK_PMAP_ANIM,
			SHOFIXTI_SML_MASK_PMAP_ANIM,
		},
		{
			DART_BIG_MASK_PMAP_ANIM,
			DART_MED_MASK_PMAP_ANIM,
			DART_SML_MASK_PMAP_ANIM,
		},
		{
			DESTRUCT_BIG_MASK_ANIM,
			DESTRUCT_MED_MASK_ANIM,
			DESTRUCT_SML_MASK_ANIM,
		},
		{
			SHOFIXTI_CAPTAIN_MASK_PMAP_ANIM,
			NULL, NULL, NULL, NULL, NULL
		},
	        SHOFIXTI_VICTORY_SONG,
		SHOFIXTI_SHIP_SOUNDS,
		{ NULL, NULL, NULL },
		{ NULL, NULL, NULL },
		{ NULL, NULL, NULL },
		NULL, NULL
	},
	{
		0,
		MISSILE_SPEED * MISSILE_LIFE,
		NULL,
	},
	(UNINIT_FUNC *) NULL,
	(PREPROCESS_FUNC *) NULL,
	(POSTPROCESS_FUNC *) NULL,
	(INIT_WEAPON_FUNC *) NULL,
	0,
	0, /* CodeRef */
};

static COUNT
initialize_standard_missile (ELEMENT *ShipPtr, HELEMENT MissileArray[])
{

	STARSHIP *StarShipPtr;
	MISSILE_BLOCK MissileBlock;

	GetElementStarShip (ShipPtr, &StarShipPtr);
	MissileBlock.cx = ShipPtr->next.location.x;
	MissileBlock.cy = ShipPtr->next.location.y;
	MissileBlock.farray = StarShipPtr->RaceDescPtr->ship_data.weapon;
	MissileBlock.face = MissileBlock.index = StarShipPtr->ShipFacing;
	MissileBlock.sender = ShipPtr->playerNr;
	MissileBlock.flags = IGNORE_SIMILAR;
	MissileBlock.pixoffs = SHOFIXTI_OFFSET;
	MissileBlock.speed = MISSILE_SPEED;
	MissileBlock.hit_points = MISSILE_HITS;
	MissileBlock.damage = MISSILE_DAMAGE;
	MissileBlock.life = MISSILE_LIFE;
	MissileBlock.preprocess_func = NULL;
	MissileBlock.blast_offs = MISSILE_OFFSET;
	MissileArray[0] = initialize_missile (&MissileBlock);

	return (1);
}

static void
destruct_preprocess (ELEMENT *ElementPtr)
{
#define DESTRUCT_SWITCH ((NUM_EXPLOSION_FRAMES * 3) - 3)
	PRIMITIVE *lpPrim;

	// ship_death() set the ship element's life_span to
	// (NUM_EXPLOSION_FRAMES * 3)
	lpPrim = &(GLOBAL (DisplayArray))[ElementPtr->PrimIndex];
	ElementPtr->state_flags |= CHANGING;
	if (ElementPtr->life_span > DESTRUCT_SWITCH)
	{
		// First, stamp-fill the ship's own element with changing colors
		// for 3 frames. No explosion element yet.
		SetPrimType (lpPrim, STAMPFILL_PRIM);
		if (ElementPtr->life_span == DESTRUCT_SWITCH + 2)
			SetPrimColor (lpPrim,
					BUILD_COLOR (MAKE_RGB15 (0x1F, 0x1F, 0x0A), 0x0E));
		else
			SetPrimColor (lpPrim,
					BUILD_COLOR (MAKE_RGB15 (0x1F, 0x1F, 0x1F), 0x0F));
	}
	else if (ElementPtr->life_span < DESTRUCT_SWITCH)
	{
		// Stamp-fill the explosion element with cycling colors for the
		// remainder of the glory explosion frames.
		Color color = GetPrimColor (lpPrim);

		ElementPtr->next.image.frame =
				IncFrameIndex (ElementPtr->current.image.frame);
		if (sameColor (color,
				BUILD_COLOR (MAKE_RGB15 (0x1F, 0x1F, 0x1F), 0x0F)))
			SetPrimColor (lpPrim,
					BUILD_COLOR (MAKE_RGB15 (0x1F, 0x1F, 0x0A), 0x0E));
		else if (sameColor (color,
				BUILD_COLOR (MAKE_RGB15 (0x1F, 0x1F, 0x0A), 0x0E)))
			SetPrimColor (lpPrim,
					BUILD_COLOR (MAKE_RGB15 (0x1F, 0x0A, 0x0A), 0x0C));
		else if (sameColor (color,
				BUILD_COLOR (MAKE_RGB15 (0x1F, 0x0A, 0x0A), 0x0C)))
			SetPrimColor (lpPrim,
					BUILD_COLOR (MAKE_RGB15 (0x14, 0x0A, 0x00), 0x06));
		else if (sameColor (color,
				BUILD_COLOR (MAKE_RGB15 (0x14, 0x0A, 0x00), 0x06)))
			SetPrimColor (lpPrim,
					BUILD_COLOR (MAKE_RGB15 (0x14, 0x00, 0x00), 0x04));
	}
	else
	{
		HELEMENT hDestruct;

		SetPrimType (lpPrim, NO_PRIM);
		// The ship's own element will not be drawn anymore but will remain
		// alive all through the glory explosion.
		ElementPtr->preprocess_func = NULL;

		// Spawn a separate glory explosion element.
		// XXX: Why? Why not keep using the ship's element?
		//   Is it because of conflicting state_flags, hit_points or
		//   mass_points?
		hDestruct = AllocElement ();
		if (hDestruct)
		{
			ELEMENT *DestructPtr;
			STARSHIP *StarShipPtr;

			GetElementStarShip (ElementPtr, &StarShipPtr);

			PutElement (hDestruct);
			LockElement (hDestruct, &DestructPtr);
			SetElementStarShip (DestructPtr, StarShipPtr);
			DestructPtr->hit_points = 0;
			DestructPtr->mass_points = 0;
			DestructPtr->playerNr = NEUTRAL_PLAYER_NUM;
			DestructPtr->state_flags = APPEARING | FINITE_LIFE | NONSOLID;
			SetPrimType (&(GLOBAL (DisplayArray))[DestructPtr->PrimIndex],
					STAMPFILL_PRIM);
			SetPrimColor (&(GLOBAL (DisplayArray))[DestructPtr->PrimIndex],
					BUILD_COLOR (MAKE_RGB15 (0x1F, 0x1F, 0x1F), 0x0F));
			DestructPtr->current.image.farray =
					StarShipPtr->RaceDescPtr->ship_data.special;
			DestructPtr->current.image.frame =
					StarShipPtr->RaceDescPtr->ship_data.special[0];
			DestructPtr->life_span = GetFrameCount (
					DestructPtr->current.image.frame);
			DestructPtr->current.location = ElementPtr->current.location;
			DestructPtr->preprocess_func = destruct_preprocess;
			DestructPtr->postprocess_func = NULL;
			DestructPtr->death_func = NULL;
			ZeroVelocityComponents (&DestructPtr->velocity);
			UnlockElement (hDestruct);
		}
	}
}

/* In order to detect any Orz Marines that have boarded the ship
   when it self-destructs, we'll need to see some Orz functions */
#include "../orz/orz.h"
#define ORZ_MARINE(ptr) (ptr->preprocess_func == intruder_preprocess && \
		ptr->collision_func == marine_collision)

static void
self_destruct_kill_objects (ELEMENT *ElementPtr)
{
	// This is called during PostProcessQueue(), close to or at the end,
	// for the temporary destruct element to apply the effects of glory
	// explosion. The effects are not seen until the next frame.
	HELEMENT hElement, hNextElement;

	for (hElement = GetHeadElement (); hElement != 0; hElement = hNextElement)
	{
		ELEMENT *ObjPtr;
		SIZE delta_x, delta_y;
		DWORD dist;

		LockElement (hElement, &ObjPtr);
		hNextElement = GetSuccElement (ObjPtr);

		if (!CollidingElement (ObjPtr) && !ORZ_MARINE (ObjPtr))
		{
			UnlockElement (hElement);
			continue;
		}

		delta_x = ObjPtr->next.location.x - ElementPtr->next.location.x;
		if (delta_x < 0)
			delta_x = -delta_x;
		delta_y = ObjPtr->next.location.y - ElementPtr->next.location.y;
		if (delta_y < 0)
			delta_y = -delta_y;
		delta_x = WORLD_TO_DISPLAY (delta_x);
		delta_y = WORLD_TO_DISPLAY (delta_y);
		dist = delta_x * delta_x + delta_y * delta_y;
		if (delta_x <= DESTRUCT_RANGE && delta_y <= DESTRUCT_RANGE
				&& dist <= DESTRUCT_RANGE * DESTRUCT_RANGE)
		{
			int destruction = 1 + MAX_DESTRUCTION *
					(DESTRUCT_RANGE - square_root (dist)) / DESTRUCT_RANGE;

			// XXX: Why not simply call do_damage()?
			if (ObjPtr->state_flags & PLAYER_SHIP)
			{
				if (!DeltaCrew (ObjPtr, -destruction))
					ObjPtr->life_span = 0;
			}
			else if (!GRAVITY_MASS (ObjPtr->mass_points))
			{
				if (destruction < ObjPtr->hit_points)
					ObjPtr->hit_points -= destruction;
				else
				{
					ObjPtr->hit_points = 0;
					ObjPtr->life_span = 0;
				}
			}
		}

		UnlockElement (hElement);
	}
}

// This function is called when the ship dies via Glory Device.
// The generic ship_death() function is not called for the ship in this case.
static void
shofixti_destruct_death (ELEMENT *ShipPtr)
{
	STARSHIP *StarShip;
	STARSHIP *winner;

	GetElementStarShip (ShipPtr, &StarShip);

	StopAllBattleMusic ();

	StartShipExplosion (ShipPtr, false);
	// We process the explosion ourselves because it is different
	ShipPtr->preprocess_func = destruct_preprocess;
	
	PlaySound (SetAbsSoundIndex (StarShip->RaceDescPtr->ship_data.ship_sounds,
			1), CalcSoundPosition (ShipPtr), ShipPtr, GAME_SOUND_PRIORITY + 1);

	winner = GetWinnerStarShip ();
	if (winner == NULL)
	{	// No winner determined yet
		winner = FindAliveStarShip (ShipPtr);
		if (winner == NULL)
		{	// No ships left alive after the Glory Device thus Shofixti wins
			winner = StarShip;
		}
		SetWinnerStarShip (winner);
	}
	else if (winner == StarShip)
	{	// This ship is the winner
		// It may have self-destructed before the ditty started playing,
		// and in that case, there should be no ditty
		StarShip->cur_status_flags &= ~PLAY_VICTORY_DITTY;
	}
	RecordShipDeath (ShipPtr);
}

static void
self_destruct (ELEMENT *ElementPtr)
{
	STARSHIP *StarShipPtr;
	HELEMENT hDestruct;

	GetElementStarShip (ElementPtr, &StarShipPtr);
		
	// Spawn a temporary element, which dies in this same frame, in order
	// to defer the effects of the glory explosion.
	// It will be the last element (or one of the last) for which the
	// death_func() will be called from PostProcessQueue() in this frame.
	// XXX: Why at the end? Why not just do it now?
	hDestruct = AllocElement ();
	if (hDestruct)
	{
		ELEMENT *DestructPtr;

		LockElement (hDestruct, &DestructPtr);
		DestructPtr->playerNr = ElementPtr->playerNr;
		DestructPtr->state_flags = APPEARING | NONSOLID | FINITE_LIFE;
		DestructPtr->next.location = ElementPtr->next.location;
		DestructPtr->life_span = 0;
		DestructPtr->pParent = ElementPtr->pParent;
		DestructPtr->hTarget = 0;

		DestructPtr->death_func = self_destruct_kill_objects;

		UnlockElement (hDestruct);

		PutElement (hDestruct);
	}

	// Must kill off the remaining crew ourselves
	DeltaCrew (ElementPtr, -(int)ElementPtr->crew_level);

	ElementPtr->state_flags |= NONSOLID;
	ElementPtr->life_span = 0;
	// The ship is now dead. It's death_func, i.e. shofixti_destruct_death(),
	// will be called the next frame.
	ElementPtr->death_func = shofixti_destruct_death;
}

static void
shofixti_intelligence (ELEMENT *ShipPtr, EVALUATE_DESC *ObjectsOfConcern,
		COUNT ConcernCounter)
{
	STARSHIP *StarShipPtr;

	ship_intelligence (ShipPtr, ObjectsOfConcern, ConcernCounter);

	GetElementStarShip (ShipPtr, &StarShipPtr);
	if (StarShipPtr->special_counter != 0)
		return;

	if (StarShipPtr->ship_input_state & SPECIAL)
		StarShipPtr->ship_input_state &= ~SPECIAL;
	else
	{
		EVALUATE_DESC *lpWeaponEvalDesc;
		EVALUATE_DESC *lpShipEvalDesc;

		lpWeaponEvalDesc = &ObjectsOfConcern[ENEMY_WEAPON_INDEX];
		lpShipEvalDesc = &ObjectsOfConcern[ENEMY_SHIP_INDEX];
		if (StarShipPtr->RaceDescPtr->ship_data.special[0]
				&& (GetFrameCount (StarShipPtr->RaceDescPtr->ship_data.
				captain_control.special)
				- GetFrameIndex (StarShipPtr->RaceDescPtr->ship_data.
				captain_control.special) > 5
				|| (lpShipEvalDesc->ObjectPtr != NULL
				&& lpShipEvalDesc->which_turn <= 4)
				|| (lpWeaponEvalDesc->ObjectPtr != NULL
							/* means IMMEDIATE WEAPON */
				&& (((lpWeaponEvalDesc->ObjectPtr->state_flags & PLAYER_SHIP)
				&& ShipPtr->crew_level == 1)
				|| (PlotIntercept (lpWeaponEvalDesc->ObjectPtr, ShipPtr, 2, 0)
				&& lpWeaponEvalDesc->ObjectPtr->mass_points >=
				ShipPtr->crew_level
				&& (TFB_Random () & 1))))))
			StarShipPtr->ship_input_state |= SPECIAL;
	}
}

static void
shofixti_postprocess (ELEMENT *ElementPtr)
{
	STARSHIP *StarShipPtr;

	GetElementStarShip (ElementPtr, &StarShipPtr);
	if ((StarShipPtr->cur_status_flags
			^ StarShipPtr->old_status_flags) & SPECIAL)
	{
		StarShipPtr->RaceDescPtr->ship_data.captain_control.special =
				IncFrameIndex (StarShipPtr->RaceDescPtr->ship_data.
				captain_control.special);
		if (GetFrameCount (StarShipPtr->RaceDescPtr->ship_data.
				captain_control.special)
				- GetFrameIndex (StarShipPtr->RaceDescPtr->ship_data.
				captain_control.special) == 3)
			self_destruct (ElementPtr);
	}
}

RACE_DESC*
init_shofixti (void)
{
	RACE_DESC *RaceDescPtr;
	// The caller of this func will copy the struct
	static RACE_DESC new_shofixti_desc;

	shofixti_desc.postprocess_func = shofixti_postprocess;
	shofixti_desc.init_weapon_func = initialize_standard_missile;
	shofixti_desc.cyborg_control.intelligence_func = shofixti_intelligence;

	new_shofixti_desc = shofixti_desc;
	if (LOBYTE (GLOBAL (CurrentActivity)) == IN_ENCOUNTER
			&& !GET_GAME_STATE (SHOFIXTI_RECRUITED))
	{
		// Tanaka/Katana flies in a damaged ship.
		COUNT i;

		new_shofixti_desc.ship_data.ship_rsc[0] = OLDSHOF_BIG_MASK_PMAP_ANIM;
		new_shofixti_desc.ship_data.ship_rsc[1] = OLDSHOF_MED_MASK_PMAP_ANIM;
		new_shofixti_desc.ship_data.ship_rsc[2] = OLDSHOF_SML_MASK_PMAP_ANIM;
		new_shofixti_desc.ship_data.special_rsc[0] = NULL_RESOURCE;
		new_shofixti_desc.ship_data.special_rsc[1] = NULL_RESOURCE;
		new_shofixti_desc.ship_data.special_rsc[2] = NULL_RESOURCE;
		new_shofixti_desc.ship_data.captain_control.captain_rsc =
				OLDSHOF_CAPTAIN_MASK_PMAP_ANIM;

		/* Weapon doesn't work as well */
		new_shofixti_desc.characteristics.weapon_wait = 10;
		
		/* Simulate VUX limpets */
		for (i = 0; i < NUM_LIMPETS; ++i)
		{
			if (++new_shofixti_desc.characteristics.turn_wait == 0)
				--new_shofixti_desc.characteristics.turn_wait;
			if (++new_shofixti_desc.characteristics.thrust_wait == 0)
				--new_shofixti_desc.characteristics.thrust_wait;

/* This should be the same as MIN_THRUST_INCREMENT in vux.c */
#define MIN_THRUST_INCREMENT DISPLAY_TO_WORLD (1)

			if (new_shofixti_desc.characteristics.thrust_increment <=
					MIN_THRUST_INCREMENT)
			{
				new_shofixti_desc.characteristics.max_thrust =
						new_shofixti_desc.characteristics.thrust_increment << 1;
			}
			else
			{
				COUNT num_thrusts;

				num_thrusts = new_shofixti_desc.characteristics.max_thrust /
						new_shofixti_desc.characteristics.thrust_increment;
				--new_shofixti_desc.characteristics.thrust_increment;
				new_shofixti_desc.characteristics.max_thrust =
						new_shofixti_desc.characteristics.thrust_increment *
						num_thrusts;
			}
		}
	}

	RaceDescPtr = &new_shofixti_desc;

	return (RaceDescPtr);
}

