/// \file PinballGame/console.cpp

/// \brief User console interface for debugging using a host computer.

/// \copyright No copyright, 2016, Garth Zeglin.  This file is explicitly placed
///            in the public domain.

/// \details This file contains support code for implementing a command line
/// user interface using the default serial port on an Arduino.

#include "Arduino.h"
#include "console.h"

/****************************************************************/
/**** Global variables and constants ****************************/
/****************************************************************/

// These are declared in the main file.
extern void user_print_report(void);
extern void user_reset_game(void);
extern void user_set_game_state(int value);
extern void user_set_game_score(unsigned long value);
extern void user_play_melody(void);

// The maximum message line length.
const int MAX_LINE_LENGTH = 80;
 
// The maximum number of tokens in a single message.
const int MAX_TOKENS = 10;

/****************************************************************/
/**** Utility functions *****************************************/
/****************************************************************/

/// Send a single debugging string to the console.
void send_debug_message( const char *str )
{
  Serial.print("dbg ");
  Serial.println( str );
}

/****************************************************************/
/// Send a single debugging integer to the console.
void send_debug_message( int i )
{
  Serial.print("dbg ");
  Serial.println( i );
}

/****************************************************************/
/// Send a single-argument message back to the host.
void send_message( const char *command, long value )
{
  Serial.print( command );
  Serial.print( " " );
  Serial.println( value );
}

/****************************************************************/
/// Send a two-argument message back to the host.
void send_message( const char *command, long value1, long value2 )
{
  Serial.print( command );
  Serial.print( " " );
  Serial.print( value1 );
  Serial.print( " " );
  Serial.println( value2 );
}

/****************************************************************/
// Wrapper on strcmp for clarity of code.  Returns true if strings are
// identical.
static int string_equal( char *str1, const char str2[])
{
  return !strcmp(str1, str2);
}

/****************************************************************/
/// Process an input message.  Unrecognized commands are silently ignored.
///   \param argc   number of argument tokens
///   \param argv   array of pointers to strings, one per token
static void parse_user_input(int argc, char *argv[])
{
  // Interpret the first token as a command symbol.
  char *command = argv[0];

  /* -- process zero-argument commands --------------------------- */
  if (argc == 1) {
    if ( string_equal( command, "report" )) {
      user_print_report();
    }
    else if ( string_equal( command, "reset" )) {
      user_reset_game();
    }
    else if ( string_equal( command, "melody" )) {
      user_play_melody();
    }
    else {
      send_debug_message("unrecognized command.");
    }    
  }

  /* -- process one-argument commands --------------------------- */
  else if (argc == 2) {
    int value = atoi(argv[1] );
    
    // Set the game state to a particular mode.
    if ( string_equal( command, "state" )) {
      user_set_game_state(value);
    }
    else if ( string_equal( command, "score" )) {
      user_set_game_score(value);
    }
    else {
      send_debug_message("unrecognized single-argument command.");
    }    
  }
  else {
    send_debug_message("unrecognized command format.");
  }
}

/****************************************************************/
/// Polling function to process messages arriving over the serial port.  Each
/// iteration through this polling function processes at most one character.  It
/// records the input message line into a buffer while simultaneously dividing it
/// into 'tokens' delimited by whitespace.  Each token is a string of
/// non-whitespace characters, and might represent either a symbol or an integer.
/// Once a message is complete, parse_input_message() is called.

void poll_console_input(unsigned long elapsed)
{
  static char input_buffer[ MAX_LINE_LENGTH ];   // buffer for input characters
  static char *argv[MAX_TOKENS];                 // buffer for pointers to tokens
  static int chars_in_buffer = 0;  // counter for characters in buffer
  static int chars_in_token = 0;   // counter for characters in current partially-received token (the 'open' token)
  static int argc = 0;             // counter for tokens in argv
  static int error = 0;            // flag for any error condition in the current message

  (void) elapsed;  // no-op to suppress compiler warning

  // Check if at least one byte is available on the serial input.
  if (Serial.available()) {
    int input = Serial.read();

    // If the input is a whitespace character, end any currently open token.
    if ( isspace(input) ) {
      if ( !error && chars_in_token > 0) {
	if (chars_in_buffer == MAX_LINE_LENGTH) error = 1;
	else {
	  input_buffer[chars_in_buffer++] = 0;  // end the current token
	  argc++;                               // increase the argument count
	  chars_in_token = 0;                   // reset the token state
	}
      }

      // If the whitespace input is an end-of-line character, then pass the message buffer along for interpretation.
      if (input == '\r' || input == '\n') {

	// if the message included too many tokens or too many characters, report an error
	if (error) send_debug_message("excessive input error");

	// else process any complete message
	else if (argc > 0) parse_user_input( argc, argv ); 

	// reset the full input state
	error = chars_in_token = chars_in_buffer = argc = 0;                     
      }
    }

    // Else the input is a character to store in the buffer at the end of the current token.
    else {
      // if beginning a new token
      if (chars_in_token == 0) {

	// if the token array is full, set an error state
	if (argc == MAX_TOKENS) error = 1;

	// otherwise save a pointer to the start of the token
	else argv[ argc ] = &input_buffer[chars_in_buffer];
      }

      // the save the input and update the counters
      if (!error) {
	if (chars_in_buffer == MAX_LINE_LENGTH) error = 1;
	else {
	  input_buffer[chars_in_buffer++] = input;
	  chars_in_token++;
	}
      }
    }
  }
}
