/*
** p_switch.cpp
** Switch and button maintenance and animation
**
**---------------------------------------------------------------------------
** Copyright 1998-2006 Randy Heit
** All rights reserved.
**
** Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions
** are met:
**
** 1. Redistributions of source code must retain the above copyright
**    notice, this list of conditions and the following disclaimer.
** 2. Redistributions in binary form must reproduce the above copyright
**    notice, this list of conditions and the following disclaimer in the
**    documentation and/or other materials provided with the distribution.
** 3. The name of the author may not be used to endorse or promote products
**    derived from this software without specific prior written permission.
**
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**---------------------------------------------------------------------------
**
*/



#include "doomdef.h"
#include "p_local.h"
#include "p_lnspec.h"
#include "p_3dmidtex.h"
#include "m_random.h"
#include "s_sound.h"
#include "doomstat.h"
#include "serializer_doom.h"
#include "p_maputl.h"
#include "p_spec.h"
#include "textures.h"
#include "actor.h"
#include "actorinlines.h"
#include "animations.h"

static FCRandom pr_switchanim ("AnimSwitch");

class DActiveButton : public DThinker
{
	DECLARE_CLASS (DActiveButton, DThinker)
public:
	void Construct(side_t *, int, FSwitchDef *, const DVector2 &pos, bool flippable);

	void Serialize(FSerializer &arc);
	void Tick ();

	side_t			*m_Side;
	int8_t			m_Part;
	bool			bFlippable;
	bool			bReturning;
	FSwitchDef		*m_SwitchDef;
	int32_t			m_Frame;
	uint32_t			m_Timer;
	DVector2		m_Pos;

protected:
	bool AdvanceFrame ();
};


//==========================================================================
//
// Start a button counting down till it turns off.
// [RH] Rewritten to remove MAXBUTTONS limit.
//
//==========================================================================

static bool P_StartButton (side_t *side, int Where, FSwitchDef *Switch, const DVector2 &pos, bool useagain)
{
	DActiveButton *button;
	auto iterator = side->GetLevel()->GetThinkerIterator<DActiveButton>();
	
	// See if button is already pressed
	while ( (button = iterator.Next ()) )
	{
		if (button->m_Side == side)
		{
			button->m_Timer=1;	// force advancing to the next frame
			return false;
		}
	}

	side->GetLevel()->CreateThinker<DActiveButton> (side, Where, Switch, pos, useagain);
	return true;
}

//==========================================================================
//
// Checks whether a switch is reachable
// This is optional because old maps can rely on being able to 
// use non-reachable switches.
//
//==========================================================================

bool P_CheckSwitchRange(AActor *user, line_t *line, int sideno, const DVector3 *optpos)
{
	// Activated from an empty side -> always succeed
	side_t *side = line->sidedef[sideno];
	if (side == NULL)
		return true;

	double checktop;
	double checkbot;
	sector_t *front = side->sector;
	FLineOpening open;
	int flags = line->flags;

	if (!side->GetTexture(side_t::mid).isValid())
	{ // Do not force range checks for 3DMIDTEX lines if there is no actual midtexture.
		flags &= ~ML_3DMIDTEX;
	}

	// 3DMIDTEX forces CHECKSWITCHRANGE because otherwise it might cause problems.
	if (!(flags & (ML_3DMIDTEX|ML_CHECKSWITCHRANGE)))
		return true;

	// calculate the point where the user would touch the wall.
	divline_t dll, dlu;
	double inter;
	DVector2 check;

	P_MakeDivline (line, &dll);

	DVector3 pos = optpos? *optpos : user->PosRelative(line);
	dlu.x = pos.X;
	dlu.y = pos.Y;
	dlu.dx = user->Angles.Yaw.Cos();
	dlu.dy = user->Angles.Yaw.Sin();
	inter = P_InterceptVector(&dll, &dlu);


	// Polyobjects must test the containing sector, not the one they originate from.
	if (line->sidedef[0]->Flags & WALLF_POLYOBJ)
	{
		// Get a check point slightly inside the polyobject so that this still works
		// if the polyobject lies directly on a sector boundary
		check.X = dll.x + dll.dx * (inter + 0.01);
		check.Y = dll.y + dll.dy * (inter + 0.01);
		front = line->GetLevel()->PointInSector(check);
	}
	else
	{
		check.X = dll.x + dll.dx * inter;
		check.Y = dll.y + dll.dy * inter;
	}


	// one sided line or polyobject
	if (line->sidedef[1] == NULL || (line->sidedef[0]->Flags & WALLF_POLYOBJ))
	{
	onesided:
		double sectorc = front->ceilingplane.ZatPoint(check);
		double sectorf = front->floorplane.ZatPoint(check);
		return (user->Top() >= sectorf && user->Z() <= sectorc);
	}

	// Now get the information from the line.
	P_LineOpening(open, NULL, line, check, &pos);
	if (open.range <= 0)
		goto onesided;

	if ((TexAnim.FindSwitch(side->GetTexture(side_t::top))) != NULL)
	{

		// Check 3D floors on back side
		{
			sector_t * back = line->sidedef[1 - sideno]->sector;
			for (auto rover : back->e->XFloor.ffloors)
			{
				if (!(rover->flags & FF_EXISTS)) continue;
				if (!(rover->flags & FF_UPPERTEXTURE)) continue;

				if (user->isAbove(rover->top.plane->ZatPoint(check)) ||
					user->Top() < rover->bottom.plane->ZatPoint(check))
					continue;

				// This 3D floor depicts a switch texture in front of the player's eyes
				return true;
			}
		}

		if ((user->Level->i_compatflags2 & COMPATF2_CHECKSWITCHRANGE)
			? (user->Top() >= open.top)
			: (user->Top() > open.top))
		{
			return true;
		}
	}
	if ((TexAnim.FindSwitch(side->GetTexture(side_t::bottom))) != NULL)
	{
		// Check 3D floors on back side
		{
			sector_t * back = line->sidedef[1 - sideno]->sector;
			for (unsigned i = 0; i < back->e->XFloor.ffloors.Size(); i++)
			{
				F3DFloor *rover = back->e->XFloor.ffloors[i];
				if (!(rover->flags & FF_EXISTS)) continue;
				if (!(rover->flags & FF_LOWERTEXTURE)) continue;

				if (user->isAbove(rover->top.plane->ZatPoint(check)) ||
					user->Top() < rover->bottom.plane->ZatPoint(check))
					continue;

				// This 3D floor depicts a switch texture in front of the player's eyes
				return true;
			}
		}

		if ((user->Level->i_compatflags2 & COMPATF2_CHECKSWITCHRANGE)
			? (user->Z() <= open.bottom)
			: (user->Z() < open.bottom))
		{
			return true;
		}
	}
	if ((flags & ML_3DMIDTEX) || (TexAnim.FindSwitch(side->GetTexture(side_t::mid))) != NULL)
	{
		// 3DMIDTEX lines will force a mid texture check if no switch is found on this line
		// to keep compatibility with Eternity's implementation.
		if (!P_GetMidTexturePosition(line, sideno, &checktop, &checkbot))
			return false;
		return user->isBelow(checktop) && user->Top() > checkbot;
	}
	else
	{
		// no switch found. Check whether the player can touch either top or bottom texture
		return (user->Level->i_compatflags2 & COMPATF2_CHECKSWITCHRANGE)
			? ( (user->Top() >= open.top) || (user->Z() <= open.bottom) )
			: ( (user->Top() > open.top)  || user->isBelow(open.bottom) );
	}
}

//==========================================================================
//
// Function that changes wall texture.
// Tell it if switch is ok to use again (1=yes, it's a button).
//
//==========================================================================

bool P_ChangeSwitchTexture (side_t *side, int useAgain, uint8_t special, bool *quest)
{
	int texture;
	FSoundID sound;
	FSwitchDef *Switch;

	if ((Switch = TexAnim.FindSwitch (side->GetTexture(side_t::top))) != NULL)
	{
		texture = side_t::top;
	}
	else if ((Switch = TexAnim.FindSwitch(side->GetTexture(side_t::mid))) != NULL)
	{
		texture = side_t::mid;
	}
	else if ((Switch = TexAnim.FindSwitch (side->GetTexture(side_t::bottom))) != NULL)
	{
		texture = side_t::bottom;
	}
	else
	{
		if (quest != NULL)
		{
			*quest = false;
		}
		return false;
	}

	// EXIT SWITCH?
	if (Switch->Sound != NO_SOUND)
	{
		sound = Switch->Sound;
	}
	else
	{
		sound = S_FindSound (
			special == Exit_Normal ||
			special == Exit_Secret ||
			special == Teleport_NewMap ||
			special == Teleport_EndGame
		   ? "switches/exitbutn" : "switches/normbutn");
	}

	// [RH] The original code played the sound at buttonlist->soundorg,
	//		which wasn't necessarily anywhere near the switch if it was
	//		facing a big sector (and which wasn't necessarily for the
	//		button just activated, either).
	DVector2 pt(side->linedef->v1->fPos() + side->linedef->Delta() / 2);
	bool playsound;

	side->SetTexture(texture, Switch->frames[0].Texture);
	if (useAgain || Switch->NumFrames > 1)
	{
		playsound = P_StartButton (side, texture, Switch, pt, !!useAgain);
	}
	else 
	{
		playsound = true;
	}
	if (playsound)
	{
		S_Sound (side->GetLevel(), DVector3(pt, 0), CHAN_VOICE, CHANF_LISTENERZ, sound, 1, ATTN_STATIC);
	}
	if (quest != NULL)
	{
		*quest = Switch->QuestPanel;
	}
	return true;
}

//==========================================================================
//
// Button thinker
//
//==========================================================================

IMPLEMENT_CLASS(DActiveButton, false, false)

void DActiveButton::Construct (side_t *side, int Where, FSwitchDef *Switch, const DVector2 &pos, bool useagain)
{
	m_Side = side;
	m_Part = int8_t(Where);
	m_Pos = pos;
	bFlippable = useagain;
	bReturning = false;

	m_SwitchDef = Switch;
	m_Frame = -1;
	AdvanceFrame ();
}

//==========================================================================
//
// operator<<
//
//==========================================================================

template<> FSerializer &Serialize (FSerializer &arc, const char *key, FSwitchDef* &Switch, FSwitchDef **def)
{
	if (arc.isWriting())
	{
		Serialize(arc, key, Switch->PreTexture, nullptr);
	}
	else
	{
		FTextureID tex;
		tex.SetInvalid();
		Serialize(arc, key, tex, nullptr);
		Switch = TexAnim.FindSwitch(tex);
	}
	return arc;
}

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

void DActiveButton::Serialize(FSerializer &arc)
{
	Super::Serialize (arc);
	arc("side", m_Side)
		("part", m_Part)
		("switchdef", m_SwitchDef)
		("frame", m_Frame)
		("timer", m_Timer)
		("fippable", bFlippable)
		("pos", m_Pos)
		("returning", bReturning);
}

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

void DActiveButton::Tick ()
{
	if (m_SwitchDef == NULL)
	{
		// We lost our definition due to a bad savegame.
		Destroy();
		return;
	}

	FSwitchDef *def = bReturning? m_SwitchDef->PairDef : m_SwitchDef;
	if (--m_Timer == 0)
	{
		if (m_Frame == def->NumFrames - 1)
		{
			bReturning = true;
			def = m_SwitchDef->PairDef;
			if (def != NULL)
			{
				m_Frame = -1;
				S_Sound (Level, DVector3(m_Pos, 0), CHAN_VOICE, CHANF_LISTENERZ,
					def->Sound != NO_SOUND ? FSoundID(def->Sound) : S_FindSound("switches/normbutn"),
					1, ATTN_STATIC);
				bFlippable = false;
			}
			else
			{
				Destroy ();
				return;
			}
		}
		bool killme = AdvanceFrame ();

		m_Side->SetTexture(m_Part, def->frames[m_Frame].Texture);

		if (killme)
		{
			Destroy ();
		}
	}
}

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

bool DActiveButton::AdvanceFrame ()
{
	bool ret = false;
	FSwitchDef *def = bReturning? m_SwitchDef->PairDef : m_SwitchDef;

	if (++m_Frame == def->NumFrames - 1)
	{
		if (bFlippable == true)
		{
			m_Timer = BUTTONTIME;
		}
		else
		{
			ret = true;
		}
	}
	else
	{
		m_Timer = def->frames[m_Frame].TimeMin;
		if (def->frames[m_Frame].TimeRnd != 0)
		{
			m_Timer += pr_switchanim(def->frames[m_Frame].TimeRnd);
		}
	}
	return ret;
}

