/* vi: set ts=2 shiftwidth=2 expandtab:
 *
 * Copyright (C) 2003-2008  Simon Baldwin and Mark J. Tilford
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of version 2 of the GNU General Public License
 * as published by the Free Software Foundation.
 *
 * 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
 */

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

#include <assert.h>
#include <stdlib.h>

#include "scare.h"
#include "scprotos.h"
#include "scgamest.h"


/* 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;
}
