/* 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 2
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 * Cursor and cursor trails.
 */

#include "tinsel/cursor.h"

#include "tinsel/anim.h"
#include "tinsel/background.h"
#include "tinsel/cursor.h"
#include "tinsel/dw.h"
#include "tinsel/events.h"		// For EventsManager class
#include "tinsel/film.h"
#include "tinsel/graphics.h"
#include "tinsel/handle.h"
#include "tinsel/dialogs.h"
#include "tinsel/multiobj.h"	// multi-part object defintions etc.
#include "tinsel/object.h"
#include "tinsel/pid.h"
#include "tinsel/play.h"
#include "tinsel/sched.h"
#include "tinsel/sysvar.h"
#include "tinsel/text.h"
#include "tinsel/timers.h"		// For ONE_SECOND constant
#include "tinsel/tinlib.h"		// resetidletime()
#include "tinsel/tinsel.h"		// For engine access


namespace Tinsel {

//----------------- LOCAL DEFINES --------------------

#define ITERATION_BASE		FRAC_ONE
#define ITER_ACCELERATION	(10L << (FRAC_BITS - 4))


//----------------- LOCAL GLOBAL DATA --------------------

// FIXME: Avoid non-const global vars

static OBJECT *g_McurObj = NULL;		// Main cursor object
static OBJECT *g_AcurObj = NULL;		// Auxiliary cursor object

static ANIM g_McurAnim = {0,0,0,0,0};		// Main cursor animation structure
static ANIM g_AcurAnim = {0,0,0,0,0};		// Auxiliary cursor animation structure

static bool g_bHiddenCursor = false;		// Set when cursor is hidden
static bool g_bTempNoTrailers = false;	// Set when cursor trails are hidden
static bool g_bTempHide = false;			// Set when cursor is hidden

static bool g_bFrozenCursor = false;	// Set when cursor position is frozen

static frac_t g_IterationSize = 0;

static SCNHANDLE g_hCursorFilm = 0;	// Handle to cursor reel data

static int g_numTrails = 0;
static int g_nextTrail = 0;

static bool g_bWhoa = false;		// Set by DropCursor() at the end of a scene
				// - causes cursor processes to do nothing
				// Reset when main cursor has re-initialized

static uint16 g_restart = 0;	// When main cursor has been bWhoa-ed, it waits
							// for this to be set to 0x8000.
							// Main cursor sets all the bits after a re-start
							// - each cursor trail examines it's own bit
							// to trigger a trail restart.

static short g_ACoX = 0, g_ACoY = 0;	// Auxillary cursor image's animation offsets



#define MAX_TRAILERS	10

static struct {

	ANIM	trailAnim;	// Animation structure
	OBJECT *trailObj;	// This trailer's object

} g_ntrailData [MAX_TRAILERS];

static int g_lastCursorX = 0, g_lastCursorY = 0;


//----------------- FORWARD REFERENCES --------------------

static void DoCursorMove();

/**
 * Initialize and insert a cursor trail object, set its Z-pos, and hide
 * it. Also initialize its animation script.
 */
static void InitCurTrailObj(int i, int x, int y) {
	const FREEL *pfr;		// pointer to reel
	IMAGE *pim;		// pointer to image
	const MULTI_INIT *pmi;		// MULTI_INIT structure

	const FILM *pfilm;

	if (!g_numTrails)
		return;

	// Get rid of old object
	if (g_ntrailData[i].trailObj != NULL)
		MultiDeleteObject(GetPlayfieldList(FIELD_STATUS), g_ntrailData[i].trailObj);

	pim = GetImageFromFilm(g_hCursorFilm, i+1, &pfr, &pmi, &pfilm);// Get pointer to image
	assert(BgPal()); // No background palette
	pim->hImgPal = TO_32(BgPal());

	// Initialize and insert the object, set its Z-pos, and hide it
	g_ntrailData[i].trailObj = MultiInitObject(pmi);
	MultiInsertObject(GetPlayfieldList(FIELD_STATUS), g_ntrailData[i].trailObj);
	MultiSetZPosition(g_ntrailData[i].trailObj, Z_CURSORTRAIL);
	MultiSetAniXY(g_ntrailData[i].trailObj, x, y);

	// Initialize the animation script
	InitStepAnimScript(&g_ntrailData[i].trailAnim, g_ntrailData[i].trailObj, FROM_32(pfr->script), ONE_SECOND / FROM_32(pfilm->frate));
	StepAnimScript(&g_ntrailData[i].trailAnim);
}

/**
 * Get the cursor position from the mouse driver.
 */
static bool GetDriverPosition(int *x, int *y) {
	Common::Point ptMouse = _vm->getMousePosition();
	*x = ptMouse.x;
	*y = ptMouse.y;

	return(*x >= 0 && *x <= SCREEN_WIDTH - 1 &&
		*y >= 0 && *y <= SCREEN_HEIGHT - 1);
}

/**
 * Move the cursor relative to current position.
 */
void AdjustCursorXY(int deltaX, int deltaY) {
	int x, y;

	if (deltaX || deltaY) {
		if (GetDriverPosition(&x, &y))
			_vm->setMousePosition(Common::Point(x + deltaX, y + deltaY));
	}
	DoCursorMove();
}

/**
 * Move the cursor to an absolute position.
 */
void SetCursorXY(int newx, int newy) {
	int	x, y;
	int	Loffset, Toffset;	// Screen offset

	PlayfieldGetPos(FIELD_WORLD, &Loffset, &Toffset);
	newx -= Loffset;
	newy -= Toffset;

	if (GetDriverPosition(&x, &y))
		_vm->setMousePosition(Common::Point(newx, newy));
	DoCursorMove();
}

/**
 * Move the cursor to a screen position.
 */
void SetCursorScreenXY(int newx, int newy) {
	int	x, y;

	if (GetDriverPosition(&x, &y))
		_vm->setMousePosition(Common::Point(newx, newy));
	DoCursorMove();
}

/**
 * Called by the world and his brother.
 * Returns the cursor's animation position in (x,y).
 * Returns false if there is no cursor object.
 */
bool GetCursorXYNoWait(int *x, int *y, bool absolute) {
	if (g_McurObj == NULL) {
		*x = *y = 0;
		return false;
	}

	GetAniPosition(g_McurObj, x, y);

	if (absolute) {
		int	Loffset, Toffset;	// Screen offset
		PlayfieldGetPos(FIELD_WORLD, &Loffset, &Toffset);
		*x += Loffset;
		*y += Toffset;
	}

	return true;
}

/**
 * Called by the world and his brother.
 * Returns the cursor's animation position.
 * If called while there is no cursor object, the calling process ends
 * up waiting until there is.
 */
void GetCursorXY(int *x, int *y, bool absolute) {
	//while (McurObj == NULL)
	//	ProcessSleepSelf();
	assert(g_McurObj);
	GetCursorXYNoWait(x, y, absolute);
}

/**
 * Re-initialize the main cursor to use the main cursor reel.
 * Called from TINLIB.C to restore cursor after hiding it.
 * Called from INVENTRY.C to restore cursor after customising it.
 */
void RestoreMainCursor() {
	const FILM *pfilm;

	if (g_McurObj != NULL) {
		pfilm = (const FILM *)LockMem(g_hCursorFilm);

		InitStepAnimScript(&g_McurAnim, g_McurObj, FROM_32(pfilm->reels->script), ONE_SECOND / FROM_32(pfilm->frate));
		StepAnimScript(&g_McurAnim);
	}
	g_bHiddenCursor = false;
	g_bFrozenCursor = false;
}

/**
 * Called from INVENTRY.C to customise the main cursor.
 */
void SetTempCursor(SCNHANDLE pScript) {
	if (g_McurObj != NULL)
		InitStepAnimScript(&g_McurAnim, g_McurObj, pScript, 2);
}

/**
 * Hide the cursor.
 */
void DwHideCursor() {
	int i;

	g_bHiddenCursor = true;

	if (g_McurObj)
		MultiHideObject(g_McurObj);
	if (g_AcurObj)
		MultiHideObject(g_AcurObj);

	for (i = 0; i < g_numTrails; i++) {
		if (g_ntrailData[i].trailObj != NULL) {
			MultiDeleteObject(GetPlayfieldList(FIELD_STATUS), g_ntrailData[i].trailObj);
			g_ntrailData[i].trailObj = NULL;
		}
	}
}

/**
 * Unhide the cursor.
 */
void UnHideCursor() {
	g_bHiddenCursor = false;
}

/**
 * Freeze the cursor.
 */
void FreezeCursor() {
	g_bFrozenCursor = true;
}

/**
 * Freeze the cursor, or not.
 */
void DoFreezeCursor(bool bFreeze) {
	g_bFrozenCursor = bFreeze;
}

/**
 * HideCursorTrails
 */
void HideCursorTrails() {
	int i;

	g_bTempNoTrailers = true;

	for (i = 0; i < g_numTrails; i++)	{
		if (g_ntrailData[i].trailObj != NULL) {
			MultiDeleteObject(GetPlayfieldList(FIELD_STATUS), g_ntrailData[i].trailObj);
			g_ntrailData[i].trailObj = NULL;
		}
	}
}

/**
 * UnHideCursorTrails
 */
void UnHideCursorTrails() {
	g_bTempNoTrailers = false;
}

/**
 * Get pointer to image from a film reel. And the rest.
 */
IMAGE *GetImageFromReel(const FREEL *pfr, const MULTI_INIT **ppmi) {
	const MULTI_INIT *pmi;
	const FRAME *pFrame;

	pmi = (const MULTI_INIT *)LockMem(FROM_32(pfr->mobj));
	if (ppmi)
		*ppmi = pmi;

	pFrame = (const FRAME *)LockMem(FROM_32(pmi->hMulFrame));

	// get pointer to image
	return (IMAGE *)LockMem(READ_32(pFrame));
}

/**
 * Get pointer to image from a film. And the rest.
 */
IMAGE *GetImageFromFilm(SCNHANDLE hFilm, int reel, const FREEL **ppfr, const MULTI_INIT **ppmi, const FILM **ppfilm) {
	const FILM *pfilm;
	const FREEL *pfr;

	pfilm = (const FILM *)LockMem(hFilm);
	if (ppfilm)
		*ppfilm = pfilm;

	pfr = &pfilm->reels[reel];
	if (ppfr)
		*ppfr = pfr;

	return GetImageFromReel(pfr, ppmi);
}

/**
 * Delete auxillary cursor. Restore animation offsets in the image.
 */
void DelAuxCursor() {
	if (g_AcurObj != NULL) {
		MultiDeleteObject(GetPlayfieldList(FIELD_STATUS), g_AcurObj);
		g_AcurObj = NULL;
	}
}

/**
 * Set auxillary cursor.
 * Save animation offsets from the image if required.
 */
void SetAuxCursor(SCNHANDLE hFilm) {
	IMAGE *pim;		// Pointer to auxillary cursor's image
	const FREEL *pfr;
	const MULTI_INIT *pmi;
	const FILM *pfilm;
	int	x, y;		// Cursor position

	DelAuxCursor();		// Get rid of previous

	// WORKAROUND: There's no palette when loading a DW1 savegame with a held item, so exit if so
	if (!BgPal())
		return;

	GetCursorXY(&x, &y, false);	// Note: also waits for cursor to appear

	pim = GetImageFromFilm(hFilm, 0, &pfr, &pmi, &pfilm);// Get pointer to image
	assert(BgPal()); // no background palette
	pim->hImgPal = TO_32(BgPal());			// Poke in the background palette

	g_ACoX = (short)(FROM_16(pim->imgWidth)/2 - ((int16) FROM_16(pim->anioffX)));
	g_ACoY = (short)((FROM_16(pim->imgHeight) & ~C16_FLAG_MASK)/2 -
		((int16) FROM_16(pim->anioffY)));

	// Initialize and insert the auxillary cursor object
	g_AcurObj = MultiInitObject(pmi);
	MultiInsertObject(GetPlayfieldList(FIELD_STATUS), g_AcurObj);

	// Initialize the animation and set its position
	InitStepAnimScript(&g_AcurAnim, g_AcurObj, FROM_32(pfr->script), ONE_SECOND / FROM_32(pfilm->frate));
	MultiSetAniXY(g_AcurObj, x - g_ACoX, y - g_ACoY);
	MultiSetZPosition(g_AcurObj, Z_ACURSOR);

	if (g_bHiddenCursor)
		MultiHideObject(g_AcurObj);
}

/**
 * MoveCursor
 */
static void DoCursorMove() {
	int	startX, startY;
	Common::Point ptMouse;
	frac_t newX, newY;
	unsigned dir;

	// get cursors start animation position
	GetCursorXYNoWait(&startX, &startY, false);

	// get mouse drivers current position
	ptMouse = _vm->getMousePosition();

	// convert to fixed point
	newX = intToFrac(ptMouse.x);
	newY = intToFrac(ptMouse.y);

	// modify mouse driver position depending on cursor keys
	dir = _vm->getKeyDirection();
	if (dir != 0) {
		if (dir & MSK_LEFT)
			newX -= g_IterationSize;

		if (dir & MSK_RIGHT)
			newX += g_IterationSize;

		if (dir & MSK_UP)
			newY -= g_IterationSize;

		if (dir & MSK_DOWN)
			newY += g_IterationSize;

		g_IterationSize += ITER_ACCELERATION;

		// set new mouse driver position
		_vm->setMousePosition(Common::Point(fracToInt(newX), fracToInt(newY)));
	} else

		g_IterationSize = ITERATION_BASE;

	// get new mouse driver position - could have been modified
	ptMouse = _vm->getMousePosition();

	if (g_lastCursorX != ptMouse.x || g_lastCursorY != ptMouse.y) {
		resetUserEventTime();

		if (!g_bTempNoTrailers && !g_bHiddenCursor) {
			InitCurTrailObj(g_nextTrail++, g_lastCursorX, g_lastCursorY);
			if (g_nextTrail == g_numTrails)
				g_nextTrail = 0;
		}
	}

	// adjust cursor to new mouse position
	if (g_McurObj)
		MultiSetAniXY(g_McurObj, ptMouse.x, ptMouse.y);
	if (g_AcurObj != NULL)
		MultiSetAniXY(g_AcurObj, ptMouse.x - g_ACoX, ptMouse.y - g_ACoY);

	if (InventoryActive() && g_McurObj) {
		// Notify the inventory
		Xmovement(ptMouse.x - startX);
		Ymovement(ptMouse.y - startY);
	}

	g_lastCursorX = ptMouse.x;
	g_lastCursorY = ptMouse.y;
}

/**
 * Initialize cursor object.
 */
static void InitCurObj() {
	const FILM *pFilm;
	const FREEL *pfr;
	const MULTI_INIT *pmi;
	IMAGE *pim;

	if (TinselV2) {
		pFilm = (const FILM *)LockMem(g_hCursorFilm);
		pfr = (const FREEL *)&pFilm->reels[0];
		pmi = (MULTI_INIT *)LockMem(FROM_32(pfr->mobj));

		PokeInPalette(pmi);
	} else {
		assert(BgPal()); // no background palette

		pim = GetImageFromFilm(g_hCursorFilm, 0, &pfr, &pmi, &pFilm);// Get pointer to image
		pim->hImgPal = TO_32(BgPal());

		g_AcurObj = NULL;		// No auxillary cursor
	}

	g_McurObj = MultiInitObject(pmi);
	MultiInsertObject(GetPlayfieldList(FIELD_STATUS), g_McurObj);

	InitStepAnimScript(&g_McurAnim, g_McurObj, FROM_32(pfr->script), ONE_SECOND / FROM_32(pFilm->frate));
}

/**
 * Initialize the cursor position.
 */
static void InitCurPos() {
	Common::Point ptMouse = _vm->getMousePosition();
	g_lastCursorX = ptMouse.x;
	g_lastCursorY = ptMouse.y;

	MultiSetZPosition(g_McurObj, Z_CURSOR);
	DoCursorMove();
	MultiHideObject(g_McurObj);

	g_IterationSize = ITERATION_BASE;
}

/**
 * CursorStoppedCheck
 */
static void CursorStoppedCheck(CORO_PARAM) {
	// COROUTINE
	CORO_BEGIN_CONTEXT;
	CORO_END_CONTEXT(_ctx);

	CORO_BEGIN_CODE(_ctx);

	// If scene is closing down
	if (g_bWhoa) {
		// ...wait for next scene start-up
		while (g_restart != 0x8000)
			CORO_SLEEP(1);

		// Re-initialize
		InitCurObj();
		InitCurPos();
		InventoryIconCursor(false);	// May be holding something

		// Re-start the cursor trails
		g_restart = (uint16)-1;		// set all bits
		g_bWhoa = false;
	}
	CORO_END_CODE;
}

/**
 * The main cursor process.
 */
void CursorProcess(CORO_PARAM, const void *) {
	// COROUTINE
	CORO_BEGIN_CONTEXT;
	CORO_END_CONTEXT(_ctx);

	CORO_BEGIN_CODE(_ctx);

	while (!g_hCursorFilm || !BgPal())
		CORO_SLEEP(1);

	InitCurObj();
	InitCurPos();
	InventoryIconCursor(false);		// May be holding something

	g_bWhoa = false;
	g_restart = 0;

	while (1) {
		// allow rescheduling
		CORO_SLEEP(1);

		// Stop/start between scenes
		CORO_INVOKE_0(CursorStoppedCheck);

		// Step the animation script(s)
		StepAnimScript(&g_McurAnim);
		if (g_AcurObj != NULL)
			StepAnimScript(&g_AcurAnim);
		for (int i = 0; i < g_numTrails; i++) {
			if (g_ntrailData[i].trailObj != NULL) {
				if (StepAnimScript(&g_ntrailData[i].trailAnim) == ScriptFinished) {
					MultiDeleteObject(GetPlayfieldList(FIELD_STATUS), g_ntrailData[i].trailObj);
					g_ntrailData[i].trailObj = NULL;
				}
			}
		}

		// Move the cursor as appropriate
		if (!g_bFrozenCursor)
			DoCursorMove();

		// If the cursor should be hidden...
		if (g_bHiddenCursor || g_bTempHide) {
			// ...hide the cursor object(s)
			MultiHideObject(g_McurObj);
			if (g_AcurObj)
				MultiHideObject(g_AcurObj);

			for (int i = 0; i < g_numTrails; i++) {
				if (g_ntrailData[i].trailObj != NULL)
					MultiHideObject(g_ntrailData[i].trailObj);
			}

			// Wait 'til cursor is again required.
			while (g_bHiddenCursor) {
				CORO_SLEEP(1);

				// Stop/start between scenes
				CORO_INVOKE_0(CursorStoppedCheck);
			}
		}
	}
	CORO_END_CODE;
}

/**
 * Called from dec_cursor() Glitter function.
 * Register the handle to cursor reel data.
 */
void DwInitCursor(SCNHANDLE bfilm) {
	const FILM *pfilm;

	g_hCursorFilm = bfilm;

	pfilm = (const FILM *)LockMem(g_hCursorFilm);
	g_numTrails = FROM_32(pfilm->numreels) - 1;

	assert(g_numTrails <= MAX_TRAILERS);
}

/**
 * DropCursor is called when a scene is closing down.
 */
void DropCursor() {
	if (TinselV2) {
		if (g_AcurObj)
			MultiDeleteObject(GetPlayfieldList(FIELD_STATUS), g_AcurObj);
		if (g_McurObj)
			MultiDeleteObject(GetPlayfieldList(FIELD_STATUS), g_McurObj);

		g_restart = 0;
	}

	g_AcurObj = NULL;		// No auxillary cursor
	g_McurObj = NULL;		// No cursor object (imminently deleted elsewhere)
	g_bHiddenCursor = false;	// Not hidden in next scene
	g_bTempNoTrailers = false;	// Trailers not hidden in next scene
	g_bWhoa = true;		// Suspend cursor processes

	for (int i = 0; i < g_numTrails; i++) {
		if (g_ntrailData[i].trailObj != NULL)		{
			MultiDeleteObject(GetPlayfieldList(FIELD_STATUS), g_ntrailData[i].trailObj);
			g_ntrailData[i].trailObj = NULL;
		}
	}
}

/**
 * RestartCursor is called when a new scene is starting up.
 */
void RestartCursor() {
	g_restart = 0x8000;	// Get the main cursor to re-initialize
}

/**
 * Called when restarting the game, ensures correct re-start with NULL
 * pointers etc.
 */
void RebootCursor() {
	g_McurObj = g_AcurObj = NULL;
	for (int i = 0; i < MAX_TRAILERS; i++)
		g_ntrailData[i].trailObj = NULL;

	g_bHiddenCursor = g_bTempNoTrailers = g_bFrozenCursor = false;

	g_hCursorFilm = 0;

	g_bWhoa = false;
	g_restart = 0;
}

void StartCursorFollowed() {
	DelAuxCursor();

	if (!SysVar(SV_ENABLEPRINTCURSOR))
		g_bTempHide = true;
}

void EndCursorFollowed() {
	InventoryIconCursor(false);	// May be holding something
	g_bTempHide = false;
}

bool isCursorShown() {
	return !(g_bTempHide || g_bHiddenCursor);
}

} // End of namespace Tinsel