/* 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 */

/*
 * Module notes:
 *
 * o The script file format is as follows.  Lines beginning '#' are comments
 *   and empty lines are ignored, otherwise the file is composed of sections.
 *   The first section line is one that starts with either '>' or '~'.  This
 *   is the next command.  The following lines, up to the next '>' or '~'
 *   section start, are concatenated into the expectation for the command.
 *   Expectations are glob patterns.  Commands starting with '>' are sent to
 *   the game; those starting with '~' are sent to the SCARE debugger.  Before
 *   the game is running, debugger commands are valid.  The first non-debugger
 *   command starts the game running.  An empty debugger command ('~') that
 *   follows any introductory debugger commands both starts the game and sets
 *   an expectation for the game's introductory text.  After the game has
 *   completed (or quit), only debugger commands are valid; others are ignored.
 *
 * o The script file structure is intentionally simple, but might be too
 *   simple for some purposes.
 */

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

#include "scare.h"
#include "sxprotos.h"


/* Assorted definitions and constants. */
static const sc_int LINE_BUFFER_SIZE = 256;
static const sc_char NUL = '\0';
static const sc_char SCRIPT_COMMENT = '#';
static const sc_char GAME_COMMAND = '>';
static const sc_char DEBUG_COMMAND = '~';

/* Verbosity, and references to the game and script being processed. */
static sc_bool scr_is_verbose = FALSE;
static sc_game scr_game = NULL;
static sx_script scr_script = NULL;

/* Script line number, and count of errors registered for the script. */
static sc_int scr_line_number = 0;
static sc_int scr_errors = 0;

/*
 * Current expected output, and game accumulated output, used by the
 * expectation checking function.
 */
static sc_char *scr_expectation = NULL;
static sc_char *scr_game_output = NULL;


/*
 * scr_set_verbose()
 *
 * Set error reporting for expectation errors detected in the script.
 */
void
scr_set_verbose (sc_bool flag)
{
  scr_is_verbose = flag;
}


/*
 * scr_test_message()
 * scr_test_failed()
 *
 * Simple common message and test case failure handling functions.  The second
 * is used by the serialization helper, so is not static.
 */
static void
scr_test_message (const sc_char *format, const sc_char *string)
{
  if (scr_is_verbose)
    {
      sx_trace ("--- ");
      sx_trace (format, string);
      sx_trace ("\n");
    }
}

void
scr_test_failed (const sc_char *format, const sc_char *string)
{
  assert (format && string);

  if (scr_is_verbose)
    {
      if (scr_line_number > 0)
        sx_trace ("--- Near line %ld: ", scr_line_number);
      else
        sx_trace ("--- ");
      sx_trace (format, string);
      sx_trace ("\n");
    }
  scr_errors++;
}


/*
 * scr_is_line_type()
 * scr_is_line_comment_or_empty()
 * scr_is_line_game_command()
 * scr_is_line_debug_command()
 * scr_is_line_command()
 * scr_is_line_empty_debug_command()
 *
 * Line classifiers, return TRUE if line has the given type.
 */
static sc_bool
scr_is_line_type (const sc_char *line, sc_char type)
{
  return line[0] == type;
}

static sc_bool
scr_is_line_comment_or_empty (const sc_char *line)
{
  return scr_is_line_type (line, SCRIPT_COMMENT)
         || strspn (line, "\t\n\v\f\r ") == strlen (line);
}

static sc_bool
scr_is_line_game_command (const sc_char *line)
{
  return scr_is_line_type (line, GAME_COMMAND);
}

static sc_bool
scr_is_line_debug_command (const sc_char *line)
{
  return scr_is_line_type (line, DEBUG_COMMAND);
}

static sc_bool
scr_is_line_command (const sc_char *line)
{
  return scr_is_line_game_command (line) || scr_is_line_debug_command (line);
}

static sc_bool
scr_is_line_empty_debug_command (const sc_char *line)
{
  return scr_is_line_type (line, DEBUG_COMMAND) && line[1] == NUL;
}


/* Script location, a pair holding the file location and the line number. */
typedef struct
{
  fpos_t position;
  sc_int line_number;
} sx_scr_location_t;
typedef sx_scr_location_t *sx_scr_locationref_t;

/*
 * scr_save_location()
 * scr_restore_location()
 *
 * Save and restore the script location in the given structure.
 */
static void
scr_save_location (sx_script script, sx_scr_locationref_t location)
{
  fgetpos (script, &location->position);
  location->line_number = scr_line_number;
}

static void
scr_restore_location (sx_script script, sx_scr_locationref_t location)
{
  fsetpos (script, &location->position);
  scr_line_number = location->line_number;
}


/*
 * scr_get_next_line()
 *
 * Helper for scr_get_next_section().  Returns the next non-comment, non-empty
 * line from the script.  Returns NULL if no more lines, or on file error.  The
 * return string is allocated, and it's the caller's responsibility to free it.
 */
static sc_char *
scr_get_next_line (sx_script script)
{
  sc_char *buffer, *line = NULL;

  /* Allocate a buffer for line reads. */
  buffer = sx_malloc (LINE_BUFFER_SIZE);

  /* Read until a significant line is found, or end of file or error. */
  while (fgets (buffer, LINE_BUFFER_SIZE, script))
    {
      scr_line_number++;
      if (!scr_is_line_comment_or_empty (buffer))
        {
          line = buffer;
          break;
        }
    }

  /* If no significant line read, free the read buffer. */
  if (!line)
    sx_free (buffer);

  return line;
}


/*
 * scr_concatenate()
 *
 * Helper for scr_get_next_section().  Builds a string formed by concatenating
 * the second argument to the first.  If the first is NULL, acts as strdup()
 * instead.
 */
static sc_char *
scr_concatenate (sc_char *string, const sc_char *buffer)
{
  /* If string is not null, concatenate buffer, otherwise duplicate. */
  if (string)
    {
      string = sx_realloc (string,
                           strlen (string) + 1 + strlen (buffer) + 1);
      strcat (string, " ");
      strcat (string, buffer);
    }
  else
    {
      string = sx_malloc (strlen (buffer) + 1);
      strcpy (string, buffer);
    }

  return string;
}


/*
 * scr_get_next_section()
 *
 * Retrieve the next command and any expectation from the script file.
 * Returns TRUE if a line is returned, FALSE at end-of-file.  Expectation may
 * be NULL if this paragraph doesn't have one; command may not be (if TRUE is
 * returned).  Command and expectation are allocated, and the caller needs to
 * free them.
 */
static sc_bool
scr_get_next_section (sx_script script,
                      sc_char **command, sc_char **expectation)
{
  sc_char *line, *first_line, *other_lines;
  sx_scr_location_t location;

  /* Clear initial line accumulation. */
  first_line = other_lines = NULL;

  /* Read the next significant line from the script. */
  scr_save_location (script, &location);
  line = scr_get_next_line (script);
  while (line)
    {
      /* If already a first line, this is other lines or section end. */
      if (first_line)
        {
          /*
           * If we found the start of the next section, reset the script
           * location that saved on the line read, and we're done.
           */
          if (scr_is_line_command (line))
            {
              scr_restore_location (script, &location);
              sx_free (line);
              break;
            }
          else
            other_lines = scr_concatenate (other_lines, line);
        }
      else
        first_line = scr_concatenate (first_line, line);

      sx_free (line);

      /* Read the next significant line from the script. */
      scr_save_location (script, &location);
      line = scr_get_next_line (script);
    }

  /* Clean up and return nothing on file error. */
  if (ferror (script))
    {
      scr_test_failed ("Script error: Failed reading script input file", "");
      sx_free (first_line);
      sx_free (other_lines);
      return FALSE;
    }

  /* Return the command and the matching expectation string, if any. */
  if (first_line)
    {
      *command = sx_normalize_string (first_line);
      *expectation = other_lines ? sx_normalize_string (other_lines) : NULL;
      return TRUE;
    }

  /* End of file, no command section read. */
  return FALSE;
}


/*
 * scr_expect()
 * scr_verify_expectation()
 *
 * Set an expectation, and compare the expectation, if any, with the
 * accumulated game output, using glob matching.  scr_verify_expectation()
 * increments the error count if the expectation isn't met, and reports the
 * error if required.  It then frees both the expectation and accumulated
 * input.
 */
static void
scr_expect (sc_char *expectation)
{
  /*
   * Save the expectation, and set up collection of game output if needed.
   * And if not needed, ensure expectation and game output are cleared.
   */
  if (expectation)
    {
      scr_expectation = sx_malloc (strlen (expectation) + 1);
      strcpy (scr_expectation, expectation);
      scr_game_output = sx_malloc (1);
      strcpy (scr_game_output, "");
    }
  else
    {
      sx_free (scr_expectation);
      scr_expectation = NULL;
      sx_free (scr_game_output);
      scr_game_output = NULL;
    }
}

static void
scr_verify_expectation (void)
{
  /* Compare expected with actual, and handle any error detected. */
  if (scr_expectation && scr_game_output)
    {
      scr_game_output = sx_normalize_string (scr_game_output);
      if (!glob_match (scr_expectation, scr_game_output))
        {
          scr_test_failed ("Expectation error:", "");
          scr_test_message ("  Expected: \"%s\"", scr_expectation);
          scr_test_message ("  Received: \"%s\"", scr_game_output);
        }
    }

  /* Dispose of the expectation and accumulated game output. */
  sx_free (scr_expectation);
  scr_expectation = NULL;
  sx_free (scr_game_output);
  scr_game_output = NULL;
}


/*
 * scr_execute_debugger_command()
 *
 * Convenience interface for immediate execution of debugger commands.  This
 * function directly calls the debugger interface, and because it's immediate,
 * can also verify the expectation before returning to the caller.
 *
 * Also, it turns on the game debugger, and it's the caller's responsibility
 * to turn it off when it's no longer needed.
 */
static void
scr_execute_debugger_command (const sc_char *command, sc_char *expectation)
{
  sc_bool status;

  /* Set up the expectation. */
  scr_expect (expectation);

  /*
   * Execute the command via the debugger interface.  The "+1" on command
   * skips the leading '~' read in from the game script.
   */
  sc_set_game_debugger_enabled (scr_game, TRUE);
  status = sc_run_game_debugger_command (scr_game, command + 1);

  if (!status)
    {
      scr_test_failed ("Script error:"
                       " Debug command \"%s\" is not valid", command);
    }

  /* Check expectations immediately. */
  scr_verify_expectation ();
}


/*
 * scr_read_line_callback()
 *
 * Check any expectations set for the last line.  Consult the script for the
 * next line to feed to the game, and any expectation for the game output
 * for that line.  If there is an expectation, save it and set scr_game_output
 * to "" so that accumulation begins.  Then pass the next line of data back
 * to the game.
 */
static sc_bool
scr_read_line_callback (sc_char *buffer, sc_int length)
{
  sc_char *command, *expectation;
  assert (buffer && length > 0);

  /* Check pending expectation, and clear settings for the next line. */
  scr_verify_expectation ();

  /* Get the next line-expectation pair from the script stream. */
  if (scr_get_next_section (scr_script, &command, &expectation))
    {
      if (scr_is_line_debug_command (command))
        {
          /* The debugger persists where debug commands are adjacent. */
          scr_execute_debugger_command (command, expectation);
          sx_free (command);
          sx_free (expectation);

          /*
           * Returning FALSE here causes the game to re-prompt.  We could
           * loop (or tail recurse) ourselves, but returning is simpler.
           */
          return FALSE;
        }
      else
        sc_set_game_debugger_enabled (scr_game, FALSE);

      if (scr_is_line_game_command (command))
        {
          /* Set up the expectation. */
          scr_expect (expectation);

          /* Copy out the line to the return buffer, and free the line. */
          strncpy (buffer, command + 1, length);
          buffer[length - 1] = NUL;
          sx_free (command);
          sx_free (expectation);
          return TRUE;
        }

      /* Neither a '~' nor a '>' command. */
      scr_test_failed ("Script error:"
                       " Command \"%s\" is not valid, ignored", command);
      sx_free (command);
      sx_free (expectation);
      return FALSE;
    }

  /* Ensure the game debugger is off after this section. */
  sc_set_game_debugger_enabled (scr_game, FALSE);

  /*
   * We reached the end of the script without finding a "quit" command.
   * Supply one here, then.  In the unlikely even that this does not quit
   * the game, we'll iterate on this.
   */
  assert (length > 4);
  strcpy (buffer, "quit");
  return TRUE;
}


/*
 * scr_print_string_callback()
 *
 * Handler function for game output.  Accumulates strings received from the
 * game into scr_game_output, unless no expectation is set, in which case
 * the current game output will be NULL, and we can simply save the effort.
 */
static void
scr_print_string_callback (const sc_char *string)
{
  assert (string);

  if (scr_game_output)
    {
      scr_game_output = sx_realloc (scr_game_output,
                                    strlen (scr_game_output)
                                    + strlen (string) + 1);
      strcat (scr_game_output, string);
    }
}


/*
 * scr_start_script()
 *
 * Set up game monitoring so that each request for a line from the game
 * enters this module.  For each request, we grab the next "send" and
 * "expect" pair from the script, satisfy the request with the send data,
 * and match against the expectations on next request or on finalization.
 */
void
scr_start_script (sc_game game, sx_script script)
{
  sc_char *command, *expectation;
  sx_scr_location_t location;
  assert (game && script);

  /* Save the game and stream, and clear the line number and errors count. */
  assert (!scr_game && !scr_script);
  scr_game = game;
  scr_script = script;
  scr_line_number = 0;
  scr_errors = 0;

  /* Set up our callback functions to catch game i/o. */
  stub_attach_handlers (scr_read_line_callback, scr_print_string_callback,
                        file_open_file_callback, file_read_file_callback,
                        file_write_file_callback, file_close_file_callback);

  /*
   * Handle any initial debugging commands, terminating on either a non-
   * debugging one or an expectation for the game intro.
   */
  rewind (scr_script);
  scr_save_location (scr_script, &location);
  while (scr_get_next_section (scr_script, &command, &expectation))
    {
      if (scr_is_line_debug_command (command))
        {
          if (scr_is_line_empty_debug_command (command))
            {
              /* It's an intro expectation - set and break loop. */
              scr_expect (expectation);
              sx_free (command);
              sx_free (expectation);
              break;
            }
          else
            {
              /* It's a full debug command - execute it as one. */
              scr_execute_debugger_command (command, expectation);
              sx_free (command);
              sx_free (expectation);
            }
        }
      else
        {
          /*
           * It's an ordinary section - rewind so that it's the first one
           * handled in the callback, and break loop.
           */
          scr_restore_location (scr_script, &location);
          sx_free (command);
          sx_free (expectation);
          break;
        }

      /* Note script position before reading the next section. */
      scr_save_location (scr_script, &location);
    }

  /* Ensure the game debugger is off after this section. */
  sc_set_game_debugger_enabled (scr_game, FALSE);
}


/*
 * scr_finalize_script()
 *
 * Match any final received string against a possible expectation, and then
 * clear local records of the game, stream, and error count.  Returns the
 * count of errors detected during the script.
 */
sc_int
scr_finalize_script (void)
{
  sc_char *command, *expectation;
  sc_int errors;

  /* Check pending expectation, and clear settings. */
  scr_verify_expectation ();

  /* Drain the remainder of the script, ignoring non-debugging commands. */
  while (scr_get_next_section (scr_script, &command, &expectation))
    {
      if (scr_is_line_debug_command (command))
        {
          scr_execute_debugger_command (command, expectation);
          sx_free (command);
          sx_free (expectation);
        }
      else
        {
          /* Complain about script entries ignored because the game ended. */
          scr_test_failed ("Script error:"
                           " Game completed, command \"%s\" ignored", command);
          sx_free (command);
          sx_free (expectation);
        }
    }

  /* Ensure the game debugger is off after this section. */
  sc_set_game_debugger_enabled (scr_game, FALSE);

  /*
   * Remove our callback functions from the stubs, and "close" any retained
   * stream data from game save/load tests.
   */
  stub_detach_handlers ();
  file_cleanup ();

  /* Clear local records of game stream, line number, and errors count. */
  errors = scr_errors;
  scr_game = NULL;
  scr_script = NULL;
  scr_line_number = 0;
  scr_errors = 0;

  return errors;
}
