//-----------------------------------------------------------------------------
//
// Copyright 1994-1996 Raven Software
// Copyright 1999-2016 Randy Heit
// Copyright 2002-2018 Christoph Oelckers
// Copyright 2005-2008 Martin Howe
//
// 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 3 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, see http://www.gnu.org/licenses/
//
//-----------------------------------------------------------------------------
//

extend class Actor
{
	virtual Actor, int, int MorphedDeath()
	{
		return null, 0, 0;
	}

	
	//===========================================================================
	//
	// Main entry point
	//
	//===========================================================================

	bool Morph(Actor activator, class<PlayerPawn> playerclass, class<MorphedMonster> monsterclass, int duration = 0, int style = 0, class<Actor> morphflash = null, class<Actor>unmorphflash = null)
	{
		if (player != null && player.mo != null && playerclass != null)
		{
			return player.mo.MorphPlayer(activator? activator.player : null, playerclass, duration, style, morphflash, unmorphflash);
		}
		else
		{
			return MorphMonster(monsterclass, duration, style, morphflash, unmorphflash);
		}
	}
	
	//===========================================================================
	//
	// Action function variant whose arguments differ from the generic one.
	//
	//===========================================================================

	bool A_Morph(class<Actor> type, int duration = 0, int style = 0, class<Actor> morphflash = null, class<Actor>unmorphflash = null)
	{
		if (self.player != null)
		{
			let playerclass = (class<PlayerPawn>)(type);
			if (playerclass && self.player.mo != null) return player.mo.MorphPlayer(self.player, playerclass, duration, style, morphflash, unmorphflash);
		}
		else
		{
			return MorphMonster(type, duration, style, morphflash, unmorphflash);
		}
		return false;
	}

	//===========================================================================
	//
	// Main entry point
	//
	//===========================================================================

	bool UnMorph(Actor activator, int flags, bool force)
	{
		if (player)
		{
			return player.mo.UndoPlayerMorph(activator? activator.player : null, flags, force);
		}
		else 
		{
			let morphed = MorphedMonster(self);
			if (morphed)
				return morphed.UndoMonsterMorph(force);
		}
		return false;
	}

	
	//---------------------------------------------------------------------------
	//
	// FUNC P_MorphMonster
	//
	// Returns true if the monster gets turned into a chicken/pig.
	//
	//---------------------------------------------------------------------------

	virtual bool MorphMonster (Class<Actor> spawntype, int duration, int style, Class<Actor> enter_flash, Class<Actor> exit_flash)
	{
		if (player || spawntype == NULL || bDontMorph || !bIsMonster || !(spawntype is 'MorphedMonster'))
		{
			return false;
		}

		let morphed = MorphedMonster(Spawn (spawntype, Pos, NO_REPLACE));
		Substitute (morphed);
		if ((style & MRF_TRANSFERTRANSLATION) && !morphed.bDontTranslate)
		{
			morphed.Translation = Translation;
		}
		morphed.ChangeTid(tid);
		ChangeTid(0);
		morphed.Angle = Angle;
		morphed.UnmorphedMe = self;
		morphed.Alpha = Alpha;
		morphed.RenderStyle = RenderStyle;
		morphed.Score = Score;

		morphed.UnmorphTime = level.time + ((duration) ? duration : DEFMORPHTICS) + random[morphmonst]();
		morphed.MorphStyle = style;
		morphed.MorphExitFlash = (exit_flash) ? exit_flash : (class<Actor>)("TeleportFog");
		morphed.FlagsSave = bSolid * 2 + bShootable * 4 + bInvisible * 0x40;	// The factors are for savegame compatibility
		
		morphed.special = special;
		morphed.args[0] = args[0];
		morphed.args[1] = args[1];
		morphed.args[2] = args[2];
		morphed.args[3] = args[3];
		morphed.args[4] = args[4];
		morphed.CopyFriendliness (self, true);
		morphed.bShadow |= bShadow;
		morphed.bGhost |= bGhost;
		special = 0;
		bSolid = false;
		bShootable = false;
		bUnmorphed = true;
		bInvisible = true;
		let eflash = Spawn(enter_flash ? enter_flash : (class<Actor>)("TeleportFog"), Pos + (0, 0, gameinfo.TELEFOGHEIGHT), ALLOW_REPLACE);
		if (eflash)
			eflash.target = morphed;
		return true;
	}
}

extend class PlayerPawn
{
	//===========================================================================
	//
	// EndAllPowerupEffects
	//
	// Calls EndEffect() on every Powerup in the inventory list.
	//
	//===========================================================================

	void InitAllPowerupEffects()
	{
		let item = Inv;
		while (item != null)
		{
			let power = Powerup(item);
			if (power != null)
			{
				power.InitEffect();
			}
			item = item.Inv;
		}
	}
	
	//===========================================================================
	//
	// EndAllPowerupEffects
	//
	// Calls EndEffect() on every Powerup in the inventory list.
	//
	//===========================================================================

	void EndAllPowerupEffects()
	{
		let item = Inv;
		while (item != null)
		{
			let power = Powerup(item);
			if (power != null)
			{
				power.EndEffect();
			}
			item = item.Inv;
		}
	}
	
	//===========================================================================
	//
	//
	//
	//===========================================================================

	virtual void ActivateMorphWeapon ()
	{
		class<Weapon> morphweaponcls = MorphWeapon;
		player.PendingWeapon = WP_NOCHANGE;

		if (player.ReadyWeapon != null)
		{
			player.GetPSprite(PSP_WEAPON).y = WEAPONTOP;
		}

		if (morphweaponcls == null || !(morphweaponcls is 'Weapon'))
		{ // No weapon at all while morphed!
			player.ReadyWeapon = null;
		}
		else
		{
			player.ReadyWeapon = Weapon(FindInventory (morphweaponcls));
			if (player.ReadyWeapon == null)
			{
				player.ReadyWeapon = Weapon(GiveInventoryType (morphweaponcls));
				if (player.ReadyWeapon != null)
				{
					player.ReadyWeapon.GivenAsMorphWeapon = true; // flag is used only by new beastweap semantics in UndoPlayerMorph
				}
			}
			if (player.ReadyWeapon != null)
			{
				player.SetPsprite(PSP_WEAPON, player.ReadyWeapon.GetReadyState());
			}
		}

		if (player.ReadyWeapon != null)
		{
			player.SetPsprite(PSP_FLASH, null);
		}

		player.PendingWeapon = WP_NOCHANGE;
	}

	//---------------------------------------------------------------------------
	//
	// MorphPlayer
	//
	// Returns true if the player gets turned into a chicken/pig.
	//
	// TODO: Allow morphed players to receive weapon sets (not just one weapon),
	// since they have their own weapon slots now.
	//
	//---------------------------------------------------------------------------

	virtual bool MorphPlayer(playerinfo activator, Class<PlayerPawn> spawntype, int duration, int style, Class<Actor> enter_flash = null, Class<Actor> exit_flash = null)
	{
		if (bDontMorph)
		{
			return false;
		}
		if (bInvulnerable && (player != activator || !(style & MRF_WHENINVULNERABLE)))
		{ // Immune when invulnerable unless this is a power we activated
			return false;
		}
		if (player.morphTics)
		{ // Player is already a beast
			if ((GetClass() == spawntype) && bCanSuperMorph
				&& (player.morphTics < (((duration) ? duration : DEFMORPHTICS) - TICRATE))
				&& FindInventory('PowerWeaponLevel2', true) == null)
			{ // Make a super chicken
				GiveInventoryType ('PowerWeaponLevel2');
			}
			return false;
		}
		if (health <= 0)
		{ // Dead players cannot morph
			return false;
		}
		if (spawntype == null)
		{
			return false;
		}
		if (!(spawntype is 'PlayerPawn'))
		{
			return false;
		}
		if (spawntype == GetClass())
		{
			return false;
		}

		let morphed = PlayerPawn(Spawn (spawntype, Pos, NO_REPLACE));
		EndAllPowerupEffects();
		Substitute(morphed);
		if ((style & MRF_TRANSFERTRANSLATION) && !morphed.bDontTranslate)
		{
			morphed.Translation = Translation;
		}
		if (tid != 0 && (style & MRF_NEWTIDBEHAVIOUR))
		{
			morphed.ChangeTid(tid);
			ChangeTid(0);
		}
		morphed.Angle = Angle;
		morphed.target = target;
		morphed.tracer = tracer;
		morphed.alternative = self;
		morphed.FriendPlayer = FriendPlayer;
		morphed.DesignatedTeam = DesignatedTeam;
		morphed.Score = Score;
		player.PremorphWeapon = player.ReadyWeapon;
		
		morphed.special2 = bSolid * 2 + bShootable * 4 + bInvisible * 0x40;	// The factors are for savegame compatibility
		morphed.player = player;

		if (morphed.ViewHeight > player.viewheight && player.deltaviewheight == 0)
		{ // If the new view height is higher than the old one, start moving toward it.
			player.deltaviewheight = player.GetDeltaViewHeight();
		}
		morphed.bShadow |= bShadow;
		morphed.bNoGravity |= bNoGravity;
		morphed.bFly |= bFly;
		morphed.bGhost |= bGhost;

		if (enter_flash == null) enter_flash = 'TeleportFog';
		let eflash = Spawn(enter_flash, Pos + (0, 0, gameinfo.telefogheight), ALLOW_REPLACE);
		let p = player;
		player = null;
		alternative = morphed;
		bSolid = false;
		bShootable = false;
		bUnmorphed = true;
		bInvisible = true;
		
		p.morphTics = (duration) ? duration : DEFMORPHTICS;

		// [MH] Used by SBARINFO to speed up face drawing
		p.MorphedPlayerClass = spawntype;

		p.MorphStyle = style;
		if (exit_flash == null) exit_flash = 'TeleportFog';
		p.MorphExitFlash = exit_flash;
		p.health = morphed.health;
		p.mo = morphed;
		p.vel = (0, 0);
		morphed.ObtainInventory (self);
		// Remove all armor
		for (Inventory item = morphed.Inv; item != null; )
		{
			let next = item.Inv;
			if (item is 'Armor')
			{
				item.DepleteOrDestroy();
			}
			item = next;
		}
		morphed.InitAllPowerupEffects();
		morphed.ActivateMorphWeapon ();
		if (p.camera == self)	// can this happen?
		{
			p.camera = morphed;
		}
		morphed.ScoreIcon = ScoreIcon;	// [GRB]
		if (eflash)	
			eflash.target = morphed;
		return true;
	}
	
	//----------------------------------------------------------------------------
	//
	// FUNC UndoPlayerMorph
	//
	//----------------------------------------------------------------------------

	virtual bool UndoPlayerMorph(playerinfo activator, int unmorphflag = 0, bool force = false)
	{
		if (alternative == null)
		{
			return false;
		}

		let player = self.player;
		bool DeliberateUnmorphIsOkay = !!(MRF_STANDARDUNDOING & unmorphflag);

		if ((bInvulnerable) // If the player is invulnerable
			&& ((player != activator)       // and either did not decide to unmorph,
			|| (!((player.MorphStyle & MRF_WHENINVULNERABLE)  // or the morph style does not allow it
			|| (DeliberateUnmorphIsOkay))))) // (but standard morph styles always allow it),
		{ // Then the player is immune to the unmorph.
			return false;
		}

		let altmo = PlayerPawn(alternative);
		altmo.SetOrigin (Pos, false);
		altmo.bSolid = true;
		bSolid = false;
		if (!force && !altmo.TestMobjLocation())
		{ // Didn't fit
			altmo.bSolid = false;
			bSolid = true;
			player.morphTics = 2*TICRATE;
			return false;
		}
		// No longer using tracer as morph storage. That is what 'alternative' is for. 
		// If the tracer has changed on the morph, change the original too.
		altmo.target = target;
		altmo.tracer = tracer;
		self.player = null;
		altmo.alternative = alternative = null;

		// Remove the morph power if the morph is being undone prematurely.
		for (Inventory item = Inv; item != null;)
		{
			let next = item.Inv;
			if (item is "PowerMorph")
			{
				item.Destroy();
			}
			item = next;
		}
		EndAllPowerupEffects();
		altmo.ObtainInventory (self);
		Substitute(altmo);
		if ((tid != 0) && (player.MorphStyle & MRF_NEWTIDBEHAVIOUR))
		{
			altmo.ChangeTid(tid);
		}
		altmo.Angle = Angle;
		altmo.player = player;
		altmo.reactiontime = 18;
		altmo.bSolid = !!(special2 & 2);
		altmo.bShootable = !!(special2 & 4);
		altmo.bInvisible = !!(special2 & 0x40);
		altmo.Vel = (0, 0, Vel.Z);
		player.Vel = (0, 0);
		altmo.floorz = floorz;
		altmo.bShadow = bShadow;
		altmo.bNoGravity = bNoGravity;
		altmo.bGhost = bGhost;
		altmo.bUnmorphed = false;
		altmo.Score = Score;
		altmo.InitAllPowerupEffects();

		let exit_flash = player.MorphExitFlash;
		bool correctweapon = !!(player.MorphStyle & MRF_LOSEACTUALWEAPON);
		bool undobydeathsaves = !!(player.MorphStyle & MRF_UNDOBYDEATHSAVES);

		player.morphTics = 0;
		player.MorphedPlayerClass = null;
		player.MorphStyle = 0;
		player.MorphExitFlash = null;
		player.viewheight = altmo.ViewHeight;
		Inventory level2 = altmo.FindInventory("PowerWeaponLevel2", true);
		if (level2 != null)
		{
			level2.Destroy ();
		}

		if ((player.health > 0) || undobydeathsaves)
		{
			player.health = altmo.health = altmo.SpawnHealth();
		}
		else // killed when morphed so stay dead
		{
			altmo.health = player.health;
		}

		player.mo = altmo;
		if (player.camera == self)
		{
			player.camera = altmo;
		}

		// [MH]
		// If the player that was morphed is the one
		// taking events, reset up the face, if any;
		// this is only needed for old-skool skins
		// and for the original DOOM status bar.
		if (player == players[consoleplayer])
		{
			if (face != 'None')
			{
				// Assume root-level base skin to begin with
				let skinindex = 0;
				let skin = player.GetSkin();
				// If a custom skin was in use, then reload it
				// or else the base skin for the player class.
				if (skin >= PlayerClasses.Size () && skin < PlayerSkins.Size())
				{
					skinindex = skin;
				}
				else if (PlayerClasses.Size () > 1)
				{
					let whatami = altmo.GetClass();
					for (int i = 0; i < PlayerClasses.Size (); ++i)
					{
						if (PlayerClasses[i].Type == whatami)
						{
							skinindex = i;
							break;
						}
					}
				}
			}
		}

		Actor eflash = null;
		if (exit_flash != null)
		{
			eflash = Spawn(exit_flash, Vec3Angle(20., altmo.Angle, gameinfo.telefogheight), ALLOW_REPLACE);
			if (eflash)	eflash.target = altmo;
		}
		WeaponSlots.SetupWeaponSlots(altmo);		// Use original class's weapon slots.
		let beastweap = player.ReadyWeapon;
		if (player.PremorphWeapon != null)
		{
			player.PremorphWeapon.PostMorphWeapon ();
		}
		else
		{
			player.ReadyWeapon = player.PendingWeapon = null;
		}
		if (correctweapon)
		{ // Better "lose morphed weapon" semantics
			class<Actor> morphweaponcls = MorphWeapon;
			if (morphweaponcls != null && morphweaponcls is 'Weapon')
			{
				let OriginalMorphWeapon = Weapon(altmo.FindInventory (morphweapon));
				if ((OriginalMorphWeapon != null) && (OriginalMorphWeapon.GivenAsMorphWeapon))
				{ // You don't get to keep your morphed weapon.
					if (OriginalMorphWeapon.SisterWeapon != null)
					{
						OriginalMorphWeapon.SisterWeapon.Destroy ();
					}
					OriginalMorphWeapon.Destroy ();
				}
			}
		}
		else // old behaviour (not really useful now)
		{ // Assumptions made here are no longer valid
			if (beastweap != null)
			{ // You don't get to keep your morphed weapon.
				if (beastweap.SisterWeapon != null)
				{
					beastweap.SisterWeapon.Destroy ();
				}
				beastweap.Destroy ();
			}
		}
		Destroy ();
		// Restore playerclass armor to its normal amount.
		let hxarmor = HexenArmor(altmo.FindInventory('HexenArmor'));
		if (hxarmor != null)
		{
			hxarmor.Slots[4] = altmo.HexenArmor[0];
		}
		return true;
	}

	//===========================================================================
	//
	//
	//
	//===========================================================================

	override Actor, int, int MorphedDeath()
	{
		// Voodoo dolls should not unmorph the real player here.
		if (player && (player.mo == self) &&
			(player.morphTics) &&
			(player.MorphStyle & MRF_UNDOBYDEATH) &&
			(alternative))
		{
			Actor realme = alternative;
			int realstyle = player.MorphStyle;
			int realhealth = health;
			if (UndoPlayerMorph(player, 0, !!(player.MorphStyle & MRF_UNDOBYDEATHFORCED)))
			{
				return realme, realstyle, realhealth;
			}
		}
		return null, 0, 0;
	}
}

//===========================================================================
//
//
//
//===========================================================================

class MorphProjectile : Actor
{

	Class<PlayerPawn> PlayerClass;
	Class<Actor> MonsterClass, MorphFlash, UnMorphFlash;
	int Duration, MorphStyle;

	Default
	{
		Damage 1;
		Projectile;
		-ACTIVATEIMPACT
		-ACTIVATEPCROSS
	}
	
	override int DoSpecialDamage (Actor target, int damage, Name damagetype)
	{
		if (target.player)
		{
			// Voodoo dolls forward this to the real player
			target.player.mo.MorphPlayer (NULL, PlayerClass, Duration, MorphStyle, MorphFlash, UnMorphFlash);
		}
		else
		{
			target.MorphMonster (MonsterClass, Duration, MorphStyle, MorphFlash, UnMorphFlash);
		}
		return -1;
	}

	
}

//===========================================================================
//
//
//
//===========================================================================

class MorphedMonster : Actor
{
	Actor UnmorphedMe;
	int UnmorphTime, MorphStyle;
	Class<Actor> MorphExitFlash;
	int FlagsSave;

	Default
	{
		Monster;
		-COUNTKILL
		+FLOORCLIP
	}

	override void OnDestroy ()
	{
		if (UnmorphedMe != NULL)
		{
			UnmorphedMe.Destroy ();
		}
		Super.OnDestroy();
	}

	override void Die (Actor source, Actor inflictor, int dmgflags, Name MeansOfDeath)
	{
		Super.Die (source, inflictor, dmgflags, MeansOfDeath);
		if (UnmorphedMe != NULL && UnmorphedMe.bUnmorphed)
		{
			UnmorphedMe.health = health;
			UnmorphedMe.Die (source, inflictor, dmgflags, MeansOfDeath);
		}
	}

	override void Tick ()
	{
		if (UnmorphTime > level.time || !UndoMonsterMorph())
		{
			Super.Tick();
		}
	}

	//----------------------------------------------------------------------------
	//
	// FUNC P_UndoMonsterMorph
	//
	// Returns true if the monster unmorphs.
	//
	//----------------------------------------------------------------------------

	virtual bool UndoMonsterMorph(bool force = false)
	{
		if (UnmorphTime == 0 || UnmorphedMe == NULL || bStayMorphed || UnmorphedMe.bStayMorphed)
		{
			return false;
		}
		let unmorphed = UnmorphedMe;
		unmorphed.SetOrigin (Pos, false);
		unmorphed.bSolid = true;
		bSolid = false;
		bool save = bTouchy;
		bTouchy = false;
		if (!force && !unmorphed.TestMobjLocation ())
		{ // Didn't fit
			unmorphed.bSolid = false;
			bSolid = true;
			bTouchy = save;
			UnmorphTime = level.time + 5*TICRATE; // Next try in 5 seconds
			return false;
		}
		unmorphed.Angle = Angle;
		unmorphed.target = target;
		unmorphed.bShadow = bShadow;
		unmorphed.bGhost = bGhost;
		unmorphed.bSolid = !!(flagssave & 2);
		unmorphed.bShootable = !!(flagssave & 4);
		unmorphed.bInvisible = !!(flagssave & 0x40);
		unmorphed.health = unmorphed.SpawnHealth();
		unmorphed.Vel = Vel;
		unmorphed.ChangeTid(tid);
		unmorphed.special = special;
		unmorphed.Score = Score;
		unmorphed.args[0] = args[0];
		unmorphed.args[1] = args[1];
		unmorphed.args[2] = args[2];
		unmorphed.args[3] = args[3];
		unmorphed.args[4] = args[4];
		unmorphed.CopyFriendliness (self, true);
		UnmorphedMe = NULL;
		Substitute(unmorphed);
		Destroy ();
		let eflash = Spawn(MorphExitFlash, Pos + (0, 0, gameinfo.TELEFOGHEIGHT), ALLOW_REPLACE);
		if (eflash)
			eflash.target = unmorphed;
		return true;
	}

	//===========================================================================
	//
	//
	//
	//===========================================================================

	override Actor, int, int MorphedDeath()
	{
		let realme = UnmorphedMe;
		if (realme != NULL)
		{
			if ((UnmorphTime) &&
				(MorphStyle & MRF_UNDOBYDEATH))
			{
				int realstyle = MorphStyle;
				int realhealth = health;
				if (UndoMonsterMorph(!!(MorphStyle & MRF_UNDOBYDEATHFORCED)))
				{
					return realme, realstyle, realhealth;
				}
			}
			if (realme.bBossDeath)
			{
				realme.health = 0;	// make sure that A_BossDeath considers it dead.
				realme.A_BossDeath();
			}
		}
		return null, 0, 0;
	}

}

