/*
  Automaton.cpp - Reactive State Machine Framework for Arduino.
  Published under the MIT License (MIT), Copyright (c) 2015-2016, J.P. van der Landen
*/

#include "Automaton.h"

/* The Machine class is a base class for creating and running State Machines
 *
 *********************************************************************************************
 *
 * Machine::state( void ) - Retrieves the current state for the machine
 *
 * (may be overridden by a subclass in which case it may return something else, like a value )
 */

int Machine::state() {
  return current;
}

/*
 * Machine::state( state ) - Sets the next state for the machine
 *
 */

Machine& Machine::state( int state ) {
  next = state;
  last_trigger = -1;
  flags &= ~ATM_SLEEP_FLAG;
  return *this;
}

/*
 * Machine::trigger( evt ) - Triggers an event for the machine
 *
 * The machine is cycled for maximum of 8 times until it is actively listening for the event
 * Then the event is triggered followed by two more cycles to process the event and the
 * following state change.
 *
 */

Machine& Machine::trigger( int evt /* = 0 */ ) {
  int new_state;
  int max_cycle = 8;
  do {
    flags &= ~ATM_SLEEP_FLAG;
    cycle();
    new_state = read_state( state_table + ( current * state_width ) + evt + ATM_ON_EXIT + 1 );
  } while ( --max_cycle && ( new_state == -1 || next_trigger != -1 ) );
  if ( new_state > -1 ) {
    next_trigger = evt;
    flags &= ~ATM_SLEEP_FLAG;
    cycle();  // Pick up the trigger
    flags &= ~ATM_SLEEP_FLAG;
    cycle();  // Process the state change
  }
  return *this;
}

/*
 * Machine::setTrace( stream, callback, symbols ) - Sets up state tracing for the machine
 *
 * Connects a stream object, a callback (atm_serial_debug) and a symbol table (string) to the object
 *
 */

Machine& Machine::setTrace( Stream* stream, swcb_sym_t callback, const char symbols[] ) {
  callback_trace = callback;
  stream_trace = stream;
  this->symbols = symbols;
  return *this;
}

/*
 * Machine::sleep( v ) - Sets or returns the current sleep flag setting
 *
 */

uint8_t Machine::sleep( int8_t v /* = 1 */ ) {
  if ( v > -1 ) flags = v ? flags | ATM_SLEEP_FLAG : flags & ~ATM_SLEEP_FLAG;
  return ( flags & ATM_SLEEP_FLAG ) > 0;
}

/*
 * Machine::begin( state_table, width ) - Initializes the state table and sets the sleep flag
 *
 */

Machine& Machine::begin( const state_t* tbl, int width ) {
  state_table = tbl;
  state_width = ATM_ON_EXIT + width + 2;
  flags &= ~ATM_SLEEP_FLAG;
  automaton.add( *this, false );
  current = -1;
  next = 0;
  next_trigger = -1;
  return *this;
}

/*
 * Machine::onPush( connectors, id, sub, slots, multi, dest, arg ) - Registers a connector destination
 *
 * connectors Connector table
 * id         Connector id
 * sub        Connector sub id (for multi-slot connectors)
 * slots      Number of slots reserved for this connector
 * multi      Register multiple (all) slots in one call
 * dest       Destination: Machine object or callback
 * arg        Argument for machine (event) or callback (idx)
 *
 */

void Machine::onPush( atm_connector connectors[], int id, int sub, int slots, int fill, Machine& machine, int event ) {
  if ( sub == -1 ) {  // auto store
    sub = 0;
    for ( int i = 0; i < slots; i++ ) {
      if ( connectors[id + i].mode() == 0 ) {  // Find a free slot
        sub = i;
      }
    }
  }
  if ( slots > 1 && fill ) {
    for ( int i = 0; i < slots; i++ ) {
      connectors[id + i].set( &machine, event );
    }
  } else {
    connectors[id + sub].set( &machine, event );
  }
}

void Machine::onPush( atm_connector connectors[], int id, int sub, int slots, int fill, atm_cb_push_t callback, int idx ) {
  if ( sub == -1 ) {  // auto store
    sub = 0;
    for ( int i = 0; i < slots; i++ ) {
      if ( connectors[id + i].mode() == 0 ) {  // Find a free slot
        sub = i;
      }
    }
  }
  if ( slots > 1 && fill ) {
    for ( int i = 0; i < slots; i++ ) {
      connectors[id + i].set( callback, idx );
    }
  } else {
    connectors[id + sub].set( callback, idx );
  }
}

/*
 * Machine::push( connectors, id, sub, v, up ) - Pushes an action through the specified connector
 *
 * connectors Connector table
 * id         Connector id
 * sub        Connector sub id (for multi-slot connectors)
 * v          Value to pass to a callback as 'v'
 * up         Value to pass to a callback as 'up'
 *
 */

void Machine::push( atm_connector connectors[], int id, int sub, int v, int up ) {
  if ( ( id & ATM_BROADCAST ) > 0 ) {
    id = id & ~ATM_BROADCAST;
    for ( int i = id; i < sub; i++ ) {
      connectors[id + i].push( v, up );
    }
  } else {
    connectors[id + sub].push( v, up );
  }
}

/*
 * Machine::mapSymbol( id, map ) - Maps a number ( event/state ) to a symbol
 *
 * 0        Machine class name (e.g. LED)
 * 1..ELSE  Event name (e.g. EVT_TIMER)
 * ELSE..   State name (e.g. IDLE)
 *
 */

const char* Machine::mapSymbol( int id, const char map[] ) {
  int cnt = 0;
  int i = 0;
  if ( id == -1 ) return "*NONE*";
  if ( id == 0 ) return map;
  while ( 1 ) {
    if ( map[i] == '\0' && ++cnt == id ) {
      i++;
      break;
    }
    i++;
  }
  return &map[i];
}

/*
 * Machine::cycle( time ) - Executes one cycle of a State Machine
 *
 * For every state change:
 * - Calls the ON_SWITCH action
 * - Calls the state trace function (if connected)
 * - Calls the previous state's ON_EXIT action
 * - Changes the active state (current) to the new
 * - Calls the new state's ON_ENTER action
 *
 * For every 'normal' cycle:
 * - Executes the ON_LOOP action
 * - Scans the event columns in the current table and calls active events
 *
 * If the 'time' argument is given, loops until that time has passed
 * otherwise executes only one cycle of the machine
 */

Machine& Machine::cycle( uint32_t time /* = 0 */ ) {
  uint32_t cycle_start = millis();
  do {
    if ( ( flags & ( ATM_SLEEP_FLAG | ATM_CYCLE_FLAG ) ) == 0 ) {
      cycles++;
      flags |= ATM_CYCLE_FLAG;
      if ( next != -1 ) {
        action( ATM_ON_SWITCH );
        if ( callback_trace ) {
          callback_trace( stream_trace, *this, symbols, mapSymbol( current == -1 ? current : current + state_width - ATM_ON_EXIT, symbols ),
                          mapSymbol( next == -1 ? next : next + state_width - ATM_ON_EXIT, symbols ),
                          mapSymbol( last_trigger == -1 ? -1 : last_trigger + 1, symbols ), millis() - state_millis, cycles );
        }
        if ( current > -1 ) action( read_state( state_table + ( current * state_width ) + ATM_ON_EXIT ) );
        current = next;
        next = -1;
        state_millis = millis();
        action( read_state( state_table + ( current * state_width ) + ATM_ON_ENTER ) );
        if ( read_state( state_table + ( current * state_width ) + ATM_ON_LOOP ) == ATM_SLEEP ) {
          flags |= ATM_SLEEP_FLAG;
        } else {
          flags &= ~ATM_SLEEP_FLAG;
        }
        cycles = 0;
      }
      state_t i = read_state( state_table + ( current * state_width ) + ATM_ON_LOOP );
      if ( i != -1 ) {
        action( i );
      }
      for ( i = ATM_ON_EXIT + 1; i < state_width; i++ ) {
        state_t next_state = read_state( state_table + ( current * state_width ) + i );
        if ( ( next_state != -1 ) && ( i == state_width - 1 || event( i - ATM_ON_EXIT - 1 ) || next_trigger == i - ATM_ON_EXIT - 1 ) ) {
          state( next_state );
          last_trigger = i - ATM_ON_EXIT - 1;
          next_trigger = -1;
          break;
        }
      }
      flags &= ~ATM_CYCLE_FLAG;
    }
  } while ( millis() - cycle_start < time );
  return *this;
}
