/* ScummVM - Graphic Adventure Engine
 *
 * ScummVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * 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/>.
 *
 */

#include "common/events.h"
#include "common/system.h"
#include "common/textconsole.h"
#include "graphics/cursorman.h"

#include "parallaction/exec.h"
#include "parallaction/input.h"
#include "parallaction/parallaction.h"
#include "parallaction/debug.h"

namespace Parallaction {

#define MOUSEARROW_WIDTH_NS		16
#define MOUSEARROW_HEIGHT_NS		16

#define MOUSECOMBO_WIDTH_NS		32	// sizes for cursor + selected inventory item
#define MOUSECOMBO_HEIGHT_NS		32

struct MouseComboProperties {
	int	_xOffset;
	int	_yOffset;
	int	_width;
	int	_height;
};
/*
// TODO: improve NS's handling of normal cursor before merging cursor code.
MouseComboProperties	_mouseComboProps_NS = {
	7,	// combo x offset (the icon from the inventory will be rendered from here)
	7,	// combo y offset (ditto)
	32,	// combo (arrow + icon) width
	32	// combo (arrow + icon) height
};
*/
MouseComboProperties	_mouseComboProps_BR = {
	8,	// combo x offset (the icon from the inventory will be rendered from here)
	8,	// combo y offset (ditto)
	68,	// combo (arrow + icon) width
	68	// combo (arrow + icon) height
};

Input::Input(Parallaction *vm) : _vm(vm) {
	_gameType = _vm->getGameType();
	_transCurrentHoverItem = 0;
	_hasDelayedAction = false;  // actived when the character needs to move before taking an action
	_mouseState = MOUSE_DISABLED;
	_activeItem._index = 0;
	_activeItem._id = 0;
	_mouseButtons = 0;
	_delayedActionZone.reset();

	_inputMode = 0;
	_hasKeyPressEvent = false;

	_dinoCursor = nullptr;
	_dougCursor = nullptr;
	_donnaCursor = nullptr;
	_comboArrow = nullptr;
	_mouseArrow = nullptr;

	initCursors();
}

Input::~Input() {
	if (_gameType == GType_Nippon) {
		delete _mouseArrow;
	}

	delete _comboArrow;
	delete _dinoCursor;
	delete _dougCursor;
	delete _donnaCursor;
}

// FIXME: the engine has 3 event loops. The following routine hosts the main one,
// and it's called from 8 different places in the code. There exist 2 more specialised
// loops which could possibly be merged into this one with some effort in changing
// caller code, i.e. adding condition checks.
//
void Input::readInput() {
	bool updateMousePos = false;

	Common::Event e;

	_mouseButtons = kMouseNone;
	_hasKeyPressEvent = false;

	Common::EventManager *eventMan = _vm->_system->getEventManager();
	while (eventMan->pollEvent(e)) {
		updateMousePos = true;

		switch (e.type) {
		case Common::EVENT_KEYDOWN:
			_hasKeyPressEvent = true;
			_keyPressed = e.kbd;

			updateMousePos = false;
			break;

		case Common::EVENT_LBUTTONDOWN:
			_mouseButtons = kMouseLeftDown;
			break;

		case Common::EVENT_LBUTTONUP:
			_mouseButtons = kMouseLeftUp;
			break;

		case Common::EVENT_RBUTTONDOWN:
			_mouseButtons = kMouseRightDown;
			break;

		case Common::EVENT_RBUTTONUP:
			_mouseButtons = kMouseRightUp;
			break;

		case Common::EVENT_RETURN_TO_LAUNCHER:
		case Common::EVENT_QUIT:
			return;

		default:
			break;

		}

	}

	if (updateMousePos) {
		setCursorPos(e.mouse);
	}
}

bool Input::getLastKeyDown(uint16 &ascii) {
	ascii = _keyPressed.ascii;
	return (_hasKeyPressEvent);
}

// FIXME: see comment for readInput()
void Input::waitForButtonEvent(uint32 buttonEventMask, int32 timeout) {

	if (buttonEventMask == kMouseNone) {
		_mouseButtons = kMouseNone;	// don't wait on nothing
		return;
	}

	const int32 LOOP_RESOLUTION = 30;
	if (timeout <= 0) {
		do {
			readInput();
			_vm->_system->delayMillis(LOOP_RESOLUTION);
		} while ((_mouseButtons & buttonEventMask) == 0);
	} else {
		do {
			readInput();
			_vm->_system->delayMillis(LOOP_RESOLUTION);
			timeout -= LOOP_RESOLUTION;
		} while ((timeout > 0) && (_mouseButtons & buttonEventMask) == 0);
	}

}


int Input::updateGameInput() {

	int event = kEvNone;

	if (!isMouseEnabled() ||
		(g_engineFlags & kEngineBlockInput) ||
		(g_engineFlags & kEngineWalking) ||
		(g_engineFlags & kEngineChangeLocation)) {

		debugC(3, kDebugInput, "updateGameInput: input flags (mouse: %i, block: %i, walking: %i, changeloc: %i)",
			isMouseEnabled(),
			(g_engineFlags & kEngineBlockInput) == 0,
			(g_engineFlags & kEngineWalking) == 0,
			(g_engineFlags & kEngineChangeLocation) == 0
		);

		return event;
	}

	if (_gameType == GType_Nippon) {
		if (_hasKeyPressEvent && (_vm->getFeatures() & GF_DEMO) == 0) {
			if (_keyPressed.keycode == Common::KEYCODE_l) event = kEvLoadGame;
			if (_keyPressed.keycode == Common::KEYCODE_s) event = kEvSaveGame;
		}
	} else
	if (_gameType == GType_BRA) {
		if (_hasKeyPressEvent && (_vm->getFeatures() & GF_DEMO) == 0) {
			if (_keyPressed.keycode == Common::KEYCODE_F5) event = kEvIngameMenu;
		}
	} else {
		error("unsupported gametype in updateGameInput");
	}


	if (event == kEvNone) {
		translateGameInput();
	}

	return event;
}


int Input::updateInput() {

	int oldMode = _inputMode;

	int event = kEvNone;
	readInput();

	switch (_inputMode) {
	case kInputModeGame:
		event = updateGameInput();
		break;

	case kInputModeInventory:
		updateInventoryInput();
		break;

	default:
		break;
	}

	// when mode changes, then consider any input consumed
	// for the current frame
	if (oldMode != _inputMode) {
		_mouseButtons = kEvNone;
		_hasKeyPressEvent = false;
	}

	return event;
}

void Input::trackMouse(ZonePtr z) {
	if ((z != _hoverZone) && (_hoverZone)) {
		stopHovering();
		return;
	}

	if (!z) {
		return;
	}

	if ((!_hoverZone) && ((z->_flags & kFlagsNoName) == 0)) {
		_hoverZone = z;
		_vm->_gfx->showFloatingLabel(_hoverZone->_label);
		return;
	}
}

void Input::stopHovering() {
	_hoverZone.reset();
	_vm->_gfx->hideFloatingLabel();
}

void Input::takeAction(ZonePtr z) {
	stopHovering();
	_vm->pauseJobs();
	_vm->runZone(z);
	_vm->resumeJobs();
}

void Input::walkTo(const Common::Point &dest) {
	stopHovering();
	setArrowCursor();
	_vm->scheduleWalk(dest.x, dest.y, true);
}

bool Input::translateGameInput() {

	if (g_engineFlags & kEnginePauseJobs) {
		return false;
	}

	if (_hasDelayedAction) {
		// if walking is over, then take programmed action
		takeAction(_delayedActionZone);
		_hasDelayedAction = false;
		_delayedActionZone.reset();
		return true;
	}

	if (_mouseButtons == kMouseRightDown) {
		// right button down shows inventory
		enterInventoryMode();
		return true;
	}

	Common::Point mousePos;
	getAbsoluteCursorPos(mousePos);
	// test if mouse is hovering on an interactive zone for the currently selected inventory item
	ZonePtr z = _vm->hitZone(_activeItem._id, mousePos.x, mousePos.y);

	if (((_mouseButtons == kMouseLeftUp) && (_activeItem._id == 0) && ((g_engineFlags & kEngineWalking) == 0)) && ((!z) || (ACTIONTYPE(z) != kZoneCommand))) {
		walkTo(mousePos);
		return true;
	}

	trackMouse(z);
	if (!z) {
		return true;
	}

	if ((_mouseButtons == kMouseLeftUp) && ((_activeItem._id != 0) || (ACTIONTYPE(z) == kZoneCommand))) {

		bool noWalk = z->_flags & kFlagsNoWalk;	// check the explicit no-walk flag
		if (_gameType == GType_BRA) {
			// action performed on object marked for self-use do not need walk in BRA
			noWalk |= ((z->_flags & kFlagsYourself) != 0);
		}

		if (noWalk) {
			takeAction(z);
		} else {
			// action delayed: if Zone defined a moveto position the character is programmed to move there,
			// else it will move to the mouse position
			_delayedActionZone = z;
			_hasDelayedAction = true;
			if (z->_moveTo.y != 0) {
				mousePos = z->_moveTo;
			}

			walkTo(mousePos);
		}

		_vm->beep();
		setArrowCursor();
		return true;
	}

	return true;
}


void Input::enterInventoryMode() {
	Common::Point mousePos;
	getAbsoluteCursorPos(mousePos);
	bool hitCharacter = _vm->hitZone(kZoneYou, mousePos.x, mousePos.y);

	if (hitCharacter) {
		if (_activeItem._id != 0) {
			_activeItem._index = (_activeItem._id >> 16) & 0xFFFF;
			g_engineFlags |= kEngineDragging;
		} else {
			setArrowCursor();
		}
	}

	stopHovering();
	_vm->pauseJobs();
	_vm->openInventory();

	_transCurrentHoverItem = -1;

	_inputMode = kInputModeInventory;
}

void Input::exitInventoryMode() {
	// right up hides inventory
	Common::Point mousePos;
	getCursorPos(mousePos);

	int pos = _vm->getHoverInventoryItem(mousePos.x, mousePos.y);
	_vm->highlightInventoryItem(-1);			// disable

	if ((g_engineFlags & kEngineDragging)) {

		g_engineFlags &= ~kEngineDragging;
		ZonePtr z = _vm->hitZone(kZoneMerge, _activeItem._index, _vm->getInventoryItemIndex(pos));

		if (z) {
			_vm->dropItem(z->u._mergeObj1);
			_vm->dropItem(z->u._mergeObj2);
			_vm->addInventoryItem(z->u._mergeObj3);
			_vm->_cmdExec->run(z->_commands);	// commands might set a new _inputMode
		}

	}

	_vm->closeInventory();
	if (pos == -1) {
		setArrowCursor();
	} else {
		const InventoryItem *item = _vm->getInventoryItem(pos);
		if (item->_index != 0) {
			_activeItem._id = item->_id;
			setInventoryCursor(item->_index);
		}
	}
	_vm->resumeJobs();

	// in case the input mode was not changed by the code above (especially by the commands
	// executed in case of a merge), then assume we are going back to game mode
	if (_inputMode == kInputModeInventory) {
		_inputMode = kInputModeGame;
	}
}

bool Input::updateInventoryInput() {
	if (_mouseButtons == kMouseRightUp) {
		exitInventoryMode();
		return true;
	}

	Common::Point mousePos;
	getCursorPos(mousePos);

	int16 _si = _vm->getHoverInventoryItem(mousePos.x, mousePos.y);
	if (_si != _transCurrentHoverItem) {
		_transCurrentHoverItem = _si;
		_vm->highlightInventoryItem(_si);						// enable
	}

	return true;

}

void Input::setMouseState(MouseTriState state) {
	assert(state == MOUSE_ENABLED_SHOW || state == MOUSE_ENABLED_HIDE || state == MOUSE_DISABLED);
	_mouseState = state;

	switch (_mouseState) {
	case MOUSE_ENABLED_HIDE:
	case MOUSE_DISABLED:
		CursorMan.showMouse(false);
		break;

	case MOUSE_ENABLED_SHOW:
		CursorMan.showMouse(true);
		break;

	default:
		break;
	}
}

MouseTriState Input::getMouseState() {
	return _mouseState;
}

bool Input::isMouseEnabled() {
	return (_mouseState == MOUSE_ENABLED_SHOW) || (_mouseState == MOUSE_ENABLED_HIDE);
}

void Input::getAbsoluteCursorPos(Common::Point& p) const {
	_vm->_gfx->getScrollPos(p);
	p.x += _mousePos.x;
	p.y += _mousePos.y;
}


void Input::initCursors() {

	_dinoCursor = _donnaCursor = _dougCursor = nullptr;

	switch (_gameType) {
	case GType_Nippon:
		_comboArrow = _vm->_disk->loadPointer("pointer");
		_mouseArrow = new Cnv(1, MOUSEARROW_WIDTH_NS, MOUSEARROW_HEIGHT_NS, _resMouseArrow_NS, false);
		break;

	case GType_BRA:
		if (_vm->getPlatform() == Common::kPlatformDOS) {
			_dinoCursor = _vm->_disk->loadPointer("pointer1");
			_dougCursor = _vm->_disk->loadPointer("pointer2");
			_donnaCursor = _vm->_disk->loadPointer("pointer3");

			Graphics::Surface *surf = new Graphics::Surface;
			surf->create(_mouseComboProps_BR._width, _mouseComboProps_BR._height, Graphics::PixelFormat::createFormatCLUT8());
			_comboArrow = new SurfaceToFrames(surf);
		} else {
			// TODO: Where are the Amiga cursors?
			Graphics::Surface *surf1 = new Graphics::Surface;
			surf1->create(_mouseComboProps_BR._width, _mouseComboProps_BR._height, Graphics::PixelFormat::createFormatCLUT8());
			_comboArrow = new SurfaceToFrames(surf1);

			// TODO: scale mouse cursor (see staticres.cpp)
			Graphics::Surface *surf2 = new Graphics::Surface;
			surf2->create(32, 16, Graphics::PixelFormat::createFormatCLUT8());
			memcpy(surf2->getPixels(), _resMouseArrow_BR_Amiga, 32*16);
			_mouseArrow = new SurfaceToFrames(surf2);
		}
		break;

	default:
		warning("Input::initCursors: unknown gametype");
	}

}

void Input::setArrowCursor() {

	switch (_gameType) {
	case GType_Nippon:
		debugC(1, kDebugInput, "setting mouse cursor to arrow");
		// this stuff is needed to avoid artifacts with labels and selected items when switching cursors
		stopHovering();
		_activeItem._id = 0;
		CursorMan.replaceCursor(_mouseArrow->getData(0), MOUSEARROW_WIDTH_NS, MOUSEARROW_HEIGHT_NS, 0, 0, 0);
		break;

	case GType_BRA: {
		Common::Rect r;
		_mouseArrow->getRect(0, r);
		CursorMan.replaceCursor(_mouseArrow->getData(0), r.width(), r.height(), 0, 0, 0);
		CursorMan.showMouse(true);
		_activeItem._id = 0;
		break;
	}

	default:
		warning("Input::setArrowCursor: unknown gametype");
	}

}

void Input::setInventoryCursor(ItemName name) {
	assert(name > 0);

	switch (_gameType) {
	case GType_Nippon: {
		byte *v8 = _comboArrow->getData(0);
		// FIXME: destination offseting is not clear
		_vm->_inventoryRenderer->drawItem(name, v8 + 7 * MOUSECOMBO_WIDTH_NS + 7, MOUSECOMBO_WIDTH_NS);
		CursorMan.replaceCursor(v8, MOUSECOMBO_WIDTH_NS, MOUSECOMBO_HEIGHT_NS, 0, 0, 0);
		break;
	}

	case GType_BRA: {
		byte *src = _mouseArrow->getData(0);
		byte *dst = _comboArrow->getData(0);
		// FIXME: destination offseting is not clear
		Common::Rect srcRect, dstRect;
		_mouseArrow->getRect(0, srcRect);
		_comboArrow->getRect(0, dstRect);
		for (uint y = 0; y < (uint)srcRect.height(); y++)
			memcpy(dst + y * dstRect.width(), src + y * srcRect.width(), srcRect.width());
		_vm->_inventoryRenderer->drawItem(name, dst + _mouseComboProps_BR._yOffset * _mouseComboProps_BR._width + _mouseComboProps_BR._xOffset, _mouseComboProps_BR._width);
		CursorMan.replaceCursor(dst, _mouseComboProps_BR._width, _mouseComboProps_BR._height, 0, 0, 0);
		break;
	}

	default:
		warning("Input::setInventoryCursor: unknown gametype");
	}

}

void Input::setMenuPointer() {
	switch (_gameType) {
	case GType_Nippon: {
		error("Input::setMenuPointer not supported for Nippon Safes");
	}

	case GType_BRA: {
		if (_vm->getPlatform() == Common::kPlatformDOS) {
			// Donna's cursor doubles as the cursor used
			// in the startup menu.
			_mouseArrow = _donnaCursor;
			setArrowCursor();
		} else {
			warning("Input::setMenuPointer not yet implemented for Amiga");
		}
		break;
	}

	default:
		warning("Input::setMenuPointer: unknown gametype");
	}
}

void Input::setCharacterPointer(const char *name) {
	switch (_gameType) {
	case GType_Nippon: {
		error("Input::setCharacterPointer not supported for Nippon Safes");
	}

	case GType_BRA: {
		if (_vm->getPlatform() == Common::kPlatformDOS) {
			if (!scumm_stricmp(name, "dino")) {
				_mouseArrow = _dinoCursor;
			} else if (!scumm_stricmp(name, "donna")) {
				_mouseArrow = _donnaCursor;
			} else if (!scumm_stricmp(name, "doug")) {
				_mouseArrow = _dougCursor;
			}
			setArrowCursor();
		} else {
			warning("Input::setCharacterPointer not yet implemented for Amiga");
		}
		break;
	}

	default:
		warning("Input::setCharacterPointer: unknown gametype");
	}
}

} // namespace Parallaction
