/* 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.
 *
 * Plays films within a scene, takes into account the actor in each 'column'.								|
 */

#include "common/coroutines.h"
#include "tinsel/actors.h"
#include "tinsel/background.h"
#include "tinsel/dw.h"
#include "tinsel/film.h"
#include "tinsel/handle.h"
#include "tinsel/multiobj.h"
#include "tinsel/object.h"
#include "tinsel/pid.h"
#include "tinsel/play.h"
#include "tinsel/polygons.h"
#include "tinsel/rince.h"
#include "tinsel/sched.h"
#include "tinsel/scn.h"
#include "tinsel/sound.h"
#include "tinsel/timers.h"
#include "tinsel/tinlib.h"	// Stand()

namespace Tinsel {

struct PPINIT {
	SCNHANDLE hFilm;	// The 'film'
	int16	x;			// } Co-ordinates from the play()
	int16	y;			// } - set to (-1, -1) if none.
	int16	z;			// normally 0, set if from restore
	int16	speed;		// Film speed
	int16	actorid;	// Set if called from an actor code block
	uint8	splay;		// Set if called from splay()
	uint8	bTop;		// Set if called from topplay()
	uint8	bRestore;
	int16	sf;			// SlowFactor - only used for moving actors
	int16	column;		// Column number, first column = 0

	uint8	escOn;
	int32	myescEvent;
};

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

// FIXME: Avoid non-const global vars

static SOUNDREELS g_soundReels[MAX_SOUNDREELS];
static int g_soundReelNumbers[MAX_SOUNDREELS];

static int g_soundReelWait;

//-------------------- METHODS ----------------------

/**
 * Poke the background palette into an image.
 */
static void PokeInPalette(SCNHANDLE hMulFrame) {
	const FRAME *pFrame;		// Pointer to frame
	IMAGE *pim;		// Pointer to image

	// Could be an empty column
	if (hMulFrame) {
		pFrame = (const FRAME *)LockMem(hMulFrame);

		// get pointer to image
		pim = (IMAGE *)LockMem(READ_32(pFrame));	// handle to image

		pim->hImgPal = TO_32(BgPal());
	}
}

/**
 * Poke the background palette into an image.
 */
void PokeInPalette(const MULTI_INIT *pmi) {
	FRAME	*pFrame;		// Pointer to frame
	IMAGE	*pim;			// Pointer to image

	// Could be an empty column
	if (pmi->hMulFrame) {
		pFrame = (FRAME *)LockMem(FROM_32(pmi->hMulFrame));

		// get pointer to image
		pim = (IMAGE *)LockMem(READ_32(pFrame));	// handle to image

		pim->hImgPal = TO_32(BgPal());
	}
}

int32 NoNameFunc(int actorID, bool bNewMover) {
	PMOVER	pActor;
	int32	retval;

	pActor = GetMover(actorID);

	if (pActor != NULL && !bNewMover) {
		// If no path, just use first path in the scene
		if (pActor->hCpath == NOPOLY)
			retval = GetPolyZfactor(FirstPathPoly());
		else
			retval = GetPolyZfactor(pActor->hCpath);
	} else {
		switch (actorMaskType(actorID)) {
		case ACT_DEFAULT:
			retval = 0;
			break;
		case ACT_MASK:
			retval = 0;
			break;
		case ACT_ALWAYS:
			retval = 10;
			break;
		default:
			retval = actorMaskType(actorID);
			break;
		}
	}

	return retval;
}

static FREEL *GetReel(SCNHANDLE hFilm, int column) {
	FILM *pFilm = (FILM *)LockMem(hFilm);

	return &pFilm->reels[column];
}

static int RegisterSoundReel(SCNHANDLE hFilm, int column, int actorCol) {
	int i;

	for (i = 0; i < MAX_SOUNDREELS; i++) {
		// Should assert this doesn't happen, but let's be tolerant
		if (g_soundReels[i].hFilm == hFilm && g_soundReels[i].column == column)
			break;

		if (!g_soundReels[i].hFilm) {
			g_soundReels[i].hFilm = hFilm;
			g_soundReels[i].column = column;
			g_soundReels[i].actorCol = actorCol;
			break;
		}
	}

	g_soundReelNumbers[i]++;
	return i;
}

void NoSoundReels() {
	memset(g_soundReels, 0, sizeof(g_soundReels));
	g_soundReelWait = 0;
}

static void DeRegisterSoundReel(SCNHANDLE hFilm, int column) {
	for (int i = 0; i < MAX_SOUNDREELS; i++) {
		// Should assert this doesn't happen, but let's be tolerant
		if (g_soundReels[i].hFilm == hFilm && g_soundReels[i].column == column) {
			g_soundReels[i].hFilm = 0;
			break;
		}
	}
}

void SaveSoundReels(PSOUNDREELS psr) {
	for (int i = 0; i < MAX_SOUNDREELS; i++) {
		if (IsCdPlayHandle(g_soundReels[i].hFilm))
			g_soundReels[i].hFilm = 0;
	}

	memcpy(psr, g_soundReels, sizeof(g_soundReels));
}

void RestoreSoundReels(PSOUNDREELS psr) {
	memcpy(g_soundReels, psr, sizeof(g_soundReels));
}

static uint32 GetZfactor(int actorID, PMOVER pMover, bool bNewMover) {
	if (pMover != NULL && bNewMover == false) {
		// If no path, just use first path in the scene
		if (pMover->hCpath == NOPOLY)
			return GetPolyZfactor(FirstPathPoly());
		else
			return GetPolyZfactor(pMover->hCpath);
	} else {
		return GetActorZfactor(actorID);
	}
}

/**
 * Handles reels with sound id.
 * @param hFilm				The 'film'
 * @param column			Column number, first column = 0
 * @param speed				Film speed
 */
static void SoundReel(CORO_PARAM, SCNHANDLE hFilm, int column, int speed,
					int myescEvent, int actorCol) {
	FILM *pFilm;
	FREEL *pReel;
	ANI_SCRIPT *pAni;

	short x, y;

	CORO_BEGIN_CONTEXT;
		int myId;
		int myNum;
		int frameNumber;
		int speed;
		int sampleNumber;
		bool bFinished;
		bool bLooped;
		int reelActor;
	CORO_END_CONTEXT(_ctx);

	CORO_BEGIN_CODE(_ctx);

	if (actorCol) {
		PMULTI_INIT pmi;		// MULTI_INIT structure

		pReel = GetReel(hFilm, actorCol - 1);
		pmi = (PMULTI_INIT) LockMem(FROM_32(pReel->mobj));
		_ctx->reelActor = (int32)FROM_32(pmi->mulID);
	} else
		_ctx->reelActor = 0;

	_ctx->frameNumber = 0;
	_ctx->speed = speed;
	_ctx->sampleNumber = 0;
	_ctx->bFinished = false;
	_ctx->bLooped = false;
	_ctx->myId = RegisterSoundReel(hFilm, column, actorCol);
	_ctx->myNum = g_soundReelNumbers[_ctx->myId];

	do {
		pFilm = (FILM *)LockMem(hFilm);
		pReel = &pFilm->reels[column];

		pAni = (ANI_SCRIPT *)LockMem(FROM_32(pReel->script));

		if (_ctx->speed == -1) {
			_ctx->speed = (ONE_SECOND/FROM_32(pFilm->frate));

			// Restored reel
			for (;;) {
				if (FROM_32(pAni[_ctx->frameNumber].op) == ANI_END)
					break;
				else if (FROM_32(pAni[_ctx->frameNumber].op) == ANI_JUMP) {
					_ctx->frameNumber++;
					_ctx->frameNumber += FROM_32(pAni[_ctx->frameNumber].op);
					break;
				}
				// Could check for the other stuff here
				// but they really dont happen
				// OH YES THEY DO
				else if (FROM_32(pAni[_ctx->frameNumber].op) == ANI_ADJUSTX
					||	 FROM_32(pAni[_ctx->frameNumber].op) == ANI_ADJUSTY) {
					_ctx->frameNumber += 2;
				} else if (FROM_32(pAni[_ctx->frameNumber].op) == ANI_ADJUSTXY) {
					_ctx->frameNumber += 3;
				} else {
					// ANI_STOP, ANI_HIDE, ANI_HFLIP,
					// ANI_VFLIP, ANI_HVFLIP, default
					_ctx->frameNumber++;
				}
			}
		}

		switch (FROM_32(pAni[_ctx->frameNumber].op)) {
		case ANI_END:
			// Stop this sample if repeating
			if (_ctx->sampleNumber && _ctx->bLooped)
				_vm->_sound->stopSpecSample(_ctx->sampleNumber, 0);
			_ctx->bFinished = true;
			break;

		case ANI_JUMP:
			_ctx->frameNumber++;

			assert((int32)FROM_32(pAni[_ctx->frameNumber].op) < 0);

			_ctx->frameNumber += FROM_32(pAni[_ctx->frameNumber].op);

			assert(_ctx->frameNumber >= 0);
			continue;

		case ANI_STOP:
			// Stop this sample
			if (_ctx->sampleNumber)
				_vm->_sound->stopSpecSample(_ctx->sampleNumber, 0);
			break;

		case ANI_HIDE:
			// No op
			break;

		case ANI_HFLIP:
		case ANI_VFLIP:
		case ANI_HVFLIP:
			_ctx->frameNumber++;
			continue;

		case ANI_ADJUSTX:
		case ANI_ADJUSTY:
			_ctx->frameNumber += 2;
			continue;

		case ANI_ADJUSTXY:
			_ctx->frameNumber += 3;
			continue;

		default:
			// Stop this sample
			if (_ctx->sampleNumber)
				_vm->_sound->stopSpecSample(_ctx->sampleNumber, 0);

			_ctx->sampleNumber = FROM_32(pAni[_ctx->frameNumber++].op);
			if (_ctx->sampleNumber > 0)
				_ctx->bLooped = false;
			else {
				_ctx->sampleNumber = ~_ctx->sampleNumber;
				_ctx->bLooped = true;
			}
			x = (short)(FROM_32(pAni[_ctx->frameNumber].op) >> 16);
			y = (short)(FROM_32(pAni[_ctx->frameNumber].op) & 0xffff);

			if (x == 0)
				x = -1;

			_vm->_sound->playSample(_ctx->sampleNumber, 0, _ctx->bLooped, x, y, PRIORITY_SCRIPT,
					Audio::Mixer::kSFXSoundType);

			break;
		}

		CORO_SLEEP(_ctx->speed);
		_ctx->frameNumber++;

		if (_ctx->reelActor && GetActorPresFilm(_ctx->reelActor) != hFilm) {
			// Stop this sample if repeating
			if (_ctx->sampleNumber && _ctx->bLooped)
				_vm->_sound->stopSpecSample(_ctx->sampleNumber, 0);

			_ctx->bFinished = true;
		}

		if (myescEvent && myescEvent != GetEscEvents()) {
			// Stop this sample
			if (_ctx->sampleNumber)
				_vm->_sound->stopSpecSample(_ctx->sampleNumber, 0);

			_ctx->bFinished = true;
		}
	} while (!_ctx->bFinished && _ctx->myNum == g_soundReelNumbers[_ctx->myId]);

	// De-register - if not been replaced
	if (_ctx->myNum == g_soundReelNumbers[_ctx->myId])
		DeRegisterSoundReel(hFilm, column);

	CORO_END_CODE;
}

static void ResSoundReel(CORO_PARAM, const void *param) {
	// get the stuff copied to process when it was created
	int i = *(const int *)param;

	CORO_BEGIN_CONTEXT;
	CORO_END_CONTEXT(_ctx);

	CORO_BEGIN_CODE(_ctx);

	CORO_INVOKE_ARGS(SoundReel, (CORO_SUBCTX, g_soundReels[i].hFilm, g_soundReels[i].column,
		-1, 0, g_soundReels[i].actorCol));

	CORO_KILL_SELF();
	CORO_END_CODE;
}

static void SoundReelWaitCheck() {
	if (--g_soundReelWait == 0) {
		for (int i = 0; i < MAX_SOUNDREELS; i++) {
			if (g_soundReels[i].hFilm) {
				CoroScheduler.createProcess(PID_REEL, ResSoundReel, &i, sizeof(i));
			}
		}
	}
}

/**
 * - Don't bother if this reel is already playing for this actor.
 * - If explicit co-ordinates, use these, If embedded co-ordinates,
 * leave alone, otherwise use actor's current position.
 * - Moving actors get hidden during this play, other actors get
 * _ctx->replaced by this play.
 * - Column 0 of a film gets its appropriate Z-position, slave columns
 * get slightly bigger Z-positions, in column order.
 * - Play proceeds until the script finishes, another reel starts up for
 * this actor, or the actor gets killed.
 * - If called from an splay(), moving actor's co-ordinates are updated
 * after the play, any walk still in progress will go on from there.
 */
static void t1PlayReel(CORO_PARAM, const PPINIT *ppi) {
	CORO_BEGIN_CONTEXT;
		OBJECT	*pPlayObj;	// Object
		ANIM	thisAnim;	// Animation structure

		bool	mActor;		// Gets set if this is a moving actor
		bool	lifeNoMatter;
		bool	replaced;

		const FREEL *pfreel;	// The 'column' to play
		int		stepCount;
		int		frameCount;
		int		reelActor;
		PMOVER	pActor;
		int tmpX, tmpY;
	CORO_END_CONTEXT(_ctx);

	// FIXME: Avoid non-const global vars
	static int	firstColZ = 0;	// Z-position of column zero
	static int32	fColZfactor = 0;	// Z-factor of column zero's actor

	CORO_BEGIN_CODE(_ctx);

	const MULTI_INIT *pmi;		// MULTI_INIT structure
	bool	bNewMover;	// Gets set if a moving actor that isn't in scene yet

	const FILM *pfilm;

	_ctx->lifeNoMatter = false;
	_ctx->replaced = false;
	_ctx->pActor = NULL;
	bNewMover = false;

	pfilm = (const FILM *)LockMem(ppi->hFilm);
	_ctx->pfreel = &pfilm->reels[ppi->column];

	// Get the MULTI_INIT structure
	pmi = (const MULTI_INIT *)LockMem(FROM_32(_ctx->pfreel->mobj));

	// Save actor's ID
	_ctx->reelActor = (int32)FROM_32(pmi->mulID);

	/**** New (experimental? bit 5/1/95 ****/
	if (!TinselV0 && !actorAlive(_ctx->reelActor))
		return;
	/**** Delete a bit down there if this stays ****/

	UpdateActorEsc(_ctx->reelActor, ppi->escOn, ppi->myescEvent);

	// To handle the play()-talk(), talk()-play(), talk()-talk() and play()-play() scenarios
	if (ppi->hFilm != GetActorLatestFilm(_ctx->reelActor)) {
		// This in not the last film scheduled for this actor

		// It may be the last non-talk film though
		if (ActorIsTalking(_ctx->reelActor))
			SetActorPlayFilm(_ctx->reelActor, ppi->hFilm);	// Revert to this film after talk

		return;
	}
	if (ActorIsTalking(_ctx->reelActor)) {
		// Note: will delete this and there'll be no need to store the talk film!
		if (ppi->hFilm != GetActorTalkFilm(_ctx->reelActor)) {
			SetActorPlayFilm(_ctx->reelActor, ppi->hFilm);	// Revert to this film after talk
			return;
		}
	} else {
		SetActorPlayFilm(_ctx->reelActor, ppi->hFilm);
	}

	// If this reel is already playing for this actor, just forget it.
	if (actorReel(_ctx->reelActor) == _ctx->pfreel)
		return;

	// Poke in the background palette
	PokeInPalette(FROM_32(pmi->hMulFrame));

	// Set up and insert the multi-object
	_ctx->pPlayObj = MultiInitObject(pmi);
	if (!ppi->bTop)
		MultiInsertObject(GetPlayfieldList(FIELD_WORLD), _ctx->pPlayObj);
	else
		MultiInsertObject(GetPlayfieldList(FIELD_STATUS), _ctx->pPlayObj);

	// If co-ordinates are specified, use specified.
	// Otherwise, use actor's position if there are not embedded co-ords.
	// Add this first test for nth columns with offsets
	// in plays with (x,y)
	_ctx->tmpX = ppi->x;
	_ctx->tmpY = ppi->y;
	if (ppi->column != 0 && (pmi->mulX || pmi->mulY)) {
	} else if (_ctx->tmpX != -1 || _ctx->tmpY != -1) {
		MultiSetAniXY(_ctx->pPlayObj, _ctx->tmpX, _ctx->tmpY);
	} else if (!pmi->mulX && !pmi->mulY) {
		GetActorPos(_ctx->reelActor, &_ctx->tmpX, &_ctx->tmpY);
		MultiSetAniXY(_ctx->pPlayObj, _ctx->tmpX, _ctx->tmpY);
	}

	// If it's a moving actor, this hides the moving actor
	// used to do this only if (actorid == 0) - I don't know why
	_ctx->mActor = HideMovingActor(_ctx->reelActor, ppi->sf);

	// If it's a moving actor, get its MOVER structure.
	// If it isn't in the scene yet, get its task running - using
	// Stand() - to prevent a glitch at the end of the play.
	if (_ctx->mActor) {
		_ctx->pActor = GetMover(_ctx->reelActor);
		if (!getMActorState(_ctx->pActor)) {
			CORO_INVOKE_ARGS(Stand, (CORO_SUBCTX, _ctx->reelActor, MAGICX, MAGICY, 0));
			bNewMover = true;
		}
	}

	// Register the fact that we're playing this for this actor
	storeActorReel(_ctx->reelActor, _ctx->pfreel, ppi->hFilm, _ctx->pPlayObj, ppi->column, _ctx->tmpX, _ctx->tmpY);

	/**** Will get rid of this if the above is kept ****/
	// We may be temporarily resuscitating a dead actor
	if (ppi->actorid == 0 && !actorAlive(_ctx->reelActor))
		_ctx->lifeNoMatter = true;

	InitStepAnimScript(&_ctx->thisAnim, _ctx->pPlayObj,  FROM_32(_ctx->pfreel->script), ppi->speed);

	// If first column, set Z position as per
	// Otherwise, column 0's + column number
	// N.B. It HAS been ensured that the first column gets here first
	if (ppi->z != 0) {
		MultiSetZPosition(_ctx->pPlayObj, ppi->z);
		StoreActorZpos(_ctx->reelActor, ppi->z);
	} else if (ppi->bTop) {
		if (ppi->column == 0) {
			firstColZ = Z_TOPPLAY + actorMaskType(_ctx->reelActor);
			MultiSetZPosition(_ctx->pPlayObj, firstColZ);
			StoreActorZpos(_ctx->reelActor, firstColZ);
		} else {
			MultiSetZPosition(_ctx->pPlayObj, firstColZ + ppi->column);
			StoreActorZpos(_ctx->reelActor, firstColZ + ppi->column);
		}
	} else if (ppi->column == 0) {
		if (_ctx->mActor && !bNewMover) {
			// If no path, just use first path in the scene
			if (_ctx->pActor->hCpath == NOPOLY)
				fColZfactor = GetPolyZfactor(FirstPathPoly());
			else
				fColZfactor = GetPolyZfactor(_ctx->pActor->hCpath);
			firstColZ = AsetZPos(_ctx->pPlayObj, MultiLowest(_ctx->pPlayObj), fColZfactor);
		} else {
			switch (actorMaskType(_ctx->reelActor)) {
			case ACT_DEFAULT:
				fColZfactor = 0;
				firstColZ = 2;
				MultiSetZPosition(_ctx->pPlayObj, firstColZ);
				break;
			case ACT_MASK:
				fColZfactor = 0;
				firstColZ = MultiLowest(_ctx->pPlayObj);
				MultiSetZPosition(_ctx->pPlayObj, firstColZ);
				break;
			case ACT_ALWAYS:
				fColZfactor = 10;
				firstColZ = 10000;
				MultiSetZPosition(_ctx->pPlayObj, firstColZ);
				break;
			default:
				fColZfactor = actorMaskType(_ctx->reelActor);
				firstColZ = AsetZPos(_ctx->pPlayObj, MultiLowest(_ctx->pPlayObj), fColZfactor);
				if (firstColZ < 2) {
					// This is an experiment!
					firstColZ = 2;
					MultiSetZPosition(_ctx->pPlayObj, firstColZ);
				}
				break;
			}
		}
		StoreActorZpos(_ctx->reelActor, firstColZ);
	} else {
		if (NoNameFunc(_ctx->reelActor, bNewMover) > fColZfactor) {
			fColZfactor = NoNameFunc(_ctx->reelActor, bNewMover);
			firstColZ = fColZfactor << 10;
		}
		MultiSetZPosition(_ctx->pPlayObj, firstColZ + ppi->column);
		StoreActorZpos(_ctx->reelActor, firstColZ + ppi->column);
	}

	/*
	 * Play until the script finishes,
	 * another reel starts up for this actor,
	 * or the actor gets killed.
	 */
	_ctx->stepCount = 0;
	_ctx->frameCount = 0;
	do {
		if (_ctx->stepCount++ == 0) {
			_ctx->frameCount++;
			StoreActorSteps(_ctx->reelActor, _ctx->frameCount);
		}
		if (_ctx->stepCount == ppi->speed)
			_ctx->stepCount = 0;

		if (StepAnimScript(&_ctx->thisAnim) == ScriptFinished)
			break;

		int x, y;
		GetAniPosition(_ctx->pPlayObj, &x, &y);
		StoreActorPos(_ctx->reelActor, x, y);

		CORO_SLEEP(1);

		if (actorReel(_ctx->reelActor) != _ctx->pfreel) {
			_ctx->replaced = true;
			break;
		}

		if (ActorEsc(_ctx->reelActor) && ActorEev(_ctx->reelActor) != GetEscEvents())
			break;

	} while (_ctx->lifeNoMatter || actorAlive(_ctx->reelActor));

	// Register the fact that we're NOT playing this for this actor
	if (actorReel(_ctx->reelActor) == _ctx->pfreel)
		storeActorReel(_ctx->reelActor, NULL, 0, NULL, 0, 0, 0);

	// Ditch the object
	if (!ppi->bTop)
		MultiDeleteObject(GetPlayfieldList(FIELD_WORLD), _ctx->pPlayObj);
	else
		MultiDeleteObject(GetPlayfieldList(FIELD_STATUS), _ctx->pPlayObj);

	if (_ctx->mActor) {
		if (!_ctx->replaced)
			unHideMovingActor(_ctx->reelActor);	// Restore moving actor

		// Update it's co-ordinates if this is an splay()
		if (ppi->splay)
			restoreMovement(_ctx->reelActor);
	}
	CORO_END_CODE;
}

/**
 * - Don't bother if this reel is already playing for this actor.
 * - If explicit co-ordinates, use these, If embedded co-ordinates,
 * leave alone, otherwise use actor's current position.
 * - Moving actors get hidden during this play, other actors get
 * replaced by this play.
 * - Column 0 of a film gets its appropriate Z-position, slave columns
 * get slightly bigger Z-positions, in column order.
 * - Play proceeds until the script finishes, another reel starts up for
 * this actor, or the actor gets killed.
 * - If called from an splay(), moving actor's co-ordinates are updated
 * after the play, any walk still in progress will go on from there.
 * @param x				Co-ordinates from the play(), set to (-1, -1) if none
 * @param y				Co-ordinates from the play(), set to (-1, -1) if none
 * @param bRestore		Normally False, set if from restore
 * @param speed			Film speed
 * @param hFilm			The 'film'
 * @param column		Column number, first column = 0
 */
static void t2PlayReel(CORO_PARAM, int x, int y, bool bRestore, int speed, SCNHANDLE hFilm,
		int column, int myescEvent, bool bTop) {
	CORO_BEGIN_CONTEXT;
		bool bReplaced;
		bool bGotHidden;
		int	stepCount;
		int	frameCount;
		bool bEscapedAlready;
		bool	bPrinciple;	// true if this is the first column in a film for one actor
		bool	bRelative;	// true if relative specified in script

		FREEL		*pFreel;
		MULTI_INIT	*pmi;	// MULTI_INIT structure
		POBJECT		pPlayObj;	// Object
		ANIM		thisAnim;	// Animation structure

		int	reelActor;			// Which actor this reel belongs to
		PMOVER	pMover;			// set if it's a moving actor
		bool	bNewMover;		// Gets set if a moving actor that isn't in scene yet

		int	filmNumber;
		int	myZ;				// Remember for hide/unhide
	CORO_END_CONTEXT(_ctx);

	CORO_BEGIN_CODE(_ctx);

	_ctx->bReplaced = false;
	_ctx->bGotHidden = false;
	_ctx->stepCount = 0;
	_ctx->frameCount = 0;

	_ctx->bEscapedAlready = false;

	// Get the reel and MULTI_INIT structure
	_ctx->pFreel = GetReel(hFilm, column);
	_ctx->pmi = (MULTI_INIT *)LockMem(FROM_32(_ctx->pFreel->mobj));

	if ((int32)FROM_32(_ctx->pmi->mulID) == -2) {
		CORO_INVOKE_ARGS(SoundReel, (CORO_SUBCTX, hFilm, column, speed, myescEvent,
			FROM_32(_ctx->pmi->otherFlags) & OTH_RELATEDACTOR));
		return;
	}

	// Save actor's ID
	_ctx->reelActor = FROM_32(_ctx->pmi->mulID);

	UpdateActorEsc(_ctx->reelActor, myescEvent);

	// To handle the play()-talk(), talk()-play(), talk()-talk() and play()-play() scenarios
	if (hFilm != GetActorLatestFilm(_ctx->reelActor)) {
		// This in not the last film scheduled for this actor

		// It may be the last non-talk film though
		if (ActorIsTalking(_ctx->reelActor))
			SetActorPlayFilm(_ctx->reelActor, hFilm);	// Revert to this film after talk

		return;
	}
	if (ActorIsTalking(_ctx->reelActor)) {
		// Note: will delete this and there'll be no need to store the talk film!
		if (hFilm != GetActorTalkFilm(_ctx->reelActor)) {
			SetActorPlayFilm(_ctx->reelActor, hFilm);	// Revert to this film after talk
			return;
		}
	} else {
		SetActorPlayFilm(_ctx->reelActor, hFilm);
	}

	// Register the film for this actor
	if (hFilm != GetActorPresFilm(_ctx->reelActor)) {
		_ctx->bPrinciple = true;
		StoreActorPresFilm(_ctx->reelActor, hFilm, x, y);
	} else {
		_ctx->bPrinciple = false;

		// If this reel is already playing for this actor, just forget it.
		if (ActorReelPlaying(_ctx->reelActor, column))
			return;
	}

	/*
	 * Insert the object
	 */
	// Poke in the background palette
	PokeInPalette(_ctx->pmi);

	// Set ghost bit if wanted
	if (ActorIsGhost(_ctx->reelActor)) {
		assert(FROM_32(_ctx->pmi->mulFlags) == DMA_WNZ || FROM_32(_ctx->pmi->mulFlags) == (DMA_WNZ | DMA_GHOST));
		_ctx->pmi->mulFlags = TO_32(FROM_32(_ctx->pmi->mulFlags) | DMA_GHOST);
	}

	// Set up and insert the multi-object
	_ctx->pPlayObj = MultiInitObject(_ctx->pmi);
	if (!bTop)
		MultiInsertObject(GetPlayfieldList(FIELD_WORLD), _ctx->pPlayObj);
	else
		MultiInsertObject(GetPlayfieldList(FIELD_STATUS), _ctx->pPlayObj);

	/*
	 * More action for moving actors
	*/
	_ctx->pMover = GetMover(_ctx->reelActor);
	if (_ctx->pMover != NULL) {
		HideMover(_ctx->pMover);

		if (!MoverIs(_ctx->pMover)) {
			// Used to do a Stand here to prevent glitches

			_ctx->bNewMover = true;
		} else
			_ctx->bNewMover = false;
	}

	// Register the reel for this actor
	StoreActorReel(_ctx->reelActor, column, _ctx->pPlayObj);

	_ctx->filmNumber = GetActorFilmNumber(_ctx->reelActor);

	/*
	 * Sort out x and y
	 */
	assert( ((FROM_32(_ctx->pmi->otherFlags) & OTH_RELATIVE) && !(FROM_32(_ctx->pmi->otherFlags) & OTH_ABSOLUTE))
		|| ((FROM_32(_ctx->pmi->otherFlags) & OTH_ABSOLUTE) && !(FROM_32(_ctx->pmi->otherFlags) & OTH_RELATIVE)) );

	_ctx->bRelative = FROM_32(_ctx->pmi->otherFlags) & OTH_RELATIVE;

	if (_ctx->bRelative) {
		// Use actor's position. If (x, y) specified, move the actor.
		if (x == -1 && y == -1)
			GetActorPos(_ctx->reelActor, &x, &y);
		else
			StoreActorPos(_ctx->reelActor, x, y);
	} else if (x == -1 && y == -1)
		x = y = 0;		// Use (0,0) if no specified

	// Add embedded co-ords
	MultiSetAniXY(_ctx->pPlayObj, x + FROM_32(_ctx->pmi->mulX), y + FROM_32(_ctx->pmi->mulY));

	/*
	 * Sort out z
	 */
	if (bRestore) {
		_ctx->myZ = GetActorZpos(_ctx->reelActor, column);

		SoundReelWaitCheck();
	} else {
		// FIXME: Avoid non-const global vars
		static int baseZposn;		// Z-position of column zero
		static uint32 baseZfact;	// Z-factor of column zero's actor

		// N.B. It HAS been ensured that the first column gets here first

		if ((int32)FROM_32(_ctx->pmi->mulZ) != -1) {
			// Z override in script

			baseZfact = FROM_32(_ctx->pmi->mulZ);
			baseZposn = (baseZfact << ZSHIFT) + MultiLowest(_ctx->pPlayObj);
			if (bTop)
				baseZposn += Z_TOPPLAY;
		} else if (column == 0
				|| GetZfactor(_ctx->reelActor, _ctx->pMover, _ctx->bNewMover) > baseZfact) {
			// Subsequent columns are based on this one

			baseZfact = GetZfactor(_ctx->reelActor, _ctx->pMover, _ctx->bNewMover);
			baseZposn = (baseZfact << ZSHIFT) + MultiLowest(_ctx->pPlayObj);
			if (bTop)
				baseZposn += Z_TOPPLAY;
		}
		_ctx->myZ = baseZposn + column;
	}
	MultiSetZPosition(_ctx->pPlayObj, _ctx->myZ);
	StoreActorZpos(_ctx->reelActor, _ctx->myZ, column);

	/*
	 * Play until the script finishes,
	 * another reel starts up for this actor,
	 * or the actor gets killed.
	 */
	InitStepAnimScript(&_ctx->thisAnim, _ctx->pPlayObj, FROM_32(_ctx->pFreel->script), speed);

	if (bRestore || (ActorEsc(_ctx->reelActor) == true &&
				ActorEev(_ctx->reelActor) != GetEscEvents())) {
		// From restore, step to jump or end
		SkipFrames(&_ctx->thisAnim, -1);
	}

	for (;;) {
		if (_ctx->stepCount++ == 0) {
			_ctx->frameCount++;
			StoreActorSteps(_ctx->reelActor, _ctx->frameCount);
		}
		if (_ctx->stepCount == speed)
			_ctx->stepCount = 0;

		if (_ctx->bPrinciple && AboutToJumpOrEnd(&_ctx->thisAnim))
			IncLoopCount(_ctx->reelActor);

		if (StepAnimScript(&_ctx->thisAnim) == ScriptFinished)
			break;

		if (_ctx->bRelative) {
			GetAniPosition(_ctx->pPlayObj, &x, &y);
			StoreActorPos(_ctx->reelActor, x, y);
		}

		if (_ctx->bGotHidden) {
			if (!ActorHidden(_ctx->reelActor)) {
				MultiSetZPosition(_ctx->pPlayObj, _ctx->myZ);
				_ctx->bGotHidden = false;
			}
		} else {
			if (ActorHidden(_ctx->reelActor)) {
				MultiSetZPosition(_ctx->pPlayObj, -1);
				_ctx->bGotHidden = true;
			}
		}

		CORO_SLEEP(1);

		if (GetActorFilmNumber(_ctx->reelActor) != _ctx->filmNumber) {
			_ctx->bReplaced = true;
			break;
		}

		if (ActorEsc(_ctx->reelActor) == true && ActorEev(_ctx->reelActor) != GetEscEvents()) {
			if (!_ctx->bEscapedAlready) {
				SkipFrames(&_ctx->thisAnim, -1);
				_ctx->bEscapedAlready = true;
			}

//WHY???		UpdateActorEsc(reelActor, GetEscEvents());
// The above line of code, not commented out would fix the coffee pot flash
// but why was it commented out?
// The extra boolean is used instead, 'cos it's release week and I want to play it safe!
		}
	}

	// Register the fact that we're NOT playing this for this actor
	NotPlayingReel(_ctx->reelActor, _ctx->filmNumber, column);

	// Ditch the object
	if (!bTop)
		MultiDeleteObject(GetPlayfieldList(FIELD_WORLD), _ctx->pPlayObj);
	else
		MultiDeleteObject(GetPlayfieldList(FIELD_STATUS), _ctx->pPlayObj);

	// Restore moving actor is nessesary
	if (_ctx->pMover != NULL && _ctx->bPrinciple && !_ctx->bReplaced)
		UnHideMover(_ctx->pMover);

	CORO_END_CODE;
}

/**
 * Run all animations that comprise the play film.
 */
static void PlayProcess(CORO_PARAM, const void *param) {
	CORO_BEGIN_CONTEXT;
	CORO_END_CONTEXT(_ctx);

	const PPINIT *ppi = (const PPINIT *)param;
	CORO_BEGIN_CODE(_ctx);

	if (TinselV2)
		CORO_INVOKE_ARGS(t2PlayReel, (CORO_SUBCTX, ppi->x, ppi->y, ppi->bRestore, ppi->speed,
			ppi->hFilm, ppi->column, ppi->myescEvent, ppi->bTop));
	else
		CORO_INVOKE_1(t1PlayReel, ppi);

	CORO_END_CODE;
}

// *******************************************************


// To handle the play()-talk(), talk()-play(), talk()-talk() and play()-play() scenarios
void NewestFilm(SCNHANDLE film, const FREEL *reel) {
	const MULTI_INIT *pmi;		// MULTI_INIT structure

	// Get the MULTI_INIT structure
	pmi = (const MULTI_INIT *)LockMem(FROM_32(reel->mobj));

	if (!TinselV2 || ((int32)FROM_32(pmi->mulID) != -2))
		SetActorLatestFilm((int32)FROM_32(pmi->mulID), film);
}

// *******************************************************

/**
 * Start up a play process for each column in a film.
 *
 * NOTE: The processes are started in reverse order so that the first
 *   column's process kicks in first.
 */
void PlayFilm(CORO_PARAM, SCNHANDLE hFilm, int x, int y, int actorid, bool splay, bool sfact, bool escOn,
			  int myescEvent, bool bTop) {
	assert(hFilm != 0); // Trying to play NULL film
	const FILM *pFilm;

	CORO_BEGIN_CONTEXT;
	CORO_END_CONTEXT(_ctx);

	CORO_BEGIN_CODE(_ctx);

	pFilm = (const FILM *)LockMem(hFilm);
	PPINIT ppi;

	// Now allowed empty films!
	if (pFilm->numreels == 0)
		return;                 // Nothing to do!

	ppi.hFilm = hFilm;
	ppi.x = x;
	ppi.y = y;
	ppi.z = 0;
	ppi.bRestore = false;
	ppi.speed = (ONE_SECOND / FROM_32(pFilm->frate));
	ppi.actorid = actorid;
	ppi.splay = splay;
	ppi.bTop = bTop;
	ppi.sf = sfact;
	ppi.escOn = escOn;
	ppi.myescEvent = myescEvent;

	// Start display process for each reel in the film
	for (int i = FROM_32(pFilm->numreels) - 1; i >= 0; i--) {
		NewestFilm(hFilm, &pFilm->reels[i]);

		ppi.column = i;
		CoroScheduler.createProcess(PID_REEL, PlayProcess, &ppi, sizeof(PPINIT));
	}

	if (TinselV2) {
		// Let it all kick in and position this process
		// down the process list from the playing process(es)
		// This ensures something
		CORO_GIVE_WAY;

		if (myescEvent && myescEvent != GetEscEvents())
			CoroScheduler.rescheduleAll();
	}

	CORO_END_CODE;
}

void PlayFilm(CORO_PARAM, SCNHANDLE hFilm, int x, int y, int myescEvent, bool bTop) {
	PlayFilm(coroParam, hFilm, x, y, 0, false, false, false, myescEvent, bTop);
}

/**
 * Start up a play process for each slave column in a film.
 * Play the first column directly from the parent process.
 */
void PlayFilmc(CORO_PARAM, SCNHANDLE hFilm, int x, int y, int actorid, bool splay, bool sfact,
			   bool escOn, int myescEvent, bool bTop) {
	CORO_BEGIN_CONTEXT;
		PPINIT ppi;
		int i;
		int loopCount;
	CORO_END_CONTEXT(_ctx);

	CORO_BEGIN_CODE(_ctx);

	assert(hFilm != 0); // Trying to play NULL film
	const FILM *pFilm;

	pFilm = (const FILM *)LockMem(hFilm);

	// Now allowed empty films!
	if (pFilm->numreels == 0)
		return;                 //  Already played to completion!

	_ctx->ppi.hFilm = hFilm;
	_ctx->ppi.x = x;
	_ctx->ppi.y = y;
	_ctx->ppi.z = 0;
	_ctx->ppi.bRestore = false;
	_ctx->ppi.speed = (ONE_SECOND / FROM_32(pFilm->frate));
	_ctx->ppi.actorid = actorid;
	_ctx->ppi.splay = splay;
	_ctx->ppi.bTop = bTop;
	_ctx->ppi.sf = sfact;
	_ctx->ppi.escOn = escOn;
	_ctx->ppi.myescEvent = myescEvent;

	// Start display process for each secondary reel in the film in Tinsel 1,
	// or all of them in Tinsel 2
	for (int i = FROM_32(pFilm->numreels) - 1; i >= (TinselV2 ? 0 : 1); i--) {
		NewestFilm(hFilm, &pFilm->reels[i]);

		_ctx->ppi.column = i;
		CoroScheduler.createProcess(PID_REEL, PlayProcess, &_ctx->ppi, sizeof(PPINIT));
	}

	if (TinselV2) {
		// Let it all kick in and position this 'waiting' process
		// down the process list from the playing process(es)
		// This ensures immediate return when the reel finishes
		CORO_GIVE_WAY;

		_ctx->i = ExtractActor(hFilm);
		_ctx->loopCount = GetLoopCount(_ctx->i);

		// Wait until film changes or loop count increases
		while (GetActorPresFilm(_ctx->i) == hFilm && GetLoopCount(_ctx->i) == _ctx->loopCount) {
			if (myescEvent && myescEvent != GetEscEvents()) {
				CoroScheduler.rescheduleAll();
				break;
			}

			CORO_SLEEP(1);
		}
	} else {
		// For Tinsel 1, launch the primary reel
		NewestFilm(hFilm, &pFilm->reels[0]);

		_ctx->ppi.column = 0;
		CORO_INVOKE_1(t1PlayReel, &_ctx->ppi);
	}

	CORO_END_CODE;
}

/**
 * Start up a play process for a particular column in a film.
 *
 * NOTE: This is specifically for actors during a Tinsel 1 restore scene.
 */
void RestoreActorReels(SCNHANDLE hFilm, short reelnum, short z, int x, int y) {
	assert(!TinselV2);
	const FILM *pfilm = (const FILM *)LockMem(hFilm);
	PPINIT ppi;

	ppi.hFilm = hFilm;
	ppi.x = x;
	ppi.y = y;
	ppi.z = z;
	ppi.speed = (ONE_SECOND / FROM_32(pfilm->frate));
	ppi.actorid = 0;
	ppi.splay = false;
	ppi.bTop = false;
	ppi.bRestore = true;
	ppi.sf = 0;
	ppi.column = reelnum;
	ppi.myescEvent = 0;

	ppi.escOn = false;
	ppi.myescEvent = GetEscEvents();

	assert(pfilm->numreels);

	NewestFilm(hFilm, &pfilm->reels[reelnum]);

	// Start display process for the reel
	CoroScheduler.createProcess(PID_REEL, PlayProcess, &ppi, sizeof(ppi));
}

/**
 * Start up a play process for a particular column in a film.
 *
 * NOTE: This is specifically for actors during a Tinsel 2 restore scene.
 */
void RestoreActorReels(SCNHANDLE hFilm, int actor, int x, int y) {
	assert(TinselV2);
	FILM *pFilm = (FILM *)LockMem(hFilm);
	PPINIT ppi;

	int i;
	FREEL *pFreel;
	PMULTI_INIT	pmi;		// MULTI_INIT structure

	ppi.hFilm = hFilm;
	ppi.x = (short)x;
	ppi.y = (short)y;
	ppi.bRestore = true;
	ppi.speed = (short)(ONE_SECOND/FROM_32(pFilm->frate));
	ppi.bTop = false;
	ppi.myescEvent = 0;

	// Search backwards for now as later column will be the one
	for (i = (int)FROM_32(pFilm->numreels) - 1; i >= 0; i--) {
		pFreel = &pFilm->reels[i];
		pmi = (PMULTI_INIT) LockMem(FROM_32(pFreel->mobj));
		if ((int32)FROM_32(pmi->mulID) == actor) {
			ppi.column = (short)i;
			NewestFilm(hFilm, &pFilm->reels[i]);

			// Start display process for the reel
			CoroScheduler.createProcess(PID_REEL, PlayProcess, &ppi, sizeof(ppi));

			g_soundReelWait++;
		}
	}
}

/**
 * Get the actor id from a film (column 0)
 */
int ExtractActor(SCNHANDLE hFilm) {
	const FILM *pFilm = (const FILM *)LockMem(hFilm);
	const FREEL *pReel = &pFilm->reels[0];
	const MULTI_INIT *pmi = (const MULTI_INIT *)LockMem(FROM_32(pReel->mobj));
	return (int)FROM_32(pmi->mulID);
}

} // End of namespace Tinsel
