////////////////////////////////////////////////////////////////////////////////
//
// Copyright 2016 RWS Inc, All Rights Reserved
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of version 2 of the GNU General Public License as published by
// the Free Software Foundation
//
// 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.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
//
// crawler.h
// Project: Nostril (aka Postal)
//
//	History:
//		04/19/97 MJR	Started.
//
//		04/21/97	MJR/JMI	Converted to 3D/Converted to Postal.
//
//		04/21/97	JMI	Removed ASSERTs that values were greater than 0 in Move().
//							These were to protect against errors when using floor(),
//							which we no longer use.
//
//		04/29/97	JMI	CanWalk() no longer sets the clip case of the get attribute
//							call to include a max height (it just relies on 'no walk'
//							now).
//
//		05/29/97	JMI	Changed m_pRealm->m_pHeightMap->GetHeight() call to 
//							m_pRealm->GetTerrainAttributes().
//
//		06/26/97	JMI	Now uses m_prealm->Map3Dto2D() in Plot() (was using
//							the global version defined in reality.h which is no more).
//
//		07/01/97	JMI	Now uses GetHeightAndNoWalk() in place of 
//							GetTerrainAttributes().
//
//		08/10/97	JMI	Now crawls to the 'pushed' point.  If the crawler does make
//							it all the way to the 'pushed' point, we go back to the
//							point we crawled to just previously (which we know is 
//							valid).
//
//		08/11/97	JMI	Changed some casting in comparisons.
//
////////////////////////////////////////////////////////////////////////////////
#ifndef CRAWLER_H
#define CRAWLER_H


#include "RSPiX.h"

#include "realm.h"
#include "reality.h"

class CCrawler
	{
	//------------------------------------------------------------------------------
	// Types, enums, etc.
	//------------------------------------------------------------------------------
	public:
		typedef struct
			{
			// These values are supplied by the user
			int16_t sX;
			int16_t sZ;
			int16_t sHard;
			int16_t sPushDir;
			double dPushMag;

			// These values are for internal use only
			double dPushX;
			double dPushZ;
//			double dDragX;
//			double dDragZ;

			int16_t	sHeight;		// Stored height used only by IsGood().
			} Nub;

	//------------------------------------------------------------------------------
	// Variables
	//------------------------------------------------------------------------------
	protected:
		int16_t m_sNum;
		Nub* m_pnub;
		double m_dPushX;
		double m_dPushZ;
		double m_dPushMaxX;
		double m_dPushMaxZ;

	public:
		CRealm*	m_prealm;			// Used to access the height map, the effect map,
											// the scene, all that good stuff.
		int16_t	m_sVertTolerance;		// Maximum amount crawler can step up.

	//------------------------------------------------------------------------------
	// Functions
	//------------------------------------------------------------------------------
	public:
		////////////////////////////////////////////////////////////////////////////////
		// Constructor
		////////////////////////////////////////////////////////////////////////////////
		CCrawler()
			{
			m_pnub = 0;
			m_sNum = 0;
			m_prealm	= 0;
			}


		////////////////////////////////////////////////////////////////////////////////
		// Destructor
		////////////////////////////////////////////////////////////////////////////////
		~CCrawler()
			{
			m_pnub = 0;
			m_sNum = 0;
			}


		////////////////////////////////////////////////////////////////////////////////
		// Setup crawler
		////////////////////////////////////////////////////////////////////////////////
		void Setup(
			int16_t sNum,										// In:  Number of Nub's in array
			Nub* pnub,										// In:  Pointer to array of Nub's
			double dPushMaxX,								// In:  Maximum push in x direction
			double dPushMaxZ)								// In:  Maximum push in z direction
			{
			// Save info
			m_sNum = sNum;
			m_pnub = pnub;
			m_dPushMaxX = dPushMaxX;
			m_dPushMaxZ	= dPushMaxZ;

			// Precalculate push and drag values based on specified directions and magnitudes
			for (int16_t s = 0; s < m_sNum; s++)
				{
				m_pnub[s].dPushX =  rspCos(rspMod360(m_pnub[s].sPushDir)) * m_pnub[s].dPushMag;
				m_pnub[s].dPushZ = -rspSin(rspMod360(m_pnub[s].sPushDir)) * m_pnub[s].dPushMag;
				}
			}


		////////////////////////////////////////////////////////////////////////////////
		// Start at position #1 and move along the "path" leading to position #2, the
		// goal being to reach position #2.  The attributes encountered along the way
		// will determine how far we get before stopping.
		//
		// The returned position is the resulting position.  In the best case, it will
		// be equal to  position #2.  Worst case, it will be equal to position #1.
		// Otherwise, it will be somewhere between the two.
		//
		// As long as position #1 is valid, this function will be successfull.  This
		// should be the case as long as the caller starts out at a valid position and
		// never moves without using this function.
		//
		// If position #1 is NOT valid, this function will PROBABLY FAIL, as indicated
		// by a non-zero return value.  It is possible that it will manage to find what
		// appears to be a valid position; however, not only is this unlikely, but it
		// is important to note that it may only APPEAR to be valid -- there is no way
		// to know whether it is ACTUALLY valid.
		////////////////////////////////////////////////////////////////////////////////
		int16_t Move(											// Returns 0 if successfull, non-zero otherwise
			double dx1,										// In:  Position #1 xcoord
			double dy1,										// In:  Position #1 ycoord
			double dz1,										// In:  Position #1 zcoord
			double dx2,										// In:  Position #2 xcoord
			double dy2,										// In:  Position #2 ycoord
			double dz2,										// In:  Position #2 zcoord
			double* pdx,									// Out: Final position xcoord
			double* pdy,									// Out: Final position ycoord
			double* pdz,									// Out: Final position zcoord
			int16_t* psTerrainH)							// Out: Final terrain height

			{
			int16_t sResult = 0;

			int16_t sx1 = (int16_t)dx1;
			int16_t sy1 = (int16_t)dy1;
			int16_t sz1 = (int16_t)dz1;
			int16_t sx2 = (int16_t)dx2;
			int16_t sy2 = (int16_t)dy2;
			int16_t sz2 = (int16_t)dz2;

			// Check if starting position is valid
			if (IsGood(sx1, sy1, sz1, psTerrainH))
				{

				// Creep as far as possible towards new position
				int16_t sx;
				int16_t sy;
				int16_t	sz;
				CrawlWhileGood(sx1, sy1, sz1, sx2, sy2, sz2, &sx, &sy, &sz, psTerrainH);

				double dx	= sx;
				double dy	= sy;
				double dz	= sz;
				// If we made it to the end point, we should include the fractional value
				// lost in converting to integers when we initially assigned into
				// sx2 and sz2.
				if (sx == sx2)
					{
					dx = dx2;
					}

				if (sz == sz2)
					{
					dz = dz2;
					}

				if (dy2 < 0.0)
					dy += floor(dy2) - dy2;
				else
					dy += dy2 - floor(dy2);

				// Limit amount of push
				if (m_dPushX > m_dPushMaxX)
					m_dPushX = m_dPushMaxX;
				if (m_dPushZ > m_dPushMaxZ)
					m_dPushZ = m_dPushMaxZ;

				// Calculate "pushed" position and see if it's valid
				double dx2 = dx + m_dPushX;
				double dy2 = dy;
				double dz2 = dz + m_dPushZ;
				int16_t  sTerrainH	= *psTerrainH;	// Start with a good value b/c CrawlWhileGood() 
															// won't update the value unless there's movement.
				CrawlWhileGood((int16_t)dx, (int16_t)dy, (int16_t)dz, (int16_t)dx2, (int16_t)dy2, (int16_t)dz2, &sx, &sy, &sz, &sTerrainH);
				if ((int16_t)dx2 == sx && (int16_t)dy2 == sy && (int16_t)dz2 == sz)
					{
					// Use pushed position
					*pdx = dx2;
					*pdy = dy2;
					*pdz = dz2;
					*psTerrainH	= sTerrainH;
					}
				else
					{
					// Pushed position was not valid, so use known-good position instead
					*pdx = dx;
					*pdy = dy;
					*pdz = dz;
					}
				}
			else
				{
				// Starting position is invalid, so we're pretty much screwed.  As a
				// last-ditch effort, check if the ending position is miraculously valid.
				int16_t  sTerrainH;
				if (IsGood(sx2, sy2, sz2, &sTerrainH) )
					{
					// Use the ending position.  This might make things worse by moving to
					// past some off-limits barrier, but since we don't have any other way
					// to get valid again, we might as well try this and hope for the best.
					*pdx = dx2;
					*pdy = dy2;
					*pdz = dz2;
					*psTerrainH	= sTerrainH;
					TRACE("Crawler::Move(): Starting position is invalid, using psuedo-valid ending position!\n");
					}
				else
					{
					// Stick with the starting position, which is, unfortunately, invalid
					*pdx = dx1;
					*pdy = dy1;
					*pdz = dz1;
					sResult = -1;
					TRACE("Crawler::Move(): Starting position is invalid!\n");
					}
				}

			return sResult;
			}


	protected:
		////////////////////////////////////////////////////////////////////////////////
		// Crawl as far as possible along the path from positon #1 to position #2, the
		// goal being to reach position #2.
		//
		// This crawls along one pixel at a time (just like a line-draw algorithm),
		// checking each point to see if it's valid.  If so, it goes on.  If not, it
		// returns the previous point, which was valid.
		//
		// This function assumes position #1 is valid!  If this is not the case, all
		// bets are off!!!
		////////////////////////////////////////////////////////////////////////////////
		void CrawlWhileGood(
			int16_t sx1,
			int16_t sy1,
			int16_t sz1,
			int16_t sx2,
			int16_t sy2,
			int16_t sz2,
			int16_t* psOutX,
			int16_t* psOutY,
			int16_t* psOutZ,
			int16_t* psTerrainH)	// In/Out: Terrain height.
			{
			int16_t	sMaxTerrainH;

			// Clear push values
			m_dPushX = 0.0;
			m_dPushZ = 0.0;

			// Make a copy of the starting position
			int16_t sx = sx1;
			int16_t sy = sy1;
			int16_t sz = sz1;

			// Set initial "known good" position
			int16_t sGoodX = sx;
			int16_t sGoodY = sy;
			int16_t sGoodZ = sz;
			int16_t	sGoodH = *psTerrainH;

			// Calculate delta x and delta y
			int16_t sdx = sx2 - sx;
			int16_t sdy = sy2 - sy;
			int16_t sdz = sz2 - sz;

			// Make sure there's some movement, otherwise don't bother
			if (sdx || sdy || sdz)
				{

				// Calculate offsets for each delta
				int16_t saddx = (sdx >= 0) ? +1 : -1;
				int16_t saddy = (sdy >= 0) ? +1 : -1;
				int16_t saddz = (sdz >= 0) ? +1 : -1;

				// Make all deltas positive
				sdx = ABS(sdx);
				sdy = ABS(sdy);
				sdz = ABS(sdz);

				// This uses a Bresenham-like algorithm to move one pixel at a time along the
				// line from (sx,sy,sz) to (sx2,sy2,sz2).	 We basically continue along until
				// we hit an invalid position or the ending position, whichever comes first.
				int16_t serry;
				int16_t serrx;
				int16_t serrz;
				if ((sdx >= sdy) && (sdx >= sdz))
					{
					// dx is largest
					serry = sdx / 2;
					serrz = sdx / 2;
					do {
						serry += sdy;
						if (serry >= sdx)
							{
							serry -= sdx;
							sy += saddy;
							}
						serrz += sdz;
						if (serrz >= sdx)
							{
							serrz -= sdx;
							sz += saddz;
							}
						sx += saddx;
						if (!IsGood(sx, sy, sz, &sMaxTerrainH))
							break;
						sGoodX = sx;
						sGoodY = sy;
						sGoodZ = sz;
						sGoodH = sMaxTerrainH;
						} while (sx != sx2);
					}
				else if ((sdy >= sdx) && (sdy >= sdz))
					{
					// dy is largest
					serrx = sdy / 2;
					serrz = sdy / 2;
					do {
						serrx += sdx;
						if (serrx >= sdy)
							{
							serrx -= sdy;
							sx += saddx;
							}
						serrz += sdz;
						if (serrz >= sdy)
							{
							serrz -= sdy;
							sz += saddz;
							}
						sy += saddy;
						if (!IsGood(sx, sy, sz, &sMaxTerrainH))
							break;
						sGoodX = sx;
						sGoodY = sy;
						sGoodZ = sz;
						sGoodH = sMaxTerrainH;
						} while (sy != sy2);
					}
				else
					{
					// dz is largest
					serry = sdz / 2;
					serrx = sdz / 2;
					do {
						serry += sdy;
						if (serry >= sdz)
							{
							serry -= sdz;
							sy += saddy;
							}
						serrx += sdx;
						if (serrx >= sdz)
							{
							serrx -= sdz;
							sx += saddx;
							}
						sz += saddz;
						if (!IsGood(sx, sy, sz, &sMaxTerrainH))
							break;
						sGoodX = sx;
						sGoodY = sy;
						sGoodZ = sz;
						sGoodH = sMaxTerrainH;
						} while (sz != sz2);
					}
				}

			// Return good position
			*psOutX = sGoodX;
			*psOutY = sGoodY;
			*psOutZ = sGoodZ;
			*psTerrainH = sGoodH;
			}

		////////////////////////////////////////////////////////////////////////////////
		// Determine whether specified position is valid.
		//
		// Also updates push values as specified by any nubs are not valid.
		////////////////////////////////////////////////////////////////////////////////
		bool IsGood(
			int16_t sBaseX,				// In
			int16_t sBaseY,				// In
			int16_t sBaseZ,				// In
			int16_t* psMaxTerrainH)	// Out: Maximum terrain height from scan.
			{
			bool bResult = true;

			int16_t sMaxTerrainH	= -32768;	// Default to lowest value.

			int16_t	sTerrainH;
			for (int16_t s = 0; s < m_sNum; s++)
				{
				int16_t sx = sBaseX + m_pnub[s].sX;
				int16_t sz = sBaseZ + m_pnub[s].sZ;

				if (CanWalk(sx, sBaseY, sz, &sTerrainH) == true)
					{
//					Plot((U8)0xfa, sx, sBaseY, sz);
					}
				else
					{
//					Plot((U8)0xf9, sx, sBaseY, sz);
					if (m_pnub[s].sHard)
						bResult = false;
					m_dPushX += m_pnub[s].dPushX;
					m_dPushZ += m_pnub[s].dPushZ;
					}

				// If this is a hard nub . . .
				if (m_pnub[s].sHard)
					{
					// If this height is larger than the previous max . . .
					if (sTerrainH > sMaxTerrainH)
						{
						sMaxTerrainH	= sTerrainH;
						}
					}
				}

			// Store for return.
			*psMaxTerrainH	= sMaxTerrainH;
			
			return bResult;
			}


		////////////////////////////////////////////////////////////////////////////////
		// Centralized location for checking attributes and deciding how to interpret
		// them.
		////////////////////////////////////////////////////////////////////////////////
		bool CanWalk(	// Returns true if we can walk there, false otherwise.
			int16_t sx,	// In:  X position on attribute map.
			int16_t	sy,	// In:  Y position on attribute map.
			int16_t sz,	// In:  Z position on attribute map.
			int16_t* psH)	// Out: Terrain height at X/Z.
			{
			bool	bCanWalk;
			bool	bCannotWalk;
			*psH	= m_prealm->GetHeightAndNoWalk(sx, sz, &bCannotWalk);
			if (bCannotWalk == true								// Not walkable
				|| (*psH - sy > m_sVertTolerance) )			// Terrain higher by m_sVertTolerance.
				{
				bCanWalk	= false;
				}
			else
				{
				bCanWalk	= true;
				}

			return bCanWalk;
			}

		////////////////////////////////////////////////////////////////////////////////
		// Plot a point via the CScene.  
		////////////////////////////////////////////////////////////////////////////////
		void Plot(		// Returns nothing.
			U8	u8Color,	// Color index.
			int16_t	sx,	// In:  X position.
			int16_t	sy,	// In:  Y position.
			int16_t sz)	// In:  Z position.
			{
			// Create a line sprite.
			CSpriteLine2d*	psl2d	= new CSpriteLine2d;
			if (psl2d != NULL)
				{
				m_prealm->Map3Dto2D(
					sx, 
					sy, 
					sz, 
					&(psl2d->m_sX2), 
					&(psl2d->m_sY2) );

				psl2d->m_sX2End		= psl2d->m_sX2;
				psl2d->m_sY2End		= psl2d->m_sY2;

				psl2d->m_sPriority	= sz;
				psl2d->m_sLayer		= CRealm::GetLayerViaAttrib(m_prealm->GetLayer(sx, sz) );
				psl2d->m_u8Color		= u8Color;
				// Destroy when done.
				psl2d->m_sInFlags	= CSprite::InDeleteOnRender;
				// Put 'er there.
				m_prealm->m_scene.UpdateSprite(psl2d);
				}
			}

	};


#endif //CRAWLER_H
////////////////////////////////////////////////////////////////////////////////
// EOF
////////////////////////////////////////////////////////////////////////////////
