/* 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.
 *
 * Save and restore scene and game.
 */


#include "tinsel/actors.h"
#include "tinsel/background.h"
#include "tinsel/config.h"
#include "tinsel/drives.h"
#include "tinsel/dw.h"
#include "tinsel/faders.h"		// FadeOutFast()
#include "tinsel/graphics.h"		// ClearScreen()
#include "tinsel/handle.h"
#include "tinsel/dialogs.h"
#include "tinsel/music.h"
#include "tinsel/pid.h"
#include "tinsel/play.h"
#include "tinsel/polygons.h"
#include "tinsel/rince.h"
#include "tinsel/savescn.h"
#include "tinsel/scene.h"
#include "tinsel/sched.h"
#include "tinsel/scroll.h"
#include "tinsel/sound.h"
#include "tinsel/sysvar.h"
#include "tinsel/tinlib.h"
#include "tinsel/token.h"

#include "common/textconsole.h"

namespace Tinsel {

//----------------- EXTERN FUNCTIONS --------------------

// in BG.C
extern SCNHANDLE GetBgroundHandle();
extern void SetDoFadeIn(bool tf);

// In DOS_DW.C
void RestoreMasterProcess(INT_CONTEXT *pic);

// in EVENTS.C (declared here and not in events.h because of strange goings-on)
void RestoreProcess(INT_CONTEXT *pic);

// in SCENE.C
extern SCNHANDLE GetSceneHandle();


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

enum {
	RS_COUNT = 5,	// Restore scene count

	MAX_NEST = 4
};

//----------------- EXTERNAL GLOBAL DATA --------------------

extern int	g_thingHeld;
extern int	g_restoreCD;
extern SRSTATE g_SRstate;

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

// FIXME: Avoid non-const global vars

bool g_ASceneIsSaved = false;

static int g_savedSceneCount = 0;

static bool g_bNotDoneYet = false;

//static SAVED_DATA ssData[MAX_NEST];
static SAVED_DATA *g_ssData = NULL;
static SAVED_DATA g_sgData;

static SAVED_DATA *g_rsd = 0;

static int g_RestoreSceneCount = 0;

static bool g_bNoFade = false;

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

/**
 * Save current scene.
 * @param sd			Pointer to the scene data
 */
void DoSaveScene(SAVED_DATA *sd) {
	sd->SavedSceneHandle = GetSceneHandle();
	sd->SavedBgroundHandle = GetBgroundHandle();
	SaveMovers(sd->SavedMoverInfo);
	sd->NumSavedActors = SaveActors(sd->SavedActorInfo);
	PlayfieldGetPos(FIELD_WORLD, &sd->SavedLoffset, &sd->SavedToffset);
	SaveInterpretContexts(sd->SavedICInfo);
	sd->SavedControl = ControlIsOn();
	sd->SavedNoBlocking = GetNoBlocking();
	GetNoScrollData(&sd->SavedNoScrollData);

	if (TinselV2) {
		// Tinsel 2 specific data save
		SaveActorZ(sd->savedActorZ);
		SaveZpositions(sd->zPositions);
		SavePolygonStuff(sd->SavedPolygonStuff);
		_vm->_pcmMusic->getTunePlaying(sd->SavedTune, sizeof(sd->SavedTune));
		sd->bTinselDim = _vm->_pcmMusic->getMusicTinselDimmed();
		sd->SavedScrollFocus = GetScrollFocus();
		SaveSysVars(sd->SavedSystemVars);
		SaveSoundReels(sd->SavedSoundReels);

	} else {
		// Tinsel 1 specific data save
		SaveDeadPolys(sd->SavedDeadPolys);
		CurrentMidiFacts(&sd->SavedMidi, &sd->SavedLoop);
	}

	g_ASceneIsSaved = true;
}

/**
 * Initiate restoration of the saved scene.
 * @param sd			Pointer to the scene data
 * @param bFadeOut		Flag to perform a fade out
 */
void DoRestoreScene(SAVED_DATA *sd, bool bFadeOut) {
	g_rsd = sd;

	if (bFadeOut)
		g_RestoreSceneCount = RS_COUNT + COUNTOUT_COUNT;	// Set restore scene count
	else
		g_RestoreSceneCount = RS_COUNT;	// Set restore scene count
}

void InitializeSaveScenes() {
	if (g_ssData == NULL) {
		g_ssData = (SAVED_DATA *)calloc(MAX_NEST, sizeof(SAVED_DATA));
		if (g_ssData == NULL) {
			error("Cannot allocate memory for scene changes");
		}
	} else {
		// Re-initialize - no scenes saved
		g_savedSceneCount = 0;
	}
}

void FreeSaveScenes() {
	free(g_ssData);
	g_ssData = NULL;
}

/**
 * Checks that all non-moving actors are playing the same reel as when
 * the scene was saved.
 * Also 'stand' all the moving actors at their saved positions.
 */
void sortActors(SAVED_DATA *sd) {
	assert(!TinselV2);
	for (int i = 0; i < sd->NumSavedActors; i++) {
		ActorsLife(sd->SavedActorInfo[i].actorID, sd->SavedActorInfo[i].bAlive);

		// Should be playing the same reel.
		if (sd->SavedActorInfo[i].presFilm != 0) {
			if (!actorAlive(sd->SavedActorInfo[i].actorID))
				continue;

			RestoreActorReels(sd->SavedActorInfo[i].presFilm, sd->SavedActorInfo[i].presRnum, sd->SavedActorInfo[i].zFactor,
					sd->SavedActorInfo[i].presPlayX, sd->SavedActorInfo[i].presPlayY);
		}
	}

	RestoreAuxScales(sd->SavedMoverInfo);
	for (int i = 0; i < MAX_MOVERS; i++) {
		if (sd->SavedMoverInfo[i].bActive)
			Stand(Common::nullContext, sd->SavedMoverInfo[i].actorID, sd->SavedMoverInfo[i].objX,
				sd->SavedMoverInfo[i].objY, sd->SavedMoverInfo[i].hLastfilm);
	}
}

/**
 * Stand all the moving actors at their saved positions.
 * Not called from the foreground.
 */
static void SortMAProcess(CORO_PARAM, const void *) {
	CORO_BEGIN_CONTEXT;
		int i;
		int viaActor;
	CORO_END_CONTEXT(_ctx);


	CORO_BEGIN_CODE(_ctx);

	// Disable via actor for the stands
	_ctx->viaActor = SysVar(ISV_DIVERT_ACTOR);
	SetSysVar(ISV_DIVERT_ACTOR, 0);

	RestoreAuxScales(g_rsd->SavedMoverInfo);

	for (_ctx->i = 0; _ctx->i < MAX_MOVERS; _ctx->i++) {
		if (g_rsd->SavedMoverInfo[_ctx->i].bActive) {
			CORO_INVOKE_ARGS(Stand, (CORO_SUBCTX, g_rsd->SavedMoverInfo[_ctx->i].actorID,
				g_rsd->SavedMoverInfo[_ctx->i].objX, g_rsd->SavedMoverInfo[_ctx->i].objY,
				g_rsd->SavedMoverInfo[_ctx->i].hLastfilm));

			if (g_rsd->SavedMoverInfo[_ctx->i].bHidden)
				HideMover(GetMover(g_rsd->SavedMoverInfo[_ctx->i].actorID));
		}

		ActorPalette(g_rsd->SavedMoverInfo[_ctx->i].actorID,
			g_rsd->SavedMoverInfo[_ctx->i].startColor, g_rsd->SavedMoverInfo[_ctx->i].paletteLength);

		if (g_rsd->SavedMoverInfo[_ctx->i].brightness != BOGUS_BRIGHTNESS)
			ActorBrightness(g_rsd->SavedMoverInfo[_ctx->i].actorID, g_rsd->SavedMoverInfo[_ctx->i].brightness);
	}

	// Restore via actor
	SetSysVar(ISV_DIVERT_ACTOR, _ctx->viaActor);

	g_bNotDoneYet = false;

	CORO_END_CODE;
}


//---------------------------------------------------------------------------

void ResumeInterprets() {
	// Master script only affected on restore game, not restore scene
	if (!TinselV2 && (g_rsd == &g_sgData)) {
		CoroScheduler.killMatchingProcess(PID_MASTER_SCR, -1);
		FreeMasterInterpretContext();
	}

	for (int i = 0; i < NUM_INTERPRET; i++) {
		switch (g_rsd->SavedICInfo[i].GSort) {
		case GS_NONE:
			break;

		case GS_INVENTORY:
			if (g_rsd->SavedICInfo[i].event != POINTED) {
				RestoreProcess(&g_rsd->SavedICInfo[i]);
			}
			break;

		case GS_MASTER:
			// Master script only affected on restore game, not restore scene
			if (g_rsd == &g_sgData)
				RestoreMasterProcess(&g_rsd->SavedICInfo[i]);
			break;

		case GS_PROCESS:
			// Tinsel 2 process
			RestoreSceneProcess(&g_rsd->SavedICInfo[i]);
			break;

		case GS_GPROCESS:
			// Tinsel 2 Global processes only affected on restore game, not restore scene
			if (g_rsd == &g_sgData)
				RestoreGlobalProcess(&g_rsd->SavedICInfo[i]);
			break;

		case GS_ACTOR:
			if (TinselV2)
				RestoreProcess(&g_rsd->SavedICInfo[i]);
			else
				RestoreActorProcess(g_rsd->SavedICInfo[i].idActor, &g_rsd->SavedICInfo[i], g_rsd == &g_sgData);
			break;

		case GS_POLYGON:
		case GS_SCENE:
			RestoreProcess(&g_rsd->SavedICInfo[i]);
			break;

		default:
			warning("Unhandled GSort in ResumeInterprets");
		}
	}
}

/**
 * Do restore scene
 * @param n			Id
 */
static int DoRestoreSceneFrame(SAVED_DATA *sd, int n) {
	switch (n) {
	case RS_COUNT + COUNTOUT_COUNT:
		// Trigger pre-load and fade and start countdown
		FadeOutFast();
		break;

	case RS_COUNT:
		_vm->_sound->stopAllSamples();
		ClearScreen();

		if (TinselV2) {

			// Master script only affected on restore game, not restore scene
			if (sd == &g_sgData) {
				CoroScheduler.killMatchingProcess(PID_MASTER_SCR);
				KillGlobalProcesses();
				FreeMasterInterpretContext();
			}

			RestorePolygonStuff(sd->SavedPolygonStuff);

			// Abandon temporarily if different CD
			if (sd == &g_sgData && g_restoreCD != GetCurrentCD()) {
				g_SRstate = SR_IDLE;

				EndScene();
				SetNextCD(g_restoreCD);
				CDChangeForRestore(g_restoreCD);

				return 0;
			}
		} else {
			RestoreDeadPolys(sd->SavedDeadPolys);
		}

		// Start up the scene
		StartNewScene(sd->SavedSceneHandle, NO_ENTRY_NUM);

		SetDoFadeIn(!g_bNoFade);
		g_bNoFade = false;
		StartupBackground(Common::nullContext, sd->SavedBgroundHandle);

		if (TinselV2) {
			Offset(EX_USEXY, sd->SavedLoffset, sd->SavedToffset);
		} else {
			KillScroll();
			PlayfieldSetPos(FIELD_WORLD, sd->SavedLoffset, sd->SavedToffset);
			SetNoBlocking(sd->SavedNoBlocking);
		}

		RestoreNoScrollData(&sd->SavedNoScrollData);

		if (TinselV2) {
			// create process to sort out the moving actors
			CoroScheduler.createProcess(PID_MOVER, SortMAProcess, NULL, 0);
			g_bNotDoneYet = true;

			RestoreActorZ(sd->savedActorZ);
			RestoreZpositions(sd->zPositions);
			RestoreSysVars(sd->SavedSystemVars);
			RestoreActors(sd->NumSavedActors, sd->SavedActorInfo);
			RestoreSoundReels(sd->SavedSoundReels);
			return 1;
		}

		sortActors(sd);
		break;

	case 2:
		break;

	case 1:
		if (TinselV2) {
			if (g_bNotDoneYet)
				return n;

			if (sd == &g_sgData)
				HoldItem(g_thingHeld, true);
			if (sd->bTinselDim)
				_vm->_pcmMusic->dim(true);
			_vm->_pcmMusic->restoreThatTune(sd->SavedTune);
			ScrollFocus(sd->SavedScrollFocus);
		} else {
			RestoreMidiFacts(sd->SavedMidi, sd->SavedLoop);
		}

		if (sd->SavedControl)
			ControlOn();	// Control was on
		ResumeInterprets();
		break;

	default:
		break;
	}

	return n - 1;
}

/**
 * Restore game
 * @param num			num
 */
void RestoreGame(int num) {
	KillInventory();

	RequestRestoreGame(num, &g_sgData, &g_savedSceneCount, g_ssData);

	// Actual restoring is performed by ProcessSRQueue
}

/**
 * Save game
 * @param name			Name of savegame
 * @param desc			Description of savegame
 */
void SaveGame(char *name, char *desc) {
	// Get current scene data
	DoSaveScene(&g_sgData);

	RequestSaveGame(name, desc, &g_sgData, &g_savedSceneCount, g_ssData);

	// Actual saving is performed by ProcessSRQueue
}


//---------------------------------------------------------------------------------

bool IsRestoringScene() {
	if (g_RestoreSceneCount) {
		g_RestoreSceneCount = DoRestoreSceneFrame(g_rsd, g_RestoreSceneCount);
	}

	return g_RestoreSceneCount ? true : false;
}

/**
 * Restores Scene
 */
void TinselRestoreScene(bool bFade) {
	// only called by restore_scene PCODE
	if (g_RestoreSceneCount == 0) {
		assert(g_savedSceneCount >= 1); // No saved scene to restore

		if (g_ASceneIsSaved)
			DoRestoreScene(&g_ssData[--g_savedSceneCount], bFade);
		if (!bFade)
			g_bNoFade = true;
	}
}

/**
 * Please Save Scene
 */
void TinselSaveScene(CORO_PARAM) {
	// only called by save_scene PCODE
	CORO_BEGIN_CONTEXT;
	CORO_END_CONTEXT(_ctx);

	CORO_BEGIN_CODE(_ctx);

	assert(g_savedSceneCount < MAX_NEST); // nesting limit reached

	// Don't save the same thing multiple times!
	// FIXME/TODO: Maybe this can be changed to an assert?
	if (g_savedSceneCount && g_ssData[g_savedSceneCount-1].SavedSceneHandle == GetSceneHandle())
		CORO_KILL_SELF();

	DoSaveScene(&g_ssData[g_savedSceneCount++]);

	CORO_END_CODE;
}

} // End of namespace Tinsel