/* phantom_typist.c: starting game loading automatically
   Copyright (c) 2017 Philip Kendall

   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.

   Author contact information:

   E-mail: philip-fuse@shadowmagic.org.uk

*/

#include <string.h>
#ifdef HAVE_STRINGS_STRCASECMP
#include <strings.h>
#endif      /* #ifdef HAVE_STRINGS_STRCASECMP */

#include "config.h"

#include "libspectrum.h"

#include "compat.h"
#include "infrastructure/startup_manager.h"
#include "keyboard.h"
#include "module.h"
#include "phantom_typist.h"
#include "settings.h"
#include "timer/timer.h"

/* The various high level ways the phantom typist can type */
typedef enum phantom_typist_highlevel_mode_t {
  /* Use keyword entry */
  PHANTOM_TYPIST_HIGHLEVEL_MODE_KEYWORD,

  /* Use keystroke entry */
  PHANTOM_TYPIST_HIGHLEVEL_MODE_KEYSTROKE,

  /* Use the loader option from the menu */
  PHANTOM_TYPIST_HIGHLEVEL_MODE_MENU,

  /* Use the loader option from the menu but with an additional delay */
  PHANTOM_TYPIST_HIGHLEVEL_MODE_PLUS2A,

  /* Use the loader option from the menu but with an additional delay
     and special handling of LOAD ""CODE programs */
  PHANTOM_TYPIST_HIGHLEVEL_MODE_PLUS3
} phantom_typist_highlevel_mode_t;

/* The various strings of keypresses that the phantom typist can use */
typedef enum phantom_typist_mode_t {
  /* J, SS + P, SS + P, Enter => LOAD "" */
  PHANTOM_TYPIST_MODE_JPP,

  /* J, SS + P, SS + P, CS + SS, I, Enter => LOAD ""CODE */
  PHANTOM_TYPIST_MODE_JPPI,

  PHANTOM_TYPIST_MODE_ENTER, /* Enter only */

  /* Down, Enter, L, O, A, D, SS + P, SS + P, C, O, D, E =>
     LOAD ""CODE in BASIC */
  PHANTOM_TYPIST_MODE_DOWN_LOADPPCODE,

  /* Wait a bit and then perform the above */
  PHANTOM_TYPIST_MODE_WAIT_DOWN_LOADPPCODE,

  /* Wait a bit, Down, Enter,
     L, O, A, D, SS + P, T, SS + Z, SS + P, Enter,
     L, O, A, D, SS + P, SS + P, C, O, D, E, Enter =>
     LOAD "t" followed by LOAD ""CODE in BASIC */
  PHANTOM_TYPIST_MODE_PLUS3_CODE_BLOCK,

  /* L, O, A, D, SS + P, SS + P, Enter => LOAD "" */
  PHANTOM_TYPIST_MODE_LOADPP,

  /* L, O, A, D, SS + P, SS + P, C, O, D, E, Enter => LOAD ""CODE */
  PHANTOM_TYPIST_MODE_LOADPPCODE
} phantom_typist_mode_t;

typedef enum phantom_typist_state_t {
  /* The phantom typist is inactive */
  PHANTOM_TYPIST_STATE_INACTIVE,

  /* The phantom typist is waiting for the keyboard to be read */
  PHANTOM_TYPIST_STATE_WAITING,

  /* JPP mode */
  PHANTOM_TYPIST_STATE_LOAD,
  PHANTOM_TYPIST_STATE_QUOTE1,
  PHANTOM_TYPIST_STATE_QUOTE2,
  PHANTOM_TYPIST_STATE_ENTER,

  /* Enter mode */
  PHANTOM_TYPIST_STATE_ENTER_ONLY,
  PHANTOM_TYPIST_STATE_WAIT_AFTER_ENTER,

  /* LOAD mode - chains into the quotes of JPP mode */
  PHANTOM_TYPIST_STATE_LOAD_L,
  PHANTOM_TYPIST_STATE_LOAD_O,
  PHANTOM_TYPIST_STATE_LOAD_A,
  PHANTOM_TYPIST_STATE_LOAD_D,

  /* For producing "CODE" in keyword mode */
  PHANTOM_TYPIST_STATE_EXTENDED_MODE,
  PHANTOM_TYPIST_STATE_CODE,

  /* For producing "CODE" in single character entry mode */
  PHANTOM_TYPIST_STATE_CODE_C,
  PHANTOM_TYPIST_STATE_CODE_O,
  PHANTOM_TYPIST_STATE_CODE_D,
  PHANTOM_TYPIST_STATE_CODE_E,

  /* For selecting 128K BASIC or similar */
  PHANTOM_TYPIST_STATE_LONG_WAIT_DOWN,
  PHANTOM_TYPIST_STATE_WAIT_DOWN,
  PHANTOM_TYPIST_STATE_DOWN,
  PHANTOM_TYPIST_STATE_SELECT_BASIC,
  PHANTOM_TYPIST_STATE_WAIT,

  /* For selecting tape on a +3 machine */
  PHANTOM_TYPIST_STATE_TCOLON_T,
  PHANTOM_TYPIST_STATE_TCOLON_COLON,

  /* For typing L and O somewhat slowly */
  PHANTOM_TYPIST_STATE_SLOW_LOAD_L,
  PHANTOM_TYPIST_STATE_SLOW_LOAD_O,

} phantom_typist_state_t;

/* States for the phantom typist state machine */
struct state_info_t {
  /* The keys to be pressed in this state */
  keyboard_key_name keys_to_press[2];

  /* The function used to determine the next state */
  phantom_typist_state_t (*next_state_fn)( void );

  /* The next state to move to - used if next_state_fn is NULL */
  phantom_typist_state_t next_state;

  /* The number of frames to wait before acting in this state */
  int delay_before_state;
};

/* Next state selector functions */
static phantom_typist_state_t t_colon_or_quote( void );
static phantom_typist_state_t code_or_enter( void );
static phantom_typist_state_t next_command_or_end( void );

/* Reset function */
static void phantom_typist_reset( int hard_reset );

/* Definitions for the phantom typist's state machine */
static struct state_info_t state_info[] = {
  /* INACTIVE and WAITING - data not used */
  { { 0, 0 }, NULL, 0, 0 },
  { { 0, 0 }, NULL, 0, 0 },

  { { KEYBOARD_j, KEYBOARD_NONE }, NULL, PHANTOM_TYPIST_STATE_QUOTE1, 8 },
  { { KEYBOARD_Symbol, KEYBOARD_p }, t_colon_or_quote, 0, 0 },
  { { KEYBOARD_Symbol, KEYBOARD_p }, code_or_enter, 0, 5 },
  { { KEYBOARD_Enter, KEYBOARD_NONE }, next_command_or_end, 0, 0 },
  
  { { KEYBOARD_Enter, KEYBOARD_NONE }, NULL, PHANTOM_TYPIST_STATE_WAIT_AFTER_ENTER, 3 },
  /* This state is here to "swallow" the pause while the +3 checks for a disk */
  { { KEYBOARD_NONE, KEYBOARD_NONE }, NULL, PHANTOM_TYPIST_STATE_INACTIVE, 600 },

  { { KEYBOARD_l, KEYBOARD_NONE }, NULL, PHANTOM_TYPIST_STATE_LOAD_O, 2 },
  { { KEYBOARD_o, KEYBOARD_NONE }, NULL, PHANTOM_TYPIST_STATE_LOAD_A, 0 },
  { { KEYBOARD_a, KEYBOARD_NONE }, NULL, PHANTOM_TYPIST_STATE_LOAD_D, 5 },
  { { KEYBOARD_d, KEYBOARD_NONE }, NULL, PHANTOM_TYPIST_STATE_QUOTE1, 4 },

  { { KEYBOARD_Caps, KEYBOARD_Symbol }, NULL, PHANTOM_TYPIST_STATE_CODE, 0 },
  { { KEYBOARD_i, KEYBOARD_NONE }, NULL, PHANTOM_TYPIST_STATE_ENTER, 4 },

  { { KEYBOARD_c, KEYBOARD_NONE }, NULL, PHANTOM_TYPIST_STATE_CODE_O, 0 },
  { { KEYBOARD_o, KEYBOARD_NONE }, NULL, PHANTOM_TYPIST_STATE_CODE_D, 4 },
  { { KEYBOARD_d, KEYBOARD_NONE }, NULL, PHANTOM_TYPIST_STATE_CODE_E, 0 },
  { { KEYBOARD_e, KEYBOARD_NONE }, NULL, PHANTOM_TYPIST_STATE_ENTER, 4 },

  { { KEYBOARD_NONE, KEYBOARD_NONE }, NULL, PHANTOM_TYPIST_STATE_WAIT_DOWN, 50 },
  { { KEYBOARD_NONE, KEYBOARD_NONE }, NULL, PHANTOM_TYPIST_STATE_DOWN, 24 },
  { { KEYBOARD_Caps, KEYBOARD_6 }, NULL, PHANTOM_TYPIST_STATE_SELECT_BASIC, 4 },
  { { KEYBOARD_Enter, KEYBOARD_NONE }, NULL, PHANTOM_TYPIST_STATE_WAIT, 10 },
  { { KEYBOARD_NONE, KEYBOARD_NONE }, NULL, PHANTOM_TYPIST_STATE_LOAD_L, 28 },

  { { KEYBOARD_t, KEYBOARD_NONE }, NULL, PHANTOM_TYPIST_STATE_TCOLON_COLON, 4 },
  { { KEYBOARD_Symbol, KEYBOARD_z }, NULL, PHANTOM_TYPIST_STATE_QUOTE2, 4 },

  { { KEYBOARD_l, KEYBOARD_NONE }, NULL, PHANTOM_TYPIST_STATE_SLOW_LOAD_O, 5 },
  { { KEYBOARD_o, KEYBOARD_NONE }, NULL, PHANTOM_TYPIST_STATE_LOAD_A, 30 }
};

static phantom_typist_mode_t phantom_typist_mode;
static phantom_typist_state_t phantom_typist_state = PHANTOM_TYPIST_STATE_INACTIVE;
static phantom_typist_state_t next_phantom_typist_state = PHANTOM_TYPIST_STATE_INACTIVE;
static int delay = 0;
static libspectrum_byte keyboard_ports_read = 0x00;
static int command_count;

static module_info_t phantom_typist_module_info = {
  phantom_typist_reset,
  NULL,
  NULL,
  NULL,
  NULL,
};

static int
phantom_typist_init( void *context )
{
  module_register( &phantom_typist_module_info );
  return 0;
}

void
phantom_typist_register_startup( void )
{
  startup_manager_module dependencies[] = { STARTUP_MANAGER_MODULE_SETUID };
  startup_manager_register( STARTUP_MANAGER_MODULE_PHANTOM_TYPIST, dependencies,
                            ARRAY_SIZE( dependencies ), phantom_typist_init,
                            NULL, NULL );
}

static void
phantom_typist_reset( int hard_reset )
{
  phantom_typist_state = PHANTOM_TYPIST_STATE_INACTIVE;
  next_phantom_typist_state = PHANTOM_TYPIST_STATE_INACTIVE;
}

static phantom_typist_highlevel_mode_t
get_highlevel_mode( libspectrum_machine machine )
{
  phantom_typist_highlevel_mode_t highlevel_mode;

  switch( machine ) {
    case LIBSPECTRUM_MACHINE_16:
    case LIBSPECTRUM_MACHINE_48:
    case LIBSPECTRUM_MACHINE_48_NTSC:
    case LIBSPECTRUM_MACHINE_TC2048:
    case LIBSPECTRUM_MACHINE_TC2068:
    case LIBSPECTRUM_MACHINE_TS2068:
    default:
      highlevel_mode = PHANTOM_TYPIST_HIGHLEVEL_MODE_KEYWORD;
      break;

    case LIBSPECTRUM_MACHINE_128:
    case LIBSPECTRUM_MACHINE_128E:
    case LIBSPECTRUM_MACHINE_PLUS2:
    case LIBSPECTRUM_MACHINE_PENT:
    case LIBSPECTRUM_MACHINE_PENT512:
    case LIBSPECTRUM_MACHINE_PENT1024:
    case LIBSPECTRUM_MACHINE_SCORP:
      highlevel_mode = PHANTOM_TYPIST_HIGHLEVEL_MODE_MENU;
      break;

    case LIBSPECTRUM_MACHINE_PLUS2A:
      highlevel_mode = PHANTOM_TYPIST_HIGHLEVEL_MODE_PLUS2A;
      break;

    case LIBSPECTRUM_MACHINE_PLUS3:
    case LIBSPECTRUM_MACHINE_PLUS3E:
      highlevel_mode = PHANTOM_TYPIST_HIGHLEVEL_MODE_PLUS3;
      break;

    case LIBSPECTRUM_MACHINE_SE:
      highlevel_mode = PHANTOM_TYPIST_HIGHLEVEL_MODE_KEYSTROKE;
      break;
  }

  return highlevel_mode;
}

static void
set_state_waiting( void )
{
  command_count = 0;
  phantom_typist_state = PHANTOM_TYPIST_STATE_WAITING;
  next_phantom_typist_state = PHANTOM_TYPIST_STATE_WAITING;

  timer_start_fastloading();
}

void
phantom_typist_activate( libspectrum_machine machine, int needs_code )
{
  phantom_typist_highlevel_mode_t highlevel_mode;
  const char *setting = settings_current.phantom_typist_mode;

  if( strcasecmp( setting, "Keyword" ) == 0 ) {
    highlevel_mode = PHANTOM_TYPIST_HIGHLEVEL_MODE_KEYWORD;
  } else if( strcasecmp( setting, "Keystroke" ) == 0) {
    highlevel_mode = PHANTOM_TYPIST_HIGHLEVEL_MODE_KEYSTROKE;
  } else if( strcasecmp( setting, "Menu" ) == 0) {
    highlevel_mode = PHANTOM_TYPIST_HIGHLEVEL_MODE_MENU;
  } else if( strcasecmp( setting, "Plus 2A" ) == 0 ||
             strcasecmp( setting, "plus2a" ) == 0) {
    highlevel_mode = PHANTOM_TYPIST_HIGHLEVEL_MODE_PLUS2A;
  } else if( strcasecmp( setting, "Plus 3" ) == 0 ||
             strcasecmp( setting, "plus3" ) == 0) {
    highlevel_mode = PHANTOM_TYPIST_HIGHLEVEL_MODE_PLUS3;
  } else if( strcasecmp( setting, "Auto" ) == 0) {
    highlevel_mode = get_highlevel_mode( machine );
  } else {
    highlevel_mode = PHANTOM_TYPIST_HIGHLEVEL_MODE_KEYWORD;
  }

  switch( highlevel_mode ) {
    case PHANTOM_TYPIST_HIGHLEVEL_MODE_KEYWORD:
    default:
      phantom_typist_mode = needs_code ?
        PHANTOM_TYPIST_MODE_JPPI :
        PHANTOM_TYPIST_MODE_JPP;
      break;

    case PHANTOM_TYPIST_HIGHLEVEL_MODE_KEYSTROKE:
      phantom_typist_mode = needs_code ?
        PHANTOM_TYPIST_MODE_LOADPPCODE :
        PHANTOM_TYPIST_MODE_LOADPP;
      break;

    case PHANTOM_TYPIST_HIGHLEVEL_MODE_MENU:
      phantom_typist_mode = needs_code ?
        PHANTOM_TYPIST_MODE_DOWN_LOADPPCODE :
        PHANTOM_TYPIST_MODE_ENTER;
      break;

    case PHANTOM_TYPIST_HIGHLEVEL_MODE_PLUS2A:
      phantom_typist_mode = needs_code ?
        PHANTOM_TYPIST_MODE_WAIT_DOWN_LOADPPCODE :
        PHANTOM_TYPIST_MODE_ENTER;
      break;

    case PHANTOM_TYPIST_HIGHLEVEL_MODE_PLUS3:
      phantom_typist_mode = needs_code ?
        PHANTOM_TYPIST_MODE_PLUS3_CODE_BLOCK :
        PHANTOM_TYPIST_MODE_ENTER;
      break;
  }


  set_state_waiting();
}

void
phantom_typist_activate_disk( void )
{
  phantom_typist_mode = PHANTOM_TYPIST_MODE_ENTER;
  set_state_waiting();
}

void
phantom_typist_deactivate( void )
{
  if( phantom_typist_is_active() ) {
    phantom_typist_state = PHANTOM_TYPIST_STATE_WAITING;
    next_phantom_typist_state = PHANTOM_TYPIST_STATE_INACTIVE;
  }
}

static void
process_waiting_state( libspectrum_byte high_byte )
{
  switch( high_byte ) {
    case 0xfe:
    case 0xfd:
    case 0xfb:
    case 0xf7:
    case 0xef:
    case 0xdf:
    case 0xbf:
    case 0x7f:
      keyboard_ports_read |= ~high_byte;
      break;
  }

  if( keyboard_ports_read == 0xff ) {
    switch( phantom_typist_mode ) {
      case PHANTOM_TYPIST_MODE_JPP:
      case PHANTOM_TYPIST_MODE_JPPI:
        next_phantom_typist_state = PHANTOM_TYPIST_STATE_LOAD;
        break;

      case PHANTOM_TYPIST_MODE_ENTER:
        next_phantom_typist_state = PHANTOM_TYPIST_STATE_ENTER_ONLY;
        break;

      case PHANTOM_TYPIST_MODE_DOWN_LOADPPCODE:
        next_phantom_typist_state = PHANTOM_TYPIST_STATE_DOWN;
        break;

      case PHANTOM_TYPIST_MODE_WAIT_DOWN_LOADPPCODE:
        next_phantom_typist_state = PHANTOM_TYPIST_STATE_WAIT_DOWN;
        break;

      case PHANTOM_TYPIST_MODE_PLUS3_CODE_BLOCK:
        next_phantom_typist_state = PHANTOM_TYPIST_STATE_LONG_WAIT_DOWN;
        break;

      case PHANTOM_TYPIST_MODE_LOADPP:
      case PHANTOM_TYPIST_MODE_LOADPPCODE:
        next_phantom_typist_state = PHANTOM_TYPIST_STATE_LOAD_L;
        break;

      default:
        next_phantom_typist_state = PHANTOM_TYPIST_STATE_INACTIVE;
        break;
    }
  }
}

static phantom_typist_state_t
t_colon_or_quote( void )
{
  phantom_typist_state_t next_state;

  if( phantom_typist_mode == PHANTOM_TYPIST_MODE_PLUS3_CODE_BLOCK &&
      command_count == 0 ) {
    next_state = PHANTOM_TYPIST_STATE_TCOLON_T;
  } else {
    next_state = PHANTOM_TYPIST_STATE_QUOTE2;
  }

  return next_state;
}

static phantom_typist_state_t
code_or_enter( void )
{
  phantom_typist_state_t next_state;

  switch( phantom_typist_mode ) {
    case PHANTOM_TYPIST_MODE_JPP:
    case PHANTOM_TYPIST_MODE_LOADPP:
      next_state = PHANTOM_TYPIST_STATE_ENTER;
      break;
    case PHANTOM_TYPIST_MODE_PLUS3_CODE_BLOCK:
      next_state = command_count == 0 ?
        PHANTOM_TYPIST_STATE_ENTER :
        PHANTOM_TYPIST_STATE_CODE_C;
      break;
    case PHANTOM_TYPIST_MODE_JPPI:
      next_state = PHANTOM_TYPIST_STATE_EXTENDED_MODE;
      break;
    case PHANTOM_TYPIST_MODE_LOADPPCODE:
    case PHANTOM_TYPIST_MODE_DOWN_LOADPPCODE:
    case PHANTOM_TYPIST_MODE_WAIT_DOWN_LOADPPCODE:
      next_state = PHANTOM_TYPIST_STATE_CODE_C;
      break;
    default:
      next_state = PHANTOM_TYPIST_STATE_INACTIVE;
      break;
  }

  return next_state;
}

static phantom_typist_state_t
next_command_or_end( void )
{
  phantom_typist_state_t next_state;

  if( phantom_typist_mode == PHANTOM_TYPIST_MODE_PLUS3_CODE_BLOCK &&
      command_count == 0 ) {
    next_state = PHANTOM_TYPIST_STATE_SLOW_LOAD_L;
  } else {
    next_state = PHANTOM_TYPIST_STATE_INACTIVE;
  }

  return next_state;
}

static libspectrum_byte
process_state( libspectrum_byte high_byte )
{
  libspectrum_byte r = 0xff;
  struct state_info_t *this_state = &state_info[phantom_typist_state];

  r &= keyboard_simulate_keypress( high_byte, this_state->keys_to_press[0] );
  r &= keyboard_simulate_keypress( high_byte, this_state->keys_to_press[1] );
  next_phantom_typist_state = this_state->next_state_fn ?
    this_state->next_state_fn() :
    this_state->next_state;

  return r;
}

libspectrum_byte
phantom_typist_ula_read( libspectrum_word port )
{
  libspectrum_byte r = 0xff;
  libspectrum_byte high_byte = port >> 8;

  if( delay != 0 ) {
    return r;
  }

  switch( phantom_typist_state ) {
    case PHANTOM_TYPIST_STATE_INACTIVE:
      /* Do nothing */
      break;

    case PHANTOM_TYPIST_STATE_WAITING:
      process_waiting_state( high_byte );
      break;

     default: 
      r &= process_state( high_byte );
      break;
  }

  return r;
}

int
phantom_typist_is_active( void )
{
  return phantom_typist_state != PHANTOM_TYPIST_STATE_INACTIVE;
}

void
phantom_typist_frame( void )
{
  keyboard_ports_read = 0x00;
  if( next_phantom_typist_state != phantom_typist_state ) {
    if( phantom_typist_mode == PHANTOM_TYPIST_MODE_PLUS3_CODE_BLOCK &&
        phantom_typist_state == PHANTOM_TYPIST_STATE_ENTER ) {
      command_count++;
    }

    phantom_typist_state = next_phantom_typist_state;
    delay = state_info[phantom_typist_state].delay_before_state;

    if( next_phantom_typist_state == PHANTOM_TYPIST_STATE_INACTIVE ) {
      timer_stop_fastloading();
    }
  }

  if( delay > 0 ) {
    delay--;
  }
}
