/*
 * Copyright 2010-2016 OpenXcom Developers.
 *
 * This file is part of OpenXcom.
 *
 * OpenXcom 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.
 *
 * OpenXcom 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 OpenXcom.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "UnitFallBState.h"
#include <algorithm>
#include "TileEngine.h"
#include "Pathfinding.h"
#include "Map.h"
#include "Camera.h"
#include "../Savegame/BattleUnit.h"
#include "../Savegame/SavedBattleGame.h"
#include "../Savegame/Tile.h"
#include "../Engine/Options.h"
#include "../Mod/Armor.h"

namespace OpenXcom
{

/**
 * Sets up an UnitFallBState.
 * @param parent Pointer to the Battlescape.
 */
UnitFallBState::UnitFallBState(BattlescapeGame *parent) : BattleState(parent), _terrain(0)
{

}

/**
 * Deletes the UnitWalkBState.
 */
UnitFallBState::~UnitFallBState()
{

}

/**
 * Initializes the state.
 */
void UnitFallBState::init()
{
	_terrain = _parent->getTileEngine();
	if (_parent->getSave()->getSide() == FACTION_PLAYER)
		_parent->setStateInterval(Options::battleXcomSpeed);
	else
		_parent->setStateInterval(Options::battleAlienSpeed);

}

/**
 * Runs state functionality every cycle.
 * Progresses the fall, updates the battlescape, ...
 */
void UnitFallBState::think()
{
	for (std::list<BattleUnit*>::iterator unit = _parent->getSave()->getFallingUnits()->begin(); unit != _parent->getSave()->getFallingUnits()->end();)
	{
		if ((*unit)->getStatus() == STATUS_TURNING)
		{
			(*unit)->abortTurn();
		}
		bool largeCheck = true;
		bool falling = true;
		int size = (*unit)->getArmor()->getSize() - 1;
		if ((*unit)->getHealth() == 0 || (*unit)->getStunlevel() >= (*unit)->getHealth())
		{
			unit = _parent->getSave()->getFallingUnits()->erase(unit);
			continue;
		}
		bool onScreen = ((*unit)->getVisible() && _parent->getMap()->getCamera()->isOnScreen((*unit)->getPosition(), true, size, false));
		Tile *tileBelow = _parent->getSave()->getTile((*unit)->getPosition() + Position(0,0,-1));
		for (int x = size; x >= 0; x--)
		{
			for (int y = size; y >= 0; y--)
			{
				Tile *otherTileBelow = _parent->getSave()->getTile((*unit)->getPosition() + Position(x,y,-1));
				if (!_parent->getSave()->getTile((*unit)->getPosition() + Position(x,y,0))->hasNoFloor(otherTileBelow) || (*unit)->getMovementType() == MT_FLY)
				{
					largeCheck = false;
				}
			}
		}

		if ((*unit)->getStatus() == STATUS_WALKING || (*unit)->getStatus() == STATUS_FLYING)
		{
			(*unit)->keepWalking(tileBelow, true); 	// advances the phase
			_parent->getMap()->cacheUnit(*unit);	// make sure the unit sprites are up to date

			if ((*unit)->getPosition() != (*unit)->getLastPosition())
			{
				// Reset tiles moved from.
				for (int x = size; x >= 0; x--)
				{
					for (int y = size; y >= 0; y--)
					{
						// A falling unit might have already taken up this position so check that this unit is still here.
						if (_parent->getSave()->getTile((*unit)->getLastPosition() + Position(x,y,0))->getUnit() == (*unit))
						{
							_parent->getSave()->getTile((*unit)->getLastPosition() + Position(x,y,0))->setUnit(0);
						}
					}
				}
				// Update tiles moved to.
				for (int x = size; x >= 0; x--)
				{
					for (int y = size; y >= 0; y--)
					{
						_parent->getSave()->getTile((*unit)->getPosition() + Position(x,y,0))->setUnit((*unit), _parent->getSave()->getTile((*unit)->getPosition() + Position(x,y,-1)));
					}
				}
			}

			++unit;
			continue;
		}

		falling = largeCheck
			&& (*unit)->getPosition().z != 0
			&& (*unit)->getTile()->hasNoFloor(tileBelow)
			&& (*unit)->getMovementType() != MT_FLY
			&& (*unit)->getWalkingPhase() == 0;

		if (falling)
		{
			// Tile(s) unit is falling into.
			for (int x = (*unit)->getArmor()->getSize() - 1; x >= 0; --x)
			{
				for (int y = (*unit)->getArmor()->getSize() - 1; y >= 0; --y)
				{
					Tile *tileTarget = _parent->getSave()->getTile((*unit)->getPosition() + Position(x,y,-1));
					tilesToFallInto.push_back(tileTarget);
				}
			}
			std::list<BattleUnit*> *fallingUnits = _parent->getSave()->getFallingUnits();
			// Check each tile for units that need moving out of the way.
			for (std::vector<Tile*>::iterator i = tilesToFallInto.begin(); i < tilesToFallInto.end(); ++i)
			{
				BattleUnit *unitBelow = (*i)->getUnit();
				if (unitBelow
					&& !(std::find(fallingUnits->begin(), fallingUnits->end(), unitBelow) != fallingUnits->end())  // ignore falling units (including self)
					&& !(std::find(unitsToMove.begin(), unitsToMove.end(), unitBelow) != unitsToMove.end()))       // ignore already added units
				{
					unitsToMove.push_back(unitBelow);
				}
			}
		}

		falling = largeCheck
			&& (*unit)->getPosition().z != 0
			&& (*unit)->getTile()->hasNoFloor(tileBelow)
			&& (*unit)->getMovementType() != MT_FLY
			&& (*unit)->getWalkingPhase() == 0;

		// we are just standing around, we are done falling.
		if ((*unit)->getStatus() == STATUS_STANDING)
		{
			if (falling)
			{
				Position destination = (*unit)->getPosition() + Position(0,0,-1);
				Tile *tileDest = _parent->getSave()->getTile(destination);
				(*unit)->startWalking(Pathfinding::DIR_DOWN, destination, tileDest, onScreen);
				(*unit)->setCache(0);
				_parent->getMap()->cacheUnit(*unit);
				++unit;
			}
			else
			{
				// if the unit burns floortiles, burn floortiles
				if ((*unit)->getSpecialAbility() == SPECAB_BURNFLOOR || (*unit)->getSpecialAbility() == SPECAB_BURN_AND_EXPLODE)
				{
					(*unit)->getTile()->ignite(1);
					Position groundVoxel = ((*unit)->getPosition() * Position(16,16,24)) + Position(8,8,-((*unit)->getTile()->getTerrainLevel()));
					_parent->getTileEngine()->hit(groundVoxel, (*unit)->getBaseStats()->strength, DT_IN, (*unit));

					if ((*unit)->getStatus() != STATUS_STANDING) // ie: we burned a hole in the floor and fell through it
					{
						_parent->getPathfinding()->abortPath();
					}
				}
				// move our personal lighting with us
				_terrain->calculateUnitLighting();
				_parent->getMap()->cacheUnit(*unit);
				(*unit)->setCache(0);
				_terrain->calculateFOV(*unit);
				_parent->checkForProximityGrenades(*unit);
				if ((*unit)->getStatus() == STATUS_STANDING)
				{
					if (_parent->getTileEngine()->checkReactionFire(*unit))
						_parent->getPathfinding()->abortPath();
					unit = _parent->getSave()->getFallingUnits()->erase(unit);
				}
			}
		}
		else
		{
			++unit;
		}
	}


	// Find somewhere to move the unit(s) In danger of being squashed.
	if (!unitsToMove.empty())
	{
		std::vector<Tile*> escapeTiles;
		for (std::vector<BattleUnit*>::iterator ub = unitsToMove.begin(); ub < unitsToMove.end(); )
		{
			BattleUnit *unitBelow = (*ub);
			bool escapeFound = false;

			// We need to move all sections of the unit out of the way.
			std::vector<Position> bodySections;
			for (int x = unitBelow->getArmor()->getSize() - 1; x >= 0; --x)
			{
				for (int y = unitBelow->getArmor()->getSize() - 1; y >= 0; --y)
				{
					Position bs = unitBelow->getPosition() + Position(x, y, 0);
					bodySections.push_back(bs);
				}
			}

			// Check in each compass direction.
			for (int dir = 0; dir < Pathfinding::DIR_UP && !escapeFound; dir++)
			{
				Position offset;
				Pathfinding::directionToVector(dir, &offset);
				Position destination = unitBelow->getPosition() + offset;
				if (_parent->getSave()->getPathfinding()->getTUCost(unitBelow->getPosition(), dir, &destination, unitBelow, 0, false) == 255)
				{
					// can't move this way, try another direction
					continue;
				}
				for (std::vector<Position>::iterator bs = bodySections.begin(); bs < bodySections.end(); )
				{
					Position originalPosition = (*bs);
					Position endPosition = originalPosition + offset;

					Tile *t = _parent->getSave()->getTile(endPosition);
					Tile *bt = _parent->getSave()->getTile(endPosition + Position(0,0,-1));

					bool aboutToBeOccupiedFromAbove = t && std::find(tilesToFallInto.begin(), tilesToFallInto.end(), t) != tilesToFallInto.end();
					bool alreadyTaken = t && std::find(escapeTiles.begin(), escapeTiles.end(), t) != escapeTiles.end();
					bool alreadyOccupied = t && t->getUnit() && (t->getUnit() != unitBelow);
					bool hasFloor = t && !t->hasNoFloor(bt);
					bool unitCanFly = unitBelow->getMovementType() == MT_FLY;

					bool canMoveToTile = t && !alreadyOccupied && !alreadyTaken && !aboutToBeOccupiedFromAbove && (hasFloor || unitCanFly);
					if (canMoveToTile)
					{
						// Check next section of the unit.
						++bs;
					}
					else
					{
						// Try next direction.
						break;
					}

					// If all sections of the fallen onto unit can be moved, then we move it.
					if (bs == bodySections.end())
					{
						if (_parent->getSave()->addFallingUnit(unitBelow))
						{
							escapeFound = true;
							// Now ensure no other unit escapes to here too.
							for (int x = unitBelow->getArmor()->getSize() - 1; x >= 0; --x)
							{
								for (int y = unitBelow->getArmor()->getSize() - 1; y >= 0; --y)
								{
									Tile *et = _parent->getSave()->getTile(t->getPosition() + Position(x,y,0));
									escapeTiles.push_back(et);
								}
							}

							Tile *bu = _parent->getSave()->getTile(originalPosition + Position(0,0,-1));
							unitBelow->startWalking(dir, unitBelow->getPosition() + offset, bu,
								(unitBelow->getVisible() && _parent->getMap()->getCamera()->isOnScreen(unitBelow->getPosition(), true, unitBelow->getArmor()->getSize() - 1, false)));
							ub = unitsToMove.erase(ub);
						}
					}
				}
			}
			if (!escapeFound)
			{
				// STOMP THAT GOOMBA!
				unitBelow->knockOut(_parent);
				ub = unitsToMove.erase(ub);
			}
		}
		_parent->checkForCasualties(0,0);
	}

	if (_parent->getSave()->getFallingUnits()->empty())
	{
		tilesToFallInto.clear();
		unitsToMove.clear();
		_parent->popState();
		return;
	}
}

}
