/* 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 "glk/adrift/scare.h"
#include "glk/adrift/scprotos.h"
#include "glk/adrift/scgamest.h"

namespace Glk {
namespace Adrift {

/*
 * Module notes:
 *
 * o Event pause and resume tasks need more testing.
 */

/* Trace flag, set before running. */
static sc_bool evt_trace = FALSE;


/*
 * evt_any_task_in_state()
 *
 * Return TRUE if any task at all matches the given completion state.
 */
static sc_bool evt_any_task_in_state(sc_gameref_t game, sc_bool state) {
	sc_int task;

	/* Scan tasks for any whose completion matches input. */
	for (task = 0; task < gs_task_count(game); task++) {
		if (gs_task_done(game, task) == state)
			return TRUE;
	}

	/* No tasks matched. */
	return FALSE;
}


/*
 * evt_can_see_event()
 *
 * Return TRUE if player is in the right room for event text.
 */
sc_bool evt_can_see_event(sc_gameref_t game, sc_int event) {
	const sc_prop_setref_t bundle = gs_get_bundle(game);
	sc_vartype_t vt_key[5];
	sc_int type;

	/* Check room list for the event and return it. */
	vt_key[0].string = "Events";
	vt_key[1].integer = event;
	vt_key[2].string = "Where";
	vt_key[3].string = "Type";
	type = prop_get_integer(bundle, "I<-siss", vt_key);
	switch (type) {
	case ROOMLIST_NO_ROOMS:
		return FALSE;
	case ROOMLIST_ALL_ROOMS:
		return TRUE;

	case ROOMLIST_ONE_ROOM:
		vt_key[3].string = "Room";
		return prop_get_integer(bundle, "I<-siss", vt_key)
		       == gs_playerroom(game);

	case ROOMLIST_SOME_ROOMS:
		vt_key[3].string = "Rooms";
		vt_key[4].integer = gs_playerroom(game);
		return prop_get_boolean(bundle, "B<-sissi", vt_key);

	default:
		sc_fatal("evt_can_see_event: invalid type, %ld\n", type);
		return FALSE;
	}
}


/*
 * evt_move_object()
 *
 * Move an object from within an event.
 */
static void evt_move_object(sc_gameref_t game, sc_int object, sc_int destination) {
	/* Ignore negative values of object. */
	if (object >= 0) {
		if (evt_trace) {
			sc_trace("Event: moving object %ld to room %ld\n",
			         object, destination);
		}

		/* Move object depending on destination. */
		switch (destination) {
		case -1:               /* Hidden. */
			gs_object_make_hidden(game, object);
			break;

		case 0:                /* Held by player. */
			gs_object_player_get(game, object);
			break;

		case 1:                /* Same room as player. */
			gs_object_to_room(game, object, gs_playerroom(game));
			break;

		default:
			if (destination < gs_room_count(game) + 2)
				gs_object_to_room(game, object, destination - 2);
			else {
				sc_int roomgroup, room;

				roomgroup = destination - gs_room_count(game) - 2;
				room = lib_random_roomgroup_member(game, roomgroup);
				gs_object_to_room(game, object, room);
			}
			break;
		}

		/*
		 * If static, mark as no longer unmoved.
		 *
		 * TODO Is this the only place static objects can be moved?  And just
		 * how static is a static object if it's moveable, anyway?
		 */
		if (obj_is_static(game, object))
			gs_set_object_static_unmoved(game, object, FALSE);
	}
}


/*
 * evt_fixup_v390_v380_immediate_restart()
 *
 * Versions 3.9 and 3.8 differ from version 4.0 on immediate restart; they
 * "miss" the event start actions and move one step into the event without
 * comment.  It's arguable if this is a feature or a bug; nevertheless, we
 * can do the same thing here, though it's ugly.
 */
static sc_bool evt_fixup_v390_v380_immediate_restart(sc_gameref_t game, sc_int event) {
	const sc_prop_setref_t bundle = gs_get_bundle(game);
	sc_vartype_t vt_key[3];
	sc_int version;

	vt_key[0].string = "Version";
	version = prop_get_integer(bundle, "I<-s", vt_key);
	if (version < TAF_VERSION_400) {
		sc_int time1, time2;

		if (evt_trace)
			sc_trace("Event: applying 3.9/3.8 restart fixup\n");

		/* Set to running state. */
		gs_set_event_state(game, event, ES_RUNNING);

		/* Set up event time to be one less than a proper start. */
		vt_key[0].string = "Events";
		vt_key[1].integer = event;
		vt_key[2].string = "Time1";
		time1 = prop_get_integer(bundle, "I<-sis", vt_key);
		vt_key[2].string = "Time2";
		time2 = prop_get_integer(bundle, "I<-sis", vt_key);
		gs_set_event_time(game, event, sc_randomint(time1, time2) - 1);
	}

	/* Return TRUE if we applied the fixup. */
	return version < TAF_VERSION_400;
}


/*
 * evt_start_event()
 *
 * Change an event from WAITING to RUNNING.
 */
static void evt_start_event(sc_gameref_t game, sc_int event) {
	const sc_filterref_t filter = gs_get_filter(game);
	const sc_prop_setref_t bundle = gs_get_bundle(game);
	sc_vartype_t vt_key[4];
	sc_int time1, time2, obj1, obj1dest;

	if (evt_trace)
		sc_trace("Event: starting event %ld\n", event);

	/* If event is visible, print its start text. */
	if (evt_can_see_event(game, event)) {
		const sc_char *starttext;

		/* Get and print start text. */
		vt_key[0].string = "Events";
		vt_key[1].integer = event;
		vt_key[2].string = "StartText";
		starttext = prop_get_string(bundle, "S<-sis", vt_key);
		if (!sc_strempty(starttext)) {
			pf_buffer_string(filter, starttext);
			pf_buffer_character(filter, '\n');
		}

		/* Handle any associated resource. */
		vt_key[2].string = "Res";
		vt_key[3].integer = 0;
		res_handle_resource(game, "sisi", vt_key);
	}

	/* Move event object to destination. */
	vt_key[0].string = "Events";
	vt_key[1].integer = event;
	vt_key[2].string = "Obj1";
	obj1 = prop_get_integer(bundle, "I<-sis", vt_key) - 1;
	vt_key[2].string = "Obj1Dest";
	obj1dest = prop_get_integer(bundle, "I<-sis", vt_key) - 1;
	evt_move_object(game, obj1, obj1dest);

	/* Set the event's state and time. */
	gs_set_event_state(game, event, ES_RUNNING);

	vt_key[2].string = "Time1";
	time1 = prop_get_integer(bundle, "I<-sis", vt_key);
	vt_key[2].string = "Time2";
	time2 = prop_get_integer(bundle, "I<-sis", vt_key);
	gs_set_event_time(game, event, sc_randomint(time1, time2));

	if (evt_trace)
		sc_trace("Event: start event handling done, %ld\n", event);
}


/*
 * evt_get_starter_type()
 *
 * Return the starter type for an event.
 */
static sc_int evt_get_starter_type(sc_gameref_t game, sc_int event) {
	const sc_prop_setref_t bundle = gs_get_bundle(game);
	sc_vartype_t vt_key[3];
	sc_int startertype;

	vt_key[0].string = "Events";
	vt_key[1].integer = event;
	vt_key[2].string = "StarterType";
	startertype = prop_get_integer(bundle, "I<-sis", vt_key);

	return startertype;
}


/*
 * evt_finish_event()
 *
 * Move an event to FINISHED, or restart it.
 */
static void evt_finish_event(sc_gameref_t game, sc_int event) {
	const sc_filterref_t filter = gs_get_filter(game);
	const sc_prop_setref_t bundle = gs_get_bundle(game);
	sc_vartype_t vt_key[4];
	sc_int obj2, obj2dest, obj3, obj3dest;
	sc_int task, startertype, restarttype;
	sc_bool taskdir;

	if (evt_trace)
		sc_trace("Event: finishing event %ld\n", event);

	/* Set up invariant parts of the key. */
	vt_key[0].string = "Events";
	vt_key[1].integer = event;

	/* If event is visible, print its finish text. */
	if (evt_can_see_event(game, event)) {
		const sc_char *finishtext;

		/* Get and print finish text. */
		vt_key[2].string = "FinishText";
		finishtext = prop_get_string(bundle, "S<-sis", vt_key);
		if (!sc_strempty(finishtext)) {
			pf_buffer_string(filter, finishtext);
			pf_buffer_character(filter, '\n');
		}

		/* Handle any associated resource. */
		vt_key[2].string = "Res";
		vt_key[3].integer = 4;
		res_handle_resource(game, "sisi", vt_key);
	}

	/* Move event objects to destination. */
	vt_key[2].string = "Obj2";
	obj2 = prop_get_integer(bundle, "I<-sis", vt_key) - 1;
	vt_key[2].string = "Obj2Dest";
	obj2dest = prop_get_integer(bundle, "I<-sis", vt_key) - 1;
	evt_move_object(game, obj2, obj2dest);

	vt_key[2].string = "Obj3";
	obj3 = prop_get_integer(bundle, "I<-sis", vt_key) - 1;
	vt_key[2].string = "Obj3Dest";
	obj3dest = prop_get_integer(bundle, "I<-sis", vt_key) - 1;
	evt_move_object(game, obj3, obj3dest);

	/* See if there is an affected task. */
	vt_key[2].string = "TaskAffected";
	task = prop_get_integer(bundle, "I<-sis", vt_key) - 1;
	if (task >= 0) {
		vt_key[2].string = "TaskFinished";
		taskdir = !prop_get_boolean(bundle, "B<-sis", vt_key);
		if (task_can_run_task_directional(game, task, taskdir)) {
			if (evt_trace) {
				sc_trace("Event: event running task %ld, %s\n",
				         task, taskdir ? "forwards" : "backwards");
			}

			task_run_task(game, task, taskdir);
		} else {
			if (evt_trace)
				sc_trace("Event: event can't run task %ld\n", task);
		}
	}

	/* Handle possible restart. */
	vt_key[2].string = "RestartType";
	restarttype = prop_get_integer(bundle, "I<-sis", vt_key);
	switch (restarttype) {
	case 0:                    /* Don't restart. */
		startertype = evt_get_starter_type(game, event);
		switch (startertype) {
		case 1:                /* Immediate. */
		case 2:                /* Random delay. */
		case 3:                /* After task. */
			gs_set_event_state(game, event, ES_FINISHED);
			gs_set_event_time(game, event, 0);
			break;

		default:
			sc_fatal("evt_finish_event:"
			         " unknown value for starter type, %ld\n", startertype);
		}
		break;

	case 1:                    /* Restart immediately. */
		if (evt_fixup_v390_v380_immediate_restart(game, event))
			break;
		else
			evt_start_event(game, event);
		break;

	case 2:                    /* Restart after delay. */
		startertype = evt_get_starter_type(game, event);
		switch (startertype) {
		case 1:                /* Immediate. */
			if (evt_fixup_v390_v380_immediate_restart(game, event))
				break;
			else
				evt_start_event(game, event);
			break;

		case 2: {              /* Random delay. */
			sc_int start, end;

			gs_set_event_state(game, event, ES_WAITING);
			vt_key[2].string = "StartTime";
			start = prop_get_integer(bundle, "I<-sis", vt_key);
			vt_key[2].string = "EndTime";
			end = prop_get_integer(bundle, "I<-sis", vt_key);
			gs_set_event_time(game, event, sc_randomint(start, end));
			break;
		}

		case 3:                /* After task. */
			gs_set_event_state(game, event, ES_AWAITING);
			gs_set_event_time(game, event, 0);
			break;

		default:
			sc_fatal("evt_finish_event: unknown StarterType\n");
		}
		break;

	default:
		sc_fatal("evt_finish_event: unknown RestartType\n");
	}

	if (evt_trace)
		sc_trace("Event: finish event handling done, %ld\n", event);
}


/*
 * evt_has_starter_task()
 * evt_starter_task_is_complete()
 * evt_pauser_task_is_complete()
 * evt_resumer_task_is_complete()
 *
 * Return the status of start, pause and resume states of an event.
 */
static sc_bool evt_has_starter_task(sc_gameref_t game, sc_int event) {
	sc_int startertype;

	startertype = evt_get_starter_type(game, event);
	return startertype == 3;
}

static sc_bool evt_starter_task_is_complete(sc_gameref_t game, sc_int event) {
	const sc_prop_setref_t bundle = gs_get_bundle(game);
	sc_vartype_t vt_key[3];
	sc_int task;
	sc_bool start;

	vt_key[0].string = "Events";
	vt_key[1].integer = event;
	vt_key[2].string = "TaskNum";
	task = prop_get_integer(bundle, "I<-sis", vt_key);

	start = FALSE;
	if (task == 0) {
		if (evt_any_task_in_state(game, TRUE))
			start = TRUE;
	} else if (task > 0) {
		if (gs_task_done(game, task - 1))
			start = TRUE;
	}

	return start;
}

static sc_bool evt_pauser_task_is_complete(sc_gameref_t game, sc_int event) {
	const sc_prop_setref_t bundle = gs_get_bundle(game);
	sc_vartype_t vt_key[3];
	sc_int pausetask;
	sc_bool completed, pause;

	vt_key[0].string = "Events";
	vt_key[1].integer = event;

	vt_key[2].string = "PauseTask";
	pausetask = prop_get_integer(bundle, "I<-sis", vt_key);
	vt_key[2].string = "PauserCompleted";
	completed = !prop_get_boolean(bundle, "B<-sis", vt_key);

	pause = FALSE;
	if (pausetask == 1) {
		if (evt_any_task_in_state(game, completed))
			pause = TRUE;
	} else if (pausetask > 1) {
		if (completed == gs_task_done(game, pausetask - 2))
			pause = TRUE;
	}

	return pause;
}

static sc_bool evt_resumer_task_is_complete(sc_gameref_t game, sc_int event) {
	const sc_prop_setref_t bundle = gs_get_bundle(game);
	sc_vartype_t vt_key[3];
	sc_int resumetask;
	sc_bool completed, resume;

	vt_key[0].string = "Events";
	vt_key[1].integer = event;

	vt_key[2].string = "ResumeTask";
	resumetask = prop_get_integer(bundle, "I<-sis", vt_key);
	vt_key[2].string = "ResumerCompleted";
	completed = !prop_get_boolean(bundle, "B<-sis", vt_key);

	resume = FALSE;
	if (resumetask == 1) {
		if (evt_any_task_in_state(game, completed))
			resume = TRUE;
	} else if (resumetask > 1) {
		if (completed == gs_task_done(game, resumetask - 2))
			resume = TRUE;
	}

	return resume;
}


/*
 * evt_handle_preftime_notifications()
 *
 * Print messages and handle resources for the event where we're in mid-event
 * and getting close to some number of turns from its end.
 */
static void evt_handle_preftime_notifications(sc_gameref_t game, sc_int event) {
	const sc_filterref_t filter = gs_get_filter(game);
	const sc_prop_setref_t bundle = gs_get_bundle(game);
	sc_vartype_t vt_key[4];
	sc_int preftime1, preftime2;
	const sc_char *preftext;

	vt_key[0].string = "Events";
	vt_key[1].integer = event;

	vt_key[2].string = "PrefTime1";
	preftime1 = prop_get_integer(bundle, "I<-sis", vt_key);
	if (preftime1 == gs_event_time(game, event)) {
		vt_key[2].string = "PrefText1";
		preftext = prop_get_string(bundle, "S<-sis", vt_key);
		if (!sc_strempty(preftext)) {
			pf_buffer_string(filter, preftext);
			pf_buffer_character(filter, '\n');
		}

		vt_key[2].string = "Res";
		vt_key[3].integer = 2;
		res_handle_resource(game, "sisi", vt_key);
	}

	vt_key[2].string = "PrefTime2";
	preftime2 = prop_get_integer(bundle, "I<-sis", vt_key);
	if (preftime2 == gs_event_time(game, event)) {
		vt_key[2].string = "PrefText2";
		preftext = prop_get_string(bundle, "S<-sis", vt_key);
		if (!sc_strempty(preftext)) {
			pf_buffer_string(filter, preftext);
			pf_buffer_character(filter, '\n');
		}

		vt_key[2].string = "Res";
		vt_key[3].integer = 3;
		res_handle_resource(game, "sisi", vt_key);
	}
}


/*
 * evt_tick_event()
 *
 * Attempt to advance an event by one turn.
 */
static void evt_tick_event(sc_gameref_t game, sc_int event) {
	if (evt_trace) {
		sc_trace("Event: ticking event %ld: state %ld, time %ld\n", event,
		         gs_event_state(game, event), gs_event_time(game, event));
	}

	/* Handle call based on current event state. */
	switch (gs_event_state(game, event)) {
	case ES_WAITING: {
		if (evt_trace)
			sc_trace("Event: ticking waiting event %ld\n", event);

		/*
		 * Because we also tick an event that goes from waiting to running,
		 * events started here will tick through RUNNING too, and have their
		 * time decremented.  To get around this, so that the timer for one-
		 * shot events doesn't look one lower than it should after this
		 * transition, we need to set the initial time for events that start
		 * as soon as the game starts to one greater than that set by
		 * evt_start_time().  Here's the hack to do that; if the event starts
		 * immediately, its time will already be zero, even before decrement,
		 * which is how we tell which events to apply this hack to.
		 *
		 * TODO This seems to work, but also seems very dodgy.
		 */
		if (gs_event_time(game, event) == 0) {
			evt_start_event(game, event);

			/* If the event time was set to zero, finish immediately. */
			if (gs_event_time(game, event) <= 0)
				evt_finish_event(game, event);
			else
				gs_set_event_time(game, event, gs_event_time(game, event) + 1);
			break;
		}

		/*
		 * Decrement the event's time, and if it goes to zero, start running
		 * the event.
		 */
		gs_decrement_event_time(game, event);

		if (gs_event_time(game, event) <= 0) {
			evt_start_event(game, event);

			/* If the event time was set to zero, finish immediately. */
			if (gs_event_time(game, event) <= 0)
				evt_finish_event(game, event);
		}
	}
	break;

	case ES_RUNNING: {
		if (evt_trace)
			sc_trace("Event: ticking running event %ld\n", event);

		/*
		 * Re-check the starter task; if it's no longer completed, we need
		 * to set the event back to waiting on task.
		 */
		if (evt_has_starter_task(game, event)) {
			if (!evt_starter_task_is_complete(game, event)) {
				if (evt_trace)
					sc_trace("Event: starter task not complete\n");

				gs_set_event_state(game, event, ES_AWAITING);
				gs_set_event_time(game, event, 0);
				break;
			}
		}

		/* If the pauser has completed, but resumer not, pause this event. */
		if (evt_pauser_task_is_complete(game, event)
		        && !evt_resumer_task_is_complete(game, event)) {
			if (evt_trace)
				sc_trace("Event: pause complete\n");

			gs_set_event_state(game, event, ES_PAUSED);
			break;
		}

		/*
		 * Decrement the event's time, and print any notifications for a set
		 * number of turns from the event end.
		 */
		gs_decrement_event_time(game, event);

		if (evt_can_see_event(game, event))
			evt_handle_preftime_notifications(game, event);

		/* If the time goes to zero, finish running the event. */
		if (gs_event_time(game, event) <= 0)
			evt_finish_event(game, event);
	}
	break;

	case ES_AWAITING: {
		if (evt_trace)
			sc_trace("Event: ticking awaiting event %ld\n", event);

		/*
		 * Check the starter task.  If it's completed, start running the
		 * event.
		 */
		if (evt_starter_task_is_complete(game, event)) {
			evt_start_event(game, event);

			/* If the event time was set to zero, finish immediately. */
			if (gs_event_time(game, event) <= 0)
				evt_finish_event(game, event);
			else {
				/*
				 * If the pauser has completed, but resumer not, immediately
				 * also pause this event.
				 */
				if (evt_pauser_task_is_complete(game, event)
				        && !evt_resumer_task_is_complete(game, event)) {
					if (evt_trace)
						sc_trace("Event: pause complete, immediate pause\n");

					gs_set_event_state(game, event, ES_PAUSED);
				}
			}
		}
	}
	break;

	case ES_FINISHED: {
		if (evt_trace)
			sc_trace("Event: ticking finished event %ld\n", event);

		/*
		 * Check the starter task; if it's not completed, we need to set the
		 * event back to waiting on task.
		 *
		 * A completed event needs to go back to waiting on its task, but we
		 * don't want to set it there as soon as the event finishes.  We need
		 * to wait for the starter task to first become undone, otherwise the
		 * event just cycles endlessly, and they don't in Adrift itself.  Here
		 * is where we wait for starter tasks to become undone.
		 */
		if (evt_has_starter_task(game, event)) {
			if (!evt_starter_task_is_complete(game, event)) {
				if (evt_trace)
					sc_trace("Event: starter task not complete\n");

				gs_set_event_state(game, event, ES_AWAITING);
				gs_set_event_time(game, event, 0);
				break;
			}
		}
	}
	break;

	case ES_PAUSED: {
		if (evt_trace)
			sc_trace("Event: ticking paused event %ld\n", event);

		/* If the resumer has completed, resume this event. */
		if (evt_resumer_task_is_complete(game, event)) {
			if (evt_trace)
				sc_trace("Event: resume complete\n");

			gs_set_event_state(game, event, ES_RUNNING);
			break;
		}
	}
	break;

	default:
		sc_fatal("evt_tick: invalid event state\n");
	}

	if (evt_trace) {
		sc_trace("Event: after ticking event %ld: state %ld, time %ld\n", event,
		         gs_event_state(game, event), gs_event_time(game, event));
	}
}


/*
 * evt_tick_events()
 *
 * Attempt to advance each event by one turn.
 */
void evt_tick_events(sc_gameref_t game) {
	sc_int event;

	/*
	 * Tick all events.  If an event transitions into a running state from a
	 * paused or waiting state, tick that event again.
	 */
	for (event = 0; event < gs_event_count(game); event++) {
		sc_int prior_state, state;

		/* Note current state, and tick event forwards. */
		prior_state = gs_event_state(game, event);
		evt_tick_event(game, event);

		/*
		 * If the event went from paused or waiting to running, tick again.
		 * This looks dodgy, and probably is, but it does keep timers correct
		 * by only re-ticking events that have transitioned from non-running
		 * states to a running one, and not already-running events.  This is
		 * in effect just adding a bit of turn processing to a tick that would
		 * otherwise change state alone; a bit of laziness, in other words.
		 */
		state = gs_event_state(game, event);
		if (state == ES_RUNNING
		        && (prior_state == ES_PAUSED || prior_state == ES_WAITING))
			evt_tick_event(game, event);
	}
}


/*
 * evt_debug_trace()
 *
 * Set event tracing on/off.
 */
void evt_debug_trace(sc_bool flag) {
	evt_trace = flag;
}

} // End of namespace Adrift
} // End of namespace Glk
