/*
 * 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 <fstream>
#include "Camera.h"
#include "Map.h"
#include "../Engine/Action.h"
#include "../Engine/Options.h"
#include "../Engine/Timer.h"
#include "../fmath.h"

namespace OpenXcom
{

/**
 * Sets up a camera.
 * @param spriteWidth Width of map sprite.
 * @param spriteHeight Height of map sprite.
 * @param mapsize_x Current map size in X axis.
 * @param mapsize_y Current map size in Y axis.
 * @param mapsize_z Current map size in Z axis.
 * @param map Pointer to map surface.
 * @param visibleMapHeight Current height the view is at.
 */
Camera::Camera(int spriteWidth, int spriteHeight, int mapsize_x, int mapsize_y, int mapsize_z, Map *map, int visibleMapHeight) : _scrollMouseTimer(0), _scrollKeyTimer(0), _spriteWidth(spriteWidth), _spriteHeight(spriteHeight), _mapsize_x(mapsize_x), _mapsize_y(mapsize_y), _mapsize_z(mapsize_z), _screenWidth(map->getWidth()), _screenHeight(map->getHeight()),
																																 _mapOffset(-250,250,0), _scrollMouseX(0), _scrollMouseY(0), _scrollKeyX(0), _scrollKeyY(0), _scrollTrigger(false), _visibleMapHeight(visibleMapHeight), _showAllLayers(false), _map(map)
{
}

/**
 * Deletes the Camera.
 */
Camera::~Camera()
{

}

/**
 * Sets the camera's scrolling timer.
 * @param mouse Pointer to mouse timer.
 * @param key Pointer to key timer.
 */
void Camera::setScrollTimer(Timer *mouse, Timer *key)
{
	_scrollMouseTimer = mouse;
	_scrollKeyTimer = key;
}

/**
 * Handles camera mouse shortcuts.
 * @param action Pointer to an action.
 * @param state State that the action handlers belong to.
 */
void Camera::mousePress(Action *action, State *)
{
	if (action->getDetails()->button.button == SDL_BUTTON_LEFT && Options::battleEdgeScroll == SCROLL_TRIGGER)
	{
		_scrollTrigger = true;
		mouseOver(action, 0);
	}
	else if (Options::battleDragScrollButton != SDL_BUTTON_MIDDLE || (SDL_GetMouseState(0,0)&SDL_BUTTON(Options::battleDragScrollButton)) == 0)
	{
		if (action->getDetails()->button.button == SDL_BUTTON_WHEELUP)
		{
			up();
		}
		else if (action->getDetails()->button.button == SDL_BUTTON_WHEELDOWN)
		{
			down();
		}
	}
}

/**
 * Handles camera mouse shortcuts.
 * @param action Pointer to an action.
 * @param state State that the action handlers belong to.
 */
void Camera::mouseRelease(Action *action, State *)
{
	if (action->getDetails()->button.button == SDL_BUTTON_LEFT && Options::battleEdgeScroll == SCROLL_TRIGGER)
	{
		_scrollMouseX = 0;
		_scrollMouseY = 0;
		_scrollMouseTimer->stop();
		_scrollTrigger = false;
		int posX = action->getXMouse();
		int posY = action->getYMouse();
		if ((posX < (SCROLL_BORDER * action->getXScale()) && posX > 0)
			|| (posX > (_screenWidth - SCROLL_BORDER) * action->getXScale())
			|| (posY < (SCROLL_BORDER * action->getYScale()) && posY > 0)
			|| (posY > (_screenHeight - SCROLL_BORDER) * action->getYScale()))
			// A cheap hack to avoid handling this event as a click
			// on the map when the mouse is on the scroll-border
			action->getDetails()->button.button = 0;
	}
}

/**
 * Handles mouse over events.
 * @param action Pointer to an action.
 * @param state State that the action handlers belong to.
 */
void Camera::mouseOver(Action *action, State *)
{
	if (_map->getCursorType() == CT_NONE)
	{
		return;
	}

	if (Options::battleEdgeScroll == SCROLL_AUTO || _scrollTrigger)
	{
		int posX = action->getXMouse();
		int posY = action->getYMouse();
		int scrollSpeed = Options::battleScrollSpeed;

		//left scroll
		if (posX < (SCROLL_BORDER * action->getXScale()) && posX >= 0)
		{
			_scrollMouseX = scrollSpeed;
			// if close to top or bottom, also scroll diagonally
			//downleft
			if (posY < (SCROLL_DIAGONAL_EDGE * action->getYScale()) && posY >= 0)
			{
				_scrollMouseY = scrollSpeed/2;
			}
			//upleft
			else if (posY > (_screenHeight - SCROLL_DIAGONAL_EDGE) * action->getYScale())
			{
				_scrollMouseY = -scrollSpeed/2;
			}
			else _scrollMouseY = 0;
		}
		//right scroll
		else if (posX > (_screenWidth - SCROLL_BORDER) * action->getXScale())
		{
			_scrollMouseX = -scrollSpeed;
			// if close to top or bottom, also scroll diagonally
			//downright
			if (posY <= (SCROLL_DIAGONAL_EDGE * action->getYScale()) && posY >= 0)
			{
				_scrollMouseY = scrollSpeed/2;
			}
			//upright
			else if (posY > (_screenHeight - SCROLL_DIAGONAL_EDGE) * action->getYScale())
			{
				_scrollMouseY = -scrollSpeed/2;
			}
			else _scrollMouseY = 0;
		}
		else if (posX)
		{
			_scrollMouseX = 0;
		}

		//up
		if (posY < (SCROLL_BORDER * action->getYScale()) && posY >= 0)
		{
			_scrollMouseY = scrollSpeed;
			// if close to left or right edge, also scroll diagonally
			//up left
			if (posX < (SCROLL_DIAGONAL_EDGE * action->getXScale()) && posX >= 0)
			{
				_scrollMouseX = scrollSpeed;
				_scrollMouseY /=2;
			}
			//up right
			else if (posX > (_screenWidth - SCROLL_DIAGONAL_EDGE) * action->getXScale())
			{
				_scrollMouseX = -scrollSpeed;
				_scrollMouseY /=2;
			}
		}
		//down
		else if (posY > (_screenHeight- SCROLL_BORDER) * action->getYScale())
		{
			_scrollMouseY = -scrollSpeed;
			// if close to left or right edge, also scroll diagonally
			//down left
			if (posX < (SCROLL_DIAGONAL_EDGE * action->getXScale()) && posX >= 0)
			{
				_scrollMouseX = scrollSpeed;
				_scrollMouseY /=2;
			}
			//down right
			else if (posX > (_screenWidth - SCROLL_DIAGONAL_EDGE) * action->getXScale())
			{
				_scrollMouseX = -scrollSpeed;
				_scrollMouseY /=2;
			}
		}
		else if (posY && _scrollMouseX == 0)
		{
			_scrollMouseY = 0;
		}

		if ((_scrollMouseX || _scrollMouseY) && !_scrollMouseTimer->isRunning() && !_scrollKeyTimer->isRunning() && 0==(SDL_GetMouseState(0,0)&SDL_BUTTON(Options::battleDragScrollButton)))
		{
			_scrollMouseTimer->start();
		}
		else if ((!_scrollMouseX && !_scrollMouseY) && _scrollMouseTimer->isRunning())
		{
			_scrollMouseTimer->stop();
		}
	}
}

/**
 * Handles camera keyboard shortcuts.
 * @param action Pointer to an action.
 * @param state State that the action handlers belong to.
 */
void Camera::keyboardPress(Action *action, State *)
{
	if (_map->getCursorType() == CT_NONE)
	{
		return;
	}

	int key = action->getDetails()->key.keysym.sym;
	int scrollSpeed = Options::battleScrollSpeed;
	if (key == Options::keyBattleLeft)
	{
		_scrollKeyX = scrollSpeed;
	}
	else if (key == Options::keyBattleRight)
	{
		_scrollKeyX = -scrollSpeed;
	}
	else if (key == Options::keyBattleUp)
	{
		_scrollKeyY = scrollSpeed;
	}
	else if (key == Options::keyBattleDown)
	{
		_scrollKeyY = -scrollSpeed;
	}

	if ((_scrollKeyX || _scrollKeyY) && !_scrollKeyTimer->isRunning() && !_scrollMouseTimer->isRunning() && 0==(SDL_GetMouseState(0,0)&SDL_BUTTON(Options::battleDragScrollButton)))
	{
		_scrollKeyTimer->start();
	}
	else if ((!_scrollKeyX && !_scrollKeyY) && _scrollKeyTimer->isRunning())
	{
		_scrollKeyTimer->stop();
	}
}

/**
 * Handles camera keyboard shortcuts.
 * @param action Pointer to an action.
 * @param state State that the action handlers belong to.
 */
void Camera::keyboardRelease(Action *action, State *)
{
	if (_map->getCursorType() == CT_NONE)
	{
		return;
	}

	int key = action->getDetails()->key.keysym.sym;
	if (key == Options::keyBattleLeft)
	{
		_scrollKeyX = 0;
	}
	else if (key == Options::keyBattleRight)
	{
		_scrollKeyX = 0;
	}
	else if (key == Options::keyBattleUp)
	{
		_scrollKeyY = 0;
	}
	else if (key == Options::keyBattleDown)
	{
		_scrollKeyY = 0;
	}

	if ((_scrollKeyX || _scrollKeyY) && !_scrollKeyTimer->isRunning() && !_scrollMouseTimer->isRunning() && 0==(SDL_GetMouseState(0,0)&SDL_BUTTON(Options::battleDragScrollButton)))
	{
		_scrollKeyTimer->start();
	}
	else if ((!_scrollKeyX && !_scrollKeyY) && _scrollKeyTimer->isRunning())
	{
		_scrollKeyTimer->stop();
	}
}

/**
 * Handles mouse-scrolling.
 */
void Camera::scrollMouse()
{
	scrollXY(_scrollMouseX, _scrollMouseY, true);
}

/**
 * Handles keyboard-scrolling.
 */
void Camera::scrollKey()
{
	scrollXY(_scrollKeyX, _scrollKeyY, true);
}

/**
 * Handles scrolling with given deviation.
 * @param x X deviation.
 * @param y Y deviation.
 * @param redraw Redraw map or not.
 */
void Camera::scrollXY(int x, int y, bool redraw)
{
	_mapOffset.x += x;
	_mapOffset.y += y;

	do
	{
		convertScreenToMap((_screenWidth / 2), (_visibleMapHeight / 2), &_center.x, &_center.y);

		// Handling map bounds...
		// Ok, this is a prototype, it should be optimized.
		// Actually this should be calculated instead of slow-approximation.
		if (_center.x < 0)             { _mapOffset.x -= 1; _mapOffset.y -= 1; continue; }
		if (_center.x > _mapsize_x -1) { _mapOffset.x += 1; _mapOffset.y += 1; continue; }
		if (_center.y < 0)             { _mapOffset.x += 1; _mapOffset.y -= 1; continue; }
		if (_center.y > _mapsize_y -1) { _mapOffset.x -= 1; _mapOffset.y += 1; continue; }
		break;
	}
	while (true);

	_map->refreshSelectorPosition();
	if (redraw) _map->invalidate();
}


/**
 * Handles jumping with given deviation.
 * @param x X deviation.
 * @param y Y deviation.
 */
void Camera::jumpXY(int x, int y)
{
	_mapOffset.x += x;
	_mapOffset.y += y;
	convertScreenToMap((_screenWidth / 2), (_visibleMapHeight / 2), &_center.x, &_center.y);
}


/**
 * Goes one level up.
 */
void Camera::up()
{
	if (_mapOffset.z < _mapsize_z - 1)
	{
		_mapOffset.z++;
		_mapOffset.y += _spriteHeight * 3 / 5;
		_map->draw();
	}
}

/**
 * Goes one level down.
 */
void Camera::down()
{
	if (_mapOffset.z > 0)
	{
		_mapOffset.z--;
		_mapOffset.y -= _spriteHeight * 3 / 5;
		_map->draw();
	}
}

/**
 * Sets the view level.
 * @param viewlevel New view level.
 */
void Camera::setViewLevel(int viewlevel)
{
	_mapOffset.z = Clamp(viewlevel, 0, _mapsize_z - 1);
	_map->draw();
}


/**
 * Centers map on a certain position.
 * @param mapPos Position to center on.
 * @param redraw Redraw map or not.
 */
void Camera::centerOnPosition(Position mapPos, bool redraw)
{
	Position screenPos;
	_center = mapPos;
	_center.x = Clamp(_center.x, -1, _mapsize_x);
	_center.y = Clamp(_center.y, -1, _mapsize_y);
	convertMapToScreen(_center, &screenPos);

	_mapOffset.x = -(screenPos.x - (_screenWidth / 2));
	_mapOffset.y = -(screenPos.y - (_visibleMapHeight / 2));

	_mapOffset.z = _center.z;
	if (redraw) _map->draw();
}

/**
 * Gets map's center position.
 * @return Map's center position.
 */
Position Camera::getCenterPosition()
{
	_center.z = _mapOffset.z;
	return _center;
}

/**
 * Converts screen coordinates to map coordinates.
 * @param screenX Screen x position.
 * @param screenY Screen y position.
 * @param mapX Map x position.
 * @param mapY Map y position.
 */
void Camera::convertScreenToMap(int screenX, int screenY, int *mapX, int *mapY) const
{
	// add half a tileheight to the mouseposition per layer we are above the floor
	screenY += (-_spriteWidth/2) + (_mapOffset.z) * ((_spriteHeight + _spriteWidth / 4) / 2);

	// calculate the actual x/y pixelposition on a diamond shaped map
	// taking the view offset into account
	*mapY = - screenX + _mapOffset.x + 2 * screenY - 2 * _mapOffset.y;
	*mapX = screenY - _mapOffset.y - *mapY / 4 - (_spriteWidth/4);

	// to get the row&col itself, divide by the size of a tile
	*mapX /= (_spriteWidth / 4);
	*mapY /= _spriteWidth;

	*mapX = Clamp(*mapX, -1, _mapsize_x);
	*mapY = Clamp(*mapY, -1, _mapsize_y);
}

/**
 * Converts map coordinates X,Y,Z to screen positions X, Y.
 * @param mapPos X,Y,Z coordinates on the map.
 * @param screenPos Screen position.
 */
void Camera::convertMapToScreen(Position mapPos, Position *screenPos) const
{
	screenPos->z = 0; // not used
	screenPos->x = mapPos.x * (_spriteWidth / 2) - mapPos.y * (_spriteWidth / 2);
	screenPos->y = mapPos.x * (_spriteWidth / 4) + mapPos.y * (_spriteWidth / 4) - mapPos.z * ((_spriteHeight + _spriteWidth / 4) / 2);
}

/**
 * Converts voxel coordinates X,Y,Z to screen positions X, Y.
 * @param voxelPos X,Y,Z coordinates of the voxel.
 * @param screenPos Screen position.
 */
void Camera::convertVoxelToScreen(Position voxelPos, Position *screenPos) const
{
	Position mapPosition = Position(voxelPos.x / 16, voxelPos.y / 16, voxelPos.z / 24);
	convertMapToScreen(mapPosition, screenPos);
	double dx = voxelPos.x - (mapPosition.x * 16);
	double dy = voxelPos.y - (mapPosition.y * 16);
	double dz = voxelPos.z - (mapPosition.z * 24);
	screenPos->x += (int)(dx - dy) + (_spriteWidth/2);
	screenPos->y += (int)(((_spriteHeight / 2.0)) + (dx / 2.0) + (dy / 2.0) - dz);
	screenPos->x += _mapOffset.x;
	screenPos->y += _mapOffset.y;
}

/**
 * Gets the displayed level.
 * @return The displayed layer.
 */
int Camera::getViewLevel() const
{
	return _mapOffset.z;
}

/**
 * Gets the map size x.
 * @return The map size x.
 */
int Camera::getMapSizeX() const
{
	return _mapsize_x;
}

/**
 * Gets the map size y.
 * @return The map size y.
 */
int Camera::getMapSizeY() const
{
	return _mapsize_y;
}

/**
 * Gets the map offset.
 * @return The map offset.
 */
Position Camera::getMapOffset() const
{
	return _mapOffset;
}

/**
 * Sets the map offset.
 * @param pos The map offset.
 */
void Camera::setMapOffset(const Position& pos)
{
	_mapOffset = pos;
}

/**
 * Toggles showing all map layers.
 * @return New layer setting.
 */
int Camera::toggleShowAllLayers()
{
	_showAllLayers = !_showAllLayers;
	return _showAllLayers?2:1;
}

/**
 * Checks if the camera is showing all map layers.
 * @return Current layer setting.
 */
bool Camera::getShowAllLayers() const
{
	return _showAllLayers;
}

/**
 * Checks if map coordinates X,Y,Z are on screen.
 * @param mapPos Coordinates to check.
 * @param unitWalking True to offset coordinates for a unit walking.
 * @param unitSize size of unit (0 - single, 1 - 2x2, etc, used for walking only
 * @param boundary True if it's for caching calculation
 * @return True if the map coordinates are on screen.
 */
bool Camera::isOnScreen(Position mapPos, const bool unitWalking, const int unitSize, const bool boundary) const
{
	Position screenPos;
	convertMapToScreen(mapPos, &screenPos);
	int posx = _spriteWidth/2, posy = _spriteHeight - _spriteWidth/4;
	int sizex = _spriteWidth/2, sizey = _spriteHeight/2;
	if (unitSize > 0)
	{
		posy -= _spriteWidth /4;
		sizex = _spriteWidth*unitSize;
		sizey = _spriteWidth*unitSize/2;
	}
	screenPos.x += _mapOffset.x + posx;
	screenPos.y += _mapOffset.y + posy;
	if (unitWalking)
	{
/* pretty hardcoded hack to handle overlapping by icons
(they are always in the center at the bottom of the screen)
Free positioned icons would require more complex workaround.
__________
|________|
||      ||
|| ____ ||
||_|XX|_||
|________|
 */
		if (boundary) //to make sprite updates even being slightly outside of screen
		{
			sizex += _spriteWidth;
			sizey += _spriteWidth/2;
		}
		if ( screenPos.x < 0 - sizex
			|| screenPos.x >= _screenWidth + sizex
			|| screenPos.y < 0 - sizey
			|| screenPos.y >= _screenHeight + sizey ) return false; //totally outside
		int side = ( _screenWidth - _map->getIconWidth() ) / 2;
		if ( (screenPos.y < (_screenHeight - _map->getIconHeight()) + sizey) ) return true; //above icons
		if ( (side > 1) && ( (screenPos.x < side + sizex) || (screenPos.x >= (_screenWidth - side - sizex)) ) ) return true; //at sides (if there are any)
		return false;
	}
	else
	{
		return screenPos.x >= 0
			&& screenPos.x <= _screenWidth - 10
			&& screenPos.y >= 0
			&& screenPos.y <= _screenHeight - 10;
	}
}

/**
 * Resizes the viewable window of the camera.
 */
void Camera::resize()
{
	_screenWidth = _map->getWidth();
	_screenHeight = _map->getHeight();
	_visibleMapHeight = _map->getHeight() - _map->getIconHeight();
}

void Camera::stopMouseScrolling()
{
	_scrollMouseTimer->stop();
}

}
