// ***************** START OF ANSITERM.CPP *****************
//
//
// This file contains all the code to support the AnsiTerm 
// class, which is a terminal emulation class that supports 
// the IBM PC ANSI.SYS control sequences.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "terminal.h"
#include "ansiterm.h"

// These two defines can be used to help debug the terminal 
// emulation class.  DEBUG is used to split the screen and 
// provide a bottom window that displays escape sequences as 
// they are parsed.  KEYBOARD_FAKE lets you input escape 
// sequences from the keyboard.

//#define KEYBOARD_FAKE
#define DEBUG

//
// The debug macro in this file is only meant to be used
// when debugging in MS-DOS. A bit of additional code would
// need to be written under Windows to create a debug window.
// So we turn off the debug macro if we are building a 
// Windows program.
//
#if defined( _WINDOWS ) && defined( DEBUG )
#undef DEBUG
#endif

#if defined( DEBUG ) 
#include "textwind.h"
BaseWindow *debug_window;
#endif

// This is a list of keyboard mappings that are defined when the
// emulator first starts.  These key mappings can be remapped
// dynamically, although this class does not support the feature.
// These values are loaded into the mapping arrays when the
// constructor executes.

struct key_strings {
    int key_value;
    char *translation;
} initial_key_translations[] = { { LEFT,        "\x1b[D"  },
                                 { RIGHT,       "\x1b[C"  },
                                 { UP,          "\x1b[A"  },
                                 { DOWN,        "\x1b[B"  },
                                 { HOME,        "\x1b[H"  },
                                 { END,         "\x1b[F"  },
                                 { PGUP,        "\x1b[I"  },
                                 { PGDN,        "\x1b[G"  },
                                 { INSERT,      "\x1b[L"  },
                                 { F1,          "\x1b[M"  },
                                 { F2,          "\x1b[N"  },
                                 { F3,          "\x1b[O"  },
                                 { F4,          "\x1b[P"  },
                                 { F5,          "\x1b[Q"  },
                                 { F6,          "\x1b[R"  },
                                 { F7,          "\x1b[S"  },
                                 { F8,          "\x1b[T"  },
                                 { F9,          "\x1b[U"  },
                                 { F10,         "\x1b[V"  },
                                 { SHIFT_F1,    "\x1b[Y"  },
                                 { SHIFT_F2,    "\x1b[Z"  },
                                 { SHIFT_F3,    "\x1b[a"  },
                                 { SHIFT_F4,    "\x1b[b"  },
                                 { SHIFT_F5,    "\x1b[c"  },
                                 { SHIFT_F6,    "\x1b[d"  },
                                 { SHIFT_F7,    "\x1b[e"  },
                                 { SHIFT_F8,    "\x1b[f"  },
                                 { SHIFT_F9,    "\x1b[g"  },
                                 { SHIFT_F10,   "\x1b[h"  },
                                 { CONTROL_F1,  "\x1b[k"  },
                                 { CONTROL_F2,  "\x1b[l"  },
                                 { CONTROL_F3,  "\x1b[m"  },
                                 { CONTROL_F4,  "\x1b[n"  },
                                 { CONTROL_F5,  "\x1b[o"  },
                                 { CONTROL_F6,  "\x1b[p"  },
                                 { CONTROL_F7,  "\x1b[q"  },
                                 { CONTROL_F8,  "\x1b[r"  },
                                 { CONTROL_F9,  "\x1b[s"  },
                                 { CONTROL_F10, "\x1b[t"  },
                                 { ALT_F1,      "\x1b[w"  },
                                 { ALT_F2,      "\x1b[x"  },
                                 { ALT_F3,      "\x1b[y"  },
                                 { ALT_F4,      "\x1b[z"  },
                                 { ALT_F5,      "\x1b[@"  },
                                 { ALT_F6,      "\x1b[["  },
                                 { ALT_F7,      "\x1b[\\" },
                                 { ALT_F8,      "\x1b[]"  },
                                 { ALT_F9,      "\x1b[^"  },
                                 { ALT_F10,     "\x1b[_"  },
                                 { 0,           0         }
                               };

//
// The constructor sets up the pointers to the port and the 
// TextWindow objects.  It puts the window in a predefined 
// state, allocates the memory for the ansi string parsing 
// storage, then initializes the key maps.  If the DEBUG macro 
// is turned on, the screen is split and a second debug text 
// window is opened.
//

AnsiTerminal::AnsiTerminal( RS232 &p, BaseWindow &w ) 
    : Terminal( p, w )
{
    int key;
    char *translation;

    window->Clear();
    window->Goto();
    window->SetWrap( 1 );
    saved_row = 0;
    saved_col = 0;
    for ( int i = 0 ; i < 15 ; i++ )
        ansi_parms[ i ] = new char[ 81 ];
    ansi_parms[ 15 ] = 0;
    keys = new char *[ 256 ];
    extended_keys = new char *[ 256 ];
    for ( i = 0 ; i < 256 ; i++ ) {
        if ( keys )
            keys[ i ] = 0;
        if ( extended_keys )
            extended_keys[ i ] = 0;
    }
    for ( i = 0;
          initial_key_translations[ i ].translation != 0;
          i++ ) 
    {
        key = initial_key_translations[ i ].key_value;
        translation = initial_key_translations[ i ].translation;
        if ( extended_keys && key > 256 )
            extended_keys[ ( key >> 8 ) & 0xff ] = translation;
        else if ( keys )
            keys[ key ] = translation;
    }
#if defined( DEBUG ) 
    Set43LineMode( 1 );
    debug_window = new TextWindow( 25, 0, 80, 18 );
    debug_window->SetWrap( 1 );
#endif
}

//
// ReadPort filters port input for the end application.  If 
// it sees the first character of an escape sequence, it gets 
// parsed, and the application never sees it.  Normal characters 
// get passed straight back to the application.  Note that if 
// the KEYBOARD_FAKE macro is defined, input comes from the 
// keyboard instead of the port.
//
int AnsiTerminal::ReadPort( void )
{
     int c;

#ifdef KEYBOARD_FAKE
     while ( ( c = window->ReadKey() ) == 0 )
         ;
     if ( c == F10 )
         return RS232_ERROR;
#else
     c = port->Read();
#endif
     if ( c == ESC ) {
         parse();
         return RS232_TIMEOUT;
     }
     return c;
}

//
// The destructor for an AnsiTerminal objects just has to free 
// up the memory allocated for the ANSI parser.  If DEBUG is 
// turned on, the screen is restored to 25 line mode.  

AnsiTerminal::~AnsiTerminal( void )
{
    for ( int i = 0 ; i < 15 ; i++ )
        delete[] ansi_parms[ i ];
    delete[] keys;
    delete[] extended_keys;
#if defined( DEBUG ) && !defined( _WINDOWS )
    Set43LineMode( 0 );
#endif
}

// This is the actual parser that reads in ANSI strings.  It
// is just a fairly simple state machine, that sits in a loop
// reading in characters until it detects the end of an ANSI
// sequence.  When it is done, the ansi_parms[] array holds a 
// list of numeric and quoted strings, and parm_count is the 
// index to the last valid string.  Error handling is 
// non-existent, if anything odd happens the routine just returns 
// with a failure.

int AnsiTerminal::parse_ansi_string( void )
{
    int index;
    enum { READY_TO_READ,
           READING_DIGITS,
           READING_STRING,
           DONE_WITH_STRING } scan_state;
    int c;

    parm_count = 0;
    index = 0;

    for ( int i = 0 ; i < 15 ; i++ )
        if ( ansi_parms[ i ] != 0 )
            memset( ansi_parms[ i ], 0, 81 );
#ifdef KEYBOARD_FAKE
    while ( ( c = window->ReadKey() ) == 0 )
        ;
#else
    c = port->Read( 200 );
#endif
    if ( c != '[' )
        return 0;

    scan_state = READY_TO_READ;
    for ( ; ; ) {
        if ( index >= 80 || ansi_parms[ parm_count ] == 0 )
            return 0;
#ifdef KEYBOARD_FAKE
        while ( ( c = window->ReadKey() ) == 0 )
            ;
#else
        c = port->Read( 1000 );
#endif
        if ( c < 0 )
            return 0;
        switch ( scan_state ) {
          case READY_TO_READ:
            if ( parm_count == 0 && ( c == '=' || c == '?' ) )
              ansi_parms[ parm_count++ ][ 0 ] = (char) c;
            else if ( c == '"' ) {
              scan_state = READING_STRING;
              ansi_parms[ parm_count ][ index++ ] = (char) c;
            } else if ( c >= '0' && c <= '9' ) {
              ansi_parms[ parm_count ][ index++ ] = (char) c;
              scan_state = READING_DIGITS;
            } else if ( c == ';' ) {
              parm_count++;
              index = 0;
            } else {
              ansi_parms[ parm_count ][ index ] = (char) c;
              return 1;
            }
            break;
          case READING_DIGITS :
            if ( c == ';' ) {
              parm_count++;
              index = 0;
              scan_state = READY_TO_READ;
            } else if ( c >= '0' && c <='9' )
              ansi_parms[ parm_count ][ index++ ] = (char) c;
            else {
              ansi_parms[ ++parm_count ][ 0 ] = (char) c;
              return 1;
            }
            break;
          case READING_STRING :
            if ( c == '"' )
              scan_state = DONE_WITH_STRING;
            ansi_parms[ parm_count ][ index++ ] = (char) c;
            break;
          case DONE_WITH_STRING :
            if ( c == ';' ) {
              parm_count++;
              index = 0;
              scan_state = READY_TO_READ;
            } else {
              ansi_parms[ ++parm_count ][ 0 ] = (char) c;
              return 1;
            }
            break;
        }
    }
}

// This routine is the high level controller for the terminal
// emulation class.  It calls parse_ansi_string() to break the
// escape sequence down into usable components, then dispatches
// the appropriate member function to do the work.  If DEBUG is
// switched on, the escape sequence is dumped out to the debug
// window.

void AnsiTerminal::parse( void )
{
    if ( parse_ansi_string() ) {
#if defined( DEBUG ) 
        debug_window->Goto();
        DisplayAttribute att = debug_window->GetAttribute();
        debug_window->SetAttribute( att ^ 0x77 );
        *debug_window << "ESC [ ";
        for ( int i = 0 ; i <= parm_count; i++ )
            *debug_window << "<" << ansi_parms[ i ] << ">";
        *debug_window << "  ";
        window->Goto();
#endif
        switch( ansi_parms[ parm_count ][ 0 ] ) {
            case 'A' : cursor_move( -1, 0 ); break;
            case 'B' : cursor_move( 1, 0 ); break;
            case 'C' : cursor_move( 0, 1 ); break;
            case 'D' : cursor_move( 0, -1 ); break;
            case 'H' : position_cursor(); break;
            case 'J' : erase_in_display(); break;
            case 'K' : erase_in_line(); break;
            case 'f' : position_cursor(); break;
            case 'l' : set_mode(); break;
            case 'h' : set_mode(); break;
            case 'm' : set_color(); break;
            case 'n' : cursor_position_report(); break;
            case 's' : save_position(); break;
            case 'u' : restore_position(); break;
        }
    }
}

// ESC[#;#f and ESC[#;#H
//
// These two commands have the same effect, which is to position
// the cursor at a location specified by the two numbers, which 
// are a row and column sequence.  One or both parameters can be 
// omitted, in which case the default value of 1 is used.  Note 
// that row and column numbers in ANSI are 1 based, while the 
// BaseWindow class numbers are 0 based.

void AnsiTerminal::position_cursor()
{
    int row;
    int col;

    if ( parm_count > 0 )
        row = atoi( ansi_parms[ 0 ] );
    else
        row = 1;
    if ( parm_count > 1 )
        col = atoi( ansi_parms[ 1 ] );
    else
        col = 1;
    window->SetPosition( row - 1, col  - 1 );
}

// ESC[#A  Cursor up
// ESC[#B  Cursor down
// ESC[#C  Cursor right
// ESC[#D  Cursor left
//
// These four commands are all handled with this member function.  
// The single numeric parameter defaults to 1 if it is omitted.  
// Any movement outside the screen bounds is ignored by the 
// BaseWindow functions, so this routine doesn't have to worry 
// about it.

void AnsiTerminal::cursor_move( int row_dir, int col_dir )
{
    int offset;
    int row;
    int col;

    if ( parm_count > 0 )
        offset = atoi( ansi_parms[ 0 ] );
    else
        offset = 1;
    window->GetPosition( row, col );
    row += offset * row_dir;
    col += offset * col_dir;
    window->SetPosition( row, col );
}

// ESC[6n  Device Status Report
//
// This command is handled by issuing a Cursor Position Report 
// sequence, ESC[#;#R, with the two numeric parameters being the 
// row and column number.  Note that handling the command this way 
// is somewhat idiosynchratic to the PC.

void AnsiTerminal::cursor_position_report( void )
{
    int row;
    int col;
    char temp[ 40 ];

    if ( parm_count != 1 )
        return;
    if ( strcmp( ansi_parms[ 0 ], "6" ) != 0 )
        return;
    window->GetPosition( row, col );
    sprintf( temp, "%c[%d;%dR", ESC, row + 1, col + 1 );
    port->Write( temp );
}

// ESC[2J  Erase in display
//
// The official ANSI version of this command will erase some or 
// all of the display, depending on the value of the numeric 
// parameters. The IBM PC version only support parameter 2, which 
// erases the entire display.  The cursor is homed as part of this 
// command.

void AnsiTerminal::erase_in_display( void )
{
    if ( parm_count != 1 )
        return;
    if ( strcmp( ansi_parms[ 0 ], "2" ) != 0 )
        return;
    window->Clear();
    window->SetPosition( 0, 0 );
}

// ESC[K  Erase in line
//
// This is another ANSI command that is only partially supported 
// by IBM ANSI.  When no numeric parameter is given, the line is 
// erased from the cursor position to the end of the line.

void AnsiTerminal::erase_in_line( void )
{
    int row;
    int col;
    int width;
    int height;
    int i;

    if ( parm_count != 0 )
        return;
    window->GetPosition( row, col );
    window->GetDimensions( width, height );
    for ( i = col ; i < width; i++ )
        *window << ' ';
    window->SetPosition( row, col );
}

// ESC[s  Save Cursor Position
//
// This command saves off the current cursor position for later
// restoration.  This is an IBM extension to the ANSI standard.

void AnsiTerminal::save_position( void )
{
    if ( parm_count != 0 )
        return;
    window->GetPosition( saved_row, saved_col );
}

// ESC[u  Restore Cursor Position
//
// Another IBM extension to the ANSI standard.  This command 
// restores the previously saved cursor position.

void AnsiTerminal::restore_position( void )
{
    if ( parm_count != 0 )
        return;
    window->SetPosition( saved_row, saved_col );
}

// ESC[#;#;...;#m  Set Graphics Rendition
//
// This command sets the current display attributes to various 
// attributes. Multiple command parameters can be strung
// together in unlimited combinations.  This implementation is 
// limited to 14 parameters.

void AnsiTerminal::set_color( void )
{
    int command;
    int att;

    for ( int i = 0 ; i < parm_count ; i++ ) {
       command = atoi( ansi_parms[ i ] );
       att = window->GetAttribute();
       switch( command ) {
          case 0  : att = NORMAL_ATTRIBUTE;      break;
          case 1  : att = att | 8;               break;
          case 5  : att = att | 0x80;            break;
          case 7  : att = REVERSE_ATTRIBUTE;     break;
          case 8  : att = INVISIBLE_ATTRIBUTE;   break;
          case 30 : att = ( att & 0xf0 ) | 0x00; break;
          case 31 : att = ( att & 0xf0 ) | 0x04; break;
          case 32 : att = ( att & 0xf0 ) | 0x02; break;
          case 33 : att = ( att & 0xf0 ) | 0x0e; break;
          case 34 : att = ( att & 0xf0 ) | 0x01; break;
          case 35 : att = ( att & 0xf0 ) | 0x05; break;
          case 36 : att = ( att & 0xf0 ) | 0x03; break;
          case 37 : att = ( att & 0xf0 ) | 0x07; break;
          case 40 : att = ( att & 0x0f ) | 0x00; break;
          case 41 : att = ( att & 0x0f ) | 0x40; break;
          case 42 : att = ( att & 0x0f ) | 0x20; break;
          case 43 : att = ( att & 0x0f ) | 0x60; break;
          case 44 : att = ( att & 0x0f ) | 0x10; break;
          case 45 : att = ( att & 0x0f ) | 0x50; break;
          case 46 : att = ( att & 0x0f ) | 0x30; break;
          case 47 : att = ( att & 0x0f ) | 0x70; break;
       }
       window->SetAttribute( att );
    }
}

// ESC[=#h  ESC[=#l  Set/Reset Mode
// ESC[=h   ESC[=l
// ESC[=0h  ESC[=0l
// ESC[?7h  ESC[?7l
//
// This command is used to change the current video mode.  The
// TextWindow class used here doesn't support changing modes, so
// most versions of this command aren't supported.d  The single
// exception is the last version, which turns on/off line wrap.

void AnsiTerminal::set_mode( void )
{
    if ( parm_count != 2 )
        return;
    if ( strcmp( ansi_parms[ 0 ], "?" ) != 0 &&
         strcmp( ansi_parms[ 0 ], "=" ) != 0 )
        return;
    if ( strcmp( ansi_parms[ 1 ], "7" ) != 0 )
        return;
    switch( ansi_parms[ 2 ][ 0 ] ) {
        case 'h' : window->SetWrap( 1 ); break;
        case 'l' : window->SetWrap( 0 ); break;
    }
}

// Writing a key to the serial port is done by checking to see if
// a translation is defined.  If not, the key itself is sent out,
// otherwise the translation is sent.

void AnsiTerminal::WriteKey( int key )
{
    char *translation;

    if ( extended_keys && key > 256 ) {
        translation = extended_keys[ ( key >> 8 ) & 0xff ];
        if ( translation != 0 )
            port->Write( translation );
    } else {
        if ( keys )
            translation = keys[ key & 0xff ];
        else
            translation = 0;
        if ( translation != 0 )
            port->Write( translation );
        else
            port->Write( key );
    }
}

// ******************* END OF ANSITERM.CPP *******************
