/* vi: set ts=2 shiftwidth=2 expandtab:
 *
 * Copyright (C) 2003-2007  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., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 */

/*
 * Module notes:
 *
 * o ...
 */

#include <assert.h>
#include <stddef.h>

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


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


/*
 * npc_in_room()
 *
 * Return TRUE if a given NPC is currently in a given room.
 */
sc_bool
npc_in_room (sc_gameref_t game, sc_int npc, sc_int room)
{
  if (npc_trace)
    {
      sc_trace ("NPC: checking NPC %ld in room %ld (NPC is in %ld)\n",
                npc, room, gs_npc_location (game, npc));
    }

  return gs_npc_location (game, npc) - 1 == room;
}


/*
 * npc_count_in_room()
 *
 * Return the count of characters in the room, including the player.
 */
sc_int
npc_count_in_room (sc_gameref_t game, sc_int room)
{
  sc_int count, npc;

  /* Start with the player. */
  count = (gs_playerroom (game) == room) ? 1 : 0;

  /* Total up other NPCs inhabiting the room. */
  for (npc = 0; npc < gs_npc_count (game); npc++)
    {
      if (gs_npc_location (game, npc) - 1 == room)
        count++;
    }
  return count;
}


/*
 * npc_meet_npcs_common()
 * npc_meet_any_npcs()
 * npc_meet_specific_npc()
 * npc_meet_any_objects()
 *
 * Handle meeting characters and objects while walking.  For characters, we
 * can restrict reports to just meetings for that character.
 */
static void
npc_meet_npcs_common (sc_gameref_t game,
                      sc_int npc, sc_int walk, sc_int other_npc)
{
  const sc_prop_setref_t bundle = gs_get_bundle (game);
  sc_vartype_t vt_key[5];
  sc_int room, chartask;

  /* Get the current NPC room, and set invariant key parts. */
  room = gs_npc_location (game, npc) - 1;
  vt_key[0].string = "NPCs";
  vt_key[1].integer = npc;
  vt_key[2].string = "Walks";
  vt_key[3].integer = walk;

  /* Handle meeting characters. */
  vt_key[4].string = "CharTask";
  chartask = prop_get_integer (bundle, "I<-sisis", vt_key);
  if (chartask != 0)
    {
      sc_int meetchar;

      /*
       * Run meetchar task if appropriate.  If other_npc is passed in, we
       * restrict task running to only that NPC or to the player.
       */
      vt_key[4].string = "MeetChar";
      meetchar = prop_get_integer (bundle, "I<-sisis", vt_key);
      if ((meetchar == 0 && room == gs_playerroom (game))
          || (meetchar > 0
              && (other_npc == -1 || other_npc == meetchar - 1)
              && room == gs_npc_location (game, meetchar - 1) - 1))
        {
          if (task_can_run_task (game, chartask - 1))
            task_run_task (game, chartask - 1, TRUE);
        }
    }
}

static void
npc_meet_any_npcs (sc_gameref_t game, sc_int npc, sc_int walk)
{
  npc_meet_npcs_common (game, npc, walk, -1);
}

static void
npc_meet_specific_npc (sc_gameref_t game,
                       sc_int npc, sc_int walk, sc_int other_npc)
{
  npc_meet_npcs_common (game, npc, walk, other_npc);
}

static void
npc_meet_any_objects (sc_gameref_t game, sc_int npc, sc_int walk)
{
  const sc_prop_setref_t bundle = gs_get_bundle (game);
  sc_vartype_t vt_key[5];
  sc_int room, objecttask;

  /* Get the current NPC room, and set invariant key parts. */
  room = gs_npc_location (game, npc) - 1;
  vt_key[0].string = "NPCs";
  vt_key[1].integer = npc;
  vt_key[2].string = "Walks";
  vt_key[3].integer = walk;

  /* Handle meeting objects. */
  vt_key[4].string = "ObjectTask";
  objecttask = prop_get_integer (bundle, "I<-sisis", vt_key);
  if (objecttask != 0)
    {
      sc_int meetobject;

      /* Run meetobject task if appropriate. */
      vt_key[4].string = "MeetObject";
      meetobject = prop_get_integer (bundle, "I<-sisis", vt_key);
      if (meetobject > 0 && obj_directly_in_room (game, meetobject - 1, room))
        {
          if (task_can_run_task (game, objecttask - 1))
            task_run_task (game, objecttask - 1, TRUE);
        }
    }
}


/*
 * npc_start_npc_walk()
 *
 * Start the given walk for the given NPC.
 */
void
npc_start_npc_walk (sc_gameref_t game, sc_int npc, sc_int walk)
{
  const sc_prop_setref_t bundle = gs_get_bundle (game);
  sc_vartype_t vt_key[6];
  sc_int movetime;

  /* Retrieve movetime 0 for the NPC walk. */
  vt_key[0].string = "NPCs";
  vt_key[1].integer = npc;
  vt_key[2].string = "Walks";
  vt_key[3].integer = walk;
  vt_key[4].string = "MoveTimes";
  vt_key[5].integer = 0;
  movetime = prop_get_integer (bundle, "I<-sisisi", vt_key) + 1;

  /* Set up walkstep, and handle possibly meeting characters and objects. */
  gs_set_npc_walkstep (game, npc, walk, movetime);
  npc_meet_any_npcs (game, npc, walk);
  npc_meet_any_objects (game, npc, walk);
}


/*
 * npc_turn_update()
 * npc_setup_initial()
 *
 * Set initial values for NPC states, and update on turns.
 */
void
npc_turn_update (sc_gameref_t game)
{
  sc_int index_;

  /* Set current values for NPC seen states. */
  for (index_ = 0; index_ < gs_npc_count (game); index_++)
    {
      if (npc_in_room (game, index_, gs_playerroom (game)))
        gs_set_npc_seen (game, index_, TRUE);
    }
}

void
npc_setup_initial (sc_gameref_t game)
{
  const sc_prop_setref_t bundle = gs_get_bundle (game);
  sc_vartype_t vt_key[5];
  sc_int index_;

  /* Start any walks that do not depend on a StartTask */
  vt_key[0].string = "NPCs";
  for (index_ = 0; index_ < gs_npc_count (game); index_++)
    {
      sc_int walk;

      /* Set up invariant parts of the properties key. */
      vt_key[1].integer = index_;
      vt_key[2].string = "Walks";

      /* Process each walk, starting at the last and working backwards. */
      for (walk = gs_npc_walkstep_count (game, index_) - 1; walk >= 0; walk--)
        {
          sc_int starttask;

          /* If StartTask is zero, start walk at game start. */
          vt_key[3].integer = walk;
          vt_key[4].string = "StartTask";
          starttask = prop_get_integer (bundle, "I<-sisis", vt_key);
          if (starttask == 0)
            npc_start_npc_walk (game, index_, walk);
        }
    }

  /* Update seen flags for initial states. */
  npc_turn_update (game);
}


/*
 * npc_room_in_roomgroup()
 *
 * Return TRUE if a given room is in a given group.
 */
static sc_bool
npc_room_in_roomgroup (sc_gameref_t game,
                       sc_int room, sc_int group)
{
  const sc_prop_setref_t bundle = gs_get_bundle (game);
  sc_vartype_t vt_key[4];
  sc_int member;

  /* Check roomgroup membership. */
  vt_key[0].string = "RoomGroups";
  vt_key[1].integer = group;
  vt_key[2].string = "List";
  vt_key[3].integer = room;
  member = prop_get_integer (bundle, "I<-sisi", vt_key);
  return member != 0;
}


/* List of direction names, for printing entry/exit messages. */
static const sc_char *const DIRNAMES_4[] = {
  "the north", "the east", "the south", "the west", "above", "below",
  "inside", "outside",
  NULL
};
static const sc_char *const DIRNAMES_8[] = {
  "the north", "the east", "the south", "the west", "above", "below",
  "inside", "outside",
  "the north-east", "the south-east", "the south-west", "the north-west",
  NULL
};

/*
 * npc_random_adjacent_roomgroup_member()
 *
 * Return a random member of group adjacent to given room.
 */
static sc_int
npc_random_adjacent_roomgroup_member (sc_gameref_t game,
                                      sc_int room, sc_int group)
{
  const sc_prop_setref_t bundle = gs_get_bundle (game);
  sc_vartype_t vt_key[5];
  sc_bool eightpointcompass;
  sc_int roomlist[12], count, length, index_;

  /* If given room is "hidden", return nothing. */
  if (room == -1)
    return -1;

  /* How many exits to consider? */
  vt_key[0].string = "Globals";
  vt_key[1].string = "EightPointCompass";
  eightpointcompass = prop_get_boolean (bundle, "B<-ss", vt_key);
  if (eightpointcompass)
    length = sizeof (DIRNAMES_8) / sizeof (DIRNAMES_8[0]) - 1;
  else
    length = sizeof (DIRNAMES_4) / sizeof (DIRNAMES_4[0]) - 1;

  /* Poll adjacent rooms. */
  vt_key[0].string = "Rooms";
  vt_key[1].integer = room;
  vt_key[2].string = "Exits";
  count = 0;
  for (index_ = 0; index_ < length; index_++)
    {
      sc_int adjacent;

      vt_key[3].integer = index_;
      vt_key[4].string = "Dest";
      adjacent = prop_get_child_count (bundle, "I<-sisis", vt_key);

      if (adjacent > 0 && npc_room_in_roomgroup (game, adjacent - 1, group))
        {
          roomlist[count] = adjacent - 1;
          count++;
        }
    }

  /* Return a random adjacent room, or -1 if nothing is adjacent. */
  return (count > 0) ? roomlist[sc_randomint (0, count - 1)] : -1;
}


/*
 * npc_announce()
 *
 * Helper for npc_tick_npc().
 */
static void
npc_announce (sc_gameref_t game, sc_int npc,
              sc_int room, sc_bool is_exit, sc_int npc_room)
{
  const sc_filterref_t filter = gs_get_filter (game);
  const sc_prop_setref_t bundle = gs_get_bundle (game);
  sc_vartype_t vt_key[5], vt_rvalue;
  const sc_char *text, *name, *const *dirnames;
  sc_int dir, dir_match;
  sc_bool eightpointcompass, showenterexit, found;

  /* If no announcement required, return immediately. */
  vt_key[0].string = "NPCs";
  vt_key[1].integer = npc;
  vt_key[2].string = "ShowEnterExit";
  showenterexit = prop_get_boolean (bundle, "B<-sis", vt_key);
  if (!showenterexit)
    return;

  /* Get exit or entry text, and NPC name. */
  vt_key[2].string = is_exit ? "ExitText" : "EnterText";
  text = prop_get_string (bundle, "S<-sis", vt_key);
  vt_key[2].string = "Name";
  name = prop_get_string (bundle, "S<-sis", vt_key);

  /* Decide on four or eight point compass names list. */
  vt_key[0].string = "Globals";
  vt_key[1].string = "EightPointCompass";
  eightpointcompass = prop_get_boolean (bundle, "B<-ss", vt_key);
  dirnames = eightpointcompass ? DIRNAMES_8 : DIRNAMES_4;

  /* Set invariant key for room exit search. */
  vt_key[0].string = "Rooms";
  vt_key[1].integer = room;
  vt_key[2].string = "Exits";

  /* Find the room exit that matches the NPC room. */
  found = FALSE;
  dir_match = 0;
  for (dir = 0; dirnames[dir]; dir++)
    {
      vt_key[3].integer = dir;
      if (prop_get (bundle, "I<-sisi", &vt_rvalue, vt_key))
        {
          sc_int dest;

          /* Get room's direction destination, and compare. */
          vt_key[4].string = "Dest";
          dest = prop_get_integer (bundle, "I<-sisis", vt_key) - 1;
          if (dest == npc_room)
            {
              dir_match = dir;
              found = TRUE;
              break;
            }
        }
    }

  /* Print NPC exit/entry details. */
  pf_buffer_character (filter, '\n');
  pf_new_sentence (filter);
  pf_buffer_string (filter, name);
  pf_buffer_character (filter, ' ');
  pf_buffer_string (filter, text);
  if (found)
    {
      pf_buffer_string (filter, is_exit ? " to " : " from ");
      pf_buffer_string (filter, dirnames[dir_match]);
    }
  pf_buffer_string (filter, ".\n");

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


/*
 * npc_tick_npc_walk()
 *
 * Helper for npc_tick_npc().  Returns TRUE if the NPC moved.
 */
static sc_bool
npc_tick_npc_walk (sc_gameref_t game, sc_int npc, sc_int walk)
{
  const sc_prop_setref_t bundle = gs_get_bundle (game);
  sc_vartype_t vt_key[6];
  sc_int roomgroups, movetimes, walkstep, start, dest, destnum;

  if (npc_trace)
    {
      sc_trace ("NPC:__ ticking NPC %ld, walk %ld: step %ld\n",
                npc, walk, gs_npc_walkstep (game, npc, walk));
    }

  /* Count roomgroups for later use. */
  vt_key[0].string = "RoomGroups";
  roomgroups = prop_get_child_count (bundle, "I<-s", vt_key);

  /* Get move times array length. */
  vt_key[0].string = "NPCs";
  vt_key[1].integer = npc;
  vt_key[2].string = "Walks";
  vt_key[3].integer = walk;
  vt_key[4].string = "MoveTimes";
  movetimes = prop_get_child_count (bundle, "I<-sisis", vt_key);

  /* Find a step to match the movetime. */
  for (walkstep = 0; walkstep < movetimes - 1; walkstep++)
    {
      sc_int movetime;

      vt_key[5].integer = walkstep + 1;
      movetime = prop_get_integer (bundle, "I<-sisisi", vt_key);
      if (gs_npc_walkstep (game, npc, walk) > movetime)
        break;
    }

  /* Sort out a destination. */
  dest = start = gs_npc_location (game, npc) - 1;

  vt_key[4].string = "Rooms";
  vt_key[5].integer = walkstep;
  destnum = prop_get_integer (bundle, "I<-sisisi", vt_key);

  if (destnum == 0)             /* Hidden. */
    dest = -1;
  else if (destnum == 1)        /* Follow player. */
    dest = gs_playerroom (game);
  else if (destnum < gs_room_count (game) + 2)
    dest = destnum - 2;         /* To room. */
  else if (destnum < gs_room_count (game) + 2 + roomgroups)
    {
      sc_int initial;

      /* For roomgroup walks, move only if walksteps has just refreshed. */
      vt_key[4].string = "MoveTimes";
      vt_key[5].integer = 0;
      initial = prop_get_integer (bundle, "I<-sisisi", vt_key);
      if (gs_npc_walkstep (game, npc, walk) == initial)
        {
          sc_int group;

          group = destnum - 2 - gs_room_count (game);
          dest = npc_random_adjacent_roomgroup_member (game, start, group);
          if (dest == -1)
            dest = lib_random_roomgroup_member (game, group);
        }
    }

  /* See if the NPC actually moved. */
  if (start != dest)
    {
      if (npc_trace)
        sc_trace ("NPC:__ walking NPC %ld moved to %ld\n", npc, dest);

      /* Move NPC to destination. */
      gs_set_npc_location (game, npc, dest + 1);

      /* Announce NPC movements, and handle meeting characters and objects. */
      if (start == gs_playerroom (game))
        npc_announce (game, npc, start, TRUE, dest);
      else if (dest == gs_playerroom (game))
        npc_announce (game, npc, dest, FALSE, start);

      /* Handle meeting characters and objects, done every step. */
      npc_meet_any_npcs (game, npc, walk);
      npc_meet_any_objects (game, npc, walk);
    }

  if (npc_trace)
    {
        sc_trace ("NPC:__ after ticking NPC %ld, walk %ld: step %ld\n",
                  npc, walk, gs_npc_walkstep (game, npc, walk));
    }

  /* Return TRUE if the NPC moved. */
  return start != dest;
}


/*
 * npc_find_active_walk()
 * npc_find_any_walk()
 *
 * Locate and return the current active walk, or the last finished walk,
 * for the NPC, or -1 if none.
 */
static sc_int
npc_find_active_walk (sc_gameref_t game, sc_int npc)
{
  const sc_prop_setref_t bundle = gs_get_bundle (game);
  sc_vartype_t vt_key[6];
  sc_int walk;

  /* Set up invariant parts of the properties key. */
  vt_key[0].string = "NPCs";
  vt_key[1].integer = npc;
  vt_key[2].string = "Walks";

  /* Process each walk, starting at the last and working backwards. */
  for (walk = gs_npc_walkstep_count (game, npc) - 1; walk >= 0; walk--)
    {
      sc_int starttask, stoppingtask;

      /* Ignore finished walks. */
      if (gs_npc_walkstep (game, npc, walk) <= 0)
        continue;

      /* Get start task. */
      vt_key[3].integer = walk;
      vt_key[4].string = "StartTask";
      starttask = prop_get_integer (bundle, "I<-sisis", vt_key) - 1;

      /*
       * Check that the starter is still complete, and if not, stop walk.
       * Then keep on looking for an active walk.
       */
      if (starttask >= 0 && !gs_task_done (game, starttask))
        {
          if (npc_trace)
            sc_trace ("NPC:_ stopping NPC %ld walk, start task undone\n", npc);

          gs_set_npc_walkstep (game, npc, walk, -1);
          continue;
        }

      /* Get stopping task. */
      vt_key[4].string = "StoppingTask";
      stoppingtask = prop_get_integer (bundle, "I<-sisis", vt_key) - 1;

      /*
       * If any stopping task has completed, ignore this walk but don't
       * actually finish it; more like an event pauser, then.
       *
       * TODO Is this right?
       */
      if (stoppingtask >= 0 && gs_task_done (game, stoppingtask))
        {
          if (npc_trace)
            sc_trace ("NPC:_ ignoring NPC %ld walk, stop task done\n", npc);

          continue;
        }

      /* This walk is active and not stopped. */
      break;
    }

  /* Return the walk, or -1 if none (loop found nothing). */
  return walk;
}

static sc_int
npc_find_any_walk (sc_gameref_t game, sc_int npc)
{
  sc_int walk;

  /* Process each walk, starting at the last and working backwards. */
  for (walk = gs_npc_walkstep_count (game, npc) - 1; walk >= 0; walk--)
    {
      /*
       * A walk with a positive step is active, -1 finished, and we'll return
       * either of these.  Zero means not yet started, and we ignore these.
       */
      if (gs_npc_walkstep (game, npc, walk) > 0
          || gs_npc_walkstep (game, npc, walk) == -1)
        break;
    }

  /* Return the walk, or -1 if none (loop found nothing). */
  return walk;
}


/*
 * npc_tick_npc()
 *
 * Move an NPC one step along current walk.  Returns TRUE if the NPC moved,
 * FALSE otherwise.
 */
static sc_bool
npc_tick_npc (sc_gameref_t game, sc_int npc)
{
  const sc_prop_setref_t bundle = gs_get_bundle (game);
  sc_int walk;
  sc_bool has_moved;

  if (npc_trace)
    sc_trace ("NPC: ticking NPC %ld\n", npc);

  /* Find active walk, and if any found, make a step along it. */
  walk = npc_find_active_walk (game, npc);
  if (walk != -1)
    {
      /* Decrement steps; if this a walk end, loop if called for. */
      gs_decrement_npc_walkstep (game, npc, walk);
      if (gs_npc_walkstep (game, npc, walk) == 0)
        {
          sc_vartype_t vt_key[6];
          sc_bool is_loop;

          /* If walk is a loop, restart it, otherwise complete it. */
          vt_key[0].string = "NPCs";
          vt_key[1].integer = npc;
          vt_key[2].string = "Walks";
          vt_key[3].integer = walk;
          vt_key[4].string = "Loop";
          is_loop = prop_get_boolean (bundle, "B<-sisis", vt_key);
          if (is_loop)
            {
              vt_key[4].string = "MoveTimes";
              vt_key[5].integer = 0;
              gs_set_npc_walkstep (game, npc, walk,
                                   prop_get_integer (bundle,
                                                     "I<-sisisi", vt_key));
            }
          else
            gs_set_npc_walkstep (game, npc, walk, -1);
        }

      /* Make a move along this walk. */
      has_moved = npc_tick_npc_walk (game, npc, walk);
    }
  else
    has_moved = FALSE;

  if (npc_trace)
    sc_trace ("NPC: after ticking NPC %ld\n", npc);

  return has_moved;
}


/*
 * npc_tick_npcs()
 *
 * Move each NPC one step along current walk.
 */
void
npc_tick_npcs (sc_gameref_t game)
{
  sc_int npc;

  /*
   * Tick each NPC, and note those that moved.  For performance and conven-
   * ience, or out of laziness, use game references to note NPCs that move.
   */
  for (npc = 0; npc < gs_npc_count (game); npc++)
    game->npc_references[npc] = npc_tick_npc (game, npc);

  /*
   * Now look for any NPCs that didn't move, but have walks and are in the
   * same room as ones that did.  For these cases, we need an extra NPC
   * meeting message attempt.
   */
  for (npc = 0; npc < gs_npc_count (game); npc++)
    {
      sc_int other_npc;

      /* Ignore this NPC if it moved, or if it didn't and has no walk. */
      if (game->npc_references[npc] || npc_find_any_walk (game, npc) == -1)
        continue;

      /* Now find other NPCs that _did_ move this turn. */
      for (other_npc = 0; other_npc < gs_npc_count (game); other_npc++)
        {
          if (game->npc_references[other_npc])
            {
              /*
               * If that NPC has an active walk, try a meeting between the
               * unmoved NPC and this NPC as if the unmoved NPC had moved to
               * the same room as the moved one.
               */
              if (npc_find_active_walk (game, other_npc) != -1)
                {
                  sc_int walk;

                  walk = npc_find_any_walk (game, npc);
                  npc_meet_specific_npc (game, npc, walk, other_npc);
                }
            }
        }
    }
}


/*
 * npc_debug_trace()
 *
 * Set NPC tracing on/off.
 */
void
npc_debug_trace (sc_bool flag)
{
  npc_trace = flag;
}
