// Demo.js -- Demonstrate some of the features of ShellInABox
// Copyright (C) 2008-2009 Markus Gutschke <markus@shellinabox.com>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2 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.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// In addition to these license terms, the author grants the following
// additional rights:
//
// If you modify this program, or any covered work, by linking or
// combining it with the OpenSSL project's OpenSSL library (or a
// modified version of that library), containing parts covered by the
// terms of the OpenSSL or SSLeay licenses, the author
// grants you additional permission to convey the resulting work.
// Corresponding Source for a non-source form of such a combination
// shall include the source code for the parts of OpenSSL used as well
// as that of the covered work.
//
// You may at your option choose to remove this additional permission from
// the work, or from any part of it.
//
// It is possible to build this program in a way that it loads OpenSSL
// libraries at run-time. If doing so, the following notices are required
// by the OpenSSL and SSLeay licenses:
//
// This product includes software developed by the OpenSSL Project
// for use in the OpenSSL Toolkit. (http://www.openssl.org/)
//
// This product includes cryptographic software written by Eric Young
// (eay@cryptsoft.com)
//
//
// The most up-to-date version of this program is always available from
// http://shellinabox.com
//
//
// Notes:
//
// The author believes that for the purposes of this license, you meet the
// requirements for publishing the source code, if your web server publishes
// the source in unmodified form (i.e. with licensing information, comments,
// formatting, and identifier names intact). If there are technical reasons
// that require you to make changes to the source code when serving the
// JavaScript (e.g to remove pre-processor directives from the source), these
// changes should be done in a reversible fashion.
//
// The author does not consider websites that reference this script in
// unmodified form, and web servers that serve this script in unmodified form
// to be derived works. As such, they are believed to be outside of the
// scope of this license and not subject to the rights or restrictions of the
// GNU General Public License.
//
// If in doubt, consult a legal professional familiar with the laws that
// apply in your country.

#define STATE_IDLE     0
#define STATE_INIT     1
#define STATE_PROMPT   2
#define STATE_READLINE 3
#define STATE_COMMAND  4
#define STATE_EXEC     5
#define STATE_NEW_Y_N  6

#define TYPE_STRING    0
#define TYPE_NUMBER    1

function extend(subClass, baseClass) {
  function inheritance() { }
  inheritance.prototype          = baseClass.prototype;
  subClass.prototype             = new inheritance();
  subClass.prototype.constructor = subClass;
  subClass.prototype.superClass  = baseClass.prototype;
};

function Demo(container) {
  this.superClass.constructor.call(this, container);
  this.gotoState(STATE_INIT);
};
extend(Demo, VT100);

Demo.prototype.keysPressed = function(ch) {
  if (this.state == STATE_EXEC) {
    for (var i = 0; i < ch.length; i++) {
      var c  = ch.charAt(i);
      if (c == '\u0003') {
        this.keys = '';
        this.error('Interrupted');
        return;
      }
    }
  }
  this.keys += ch;
  this.gotoState(this.state);
};

Demo.prototype.gotoState = function(state, tmo) {
  this.state       = state;
  if (!this.timer || tmo) {
    if (!tmo) {
      tmo          = 1;
    }
    this.nextTimer = setTimeout(function(demo) {
                                  return function() {
                                    demo.demo();
                                  };
                                }(this), tmo);
  }
};

Demo.prototype.demo = function() {
  var done                  = false;
  this.nextTimer            = undefined;
  while (!done) {
    var state               = this.state;
    this.state              = STATE_PROMPT;
    switch (state) {
    case STATE_INIT:
      done                  = this.doInit();
      break;
    case STATE_PROMPT:
      done                  = this.doPrompt();
      break;
    case STATE_READLINE:
      done                  = this.doReadLine();
      break;
    case STATE_COMMAND:
      done                  = this.doCommand();
      break;
    case STATE_EXEC:
      done                  = this.doExec();
      break;
    case STATE_NEW_Y_N:
      done                  = this.doNewYN();
      break;
    default:
      done                  = true;
      break;
    }
  }
  this.timer                = this.nextTimer;
  this.nextTimer            = undefined;
};

Demo.prototype.ok = function() {
  this.vt100('OK\r\n');
  this.gotoState(STATE_PROMPT);
};

Demo.prototype.error = function(msg) {
  if (msg == undefined) {
    msg                 = 'Syntax Error';
  }
  this.printUnicode((this.cursorX != 0 ? '\r\n' : '') + '\u0007? ' + msg +
                    (this.currentLineIndex >= 0 ? ' in line ' +
                     this.program[this.evalLineIndex].lineNumber() :
                     '') + '\r\n');
  this.gotoState(STATE_PROMPT);
  this.currentLineIndex = -1;
  this.evalLineIndex    = -1;
  return undefined;
};

Demo.prototype.doInit = function() {
  this.vars    = new Object();
  this.program = new Array();
  this.printUnicode(
    '\u001Bc\u001B[34;4m' +
    'ShellInABox Demo Script\u001B[24;31m\r\n' +
    '\r\n' +
    'Copyright 2009 by Markus Gutschke <markus@shellinabox.com>\u001B[0m\r\n' +
    '\r\n' +
    '\r\n' +
    'This script simulates a minimal BASIC interpreter, allowing you to\r\n' +
    'experiment with the JavaScript terminal emulator that is part of\r\n' +
    'the ShellInABox project.\r\n' +
    '\r\n' +
    'Type HELP for a list of commands.\r\n' +
    '\r\n');
  this.gotoState(STATE_PROMPT);
  return false;
};

Demo.prototype.doPrompt = function() {
  this.keys             = '';
  this.line             = '';
  this.currentLineIndex = -1;
  this.evalLineIndex    = -1;
  this.vt100((this.cursorX != 0 ? '\r\n' : '') + '> ');
  this.gotoState(STATE_READLINE);
  return false;
};

Demo.prototype.printUnicode = function(s) {
  var out = '';
  for (var i = 0; i < s.length; i++) {
    var c = s.charAt(i);
    if (c < '\x0080') {
      out += c;
    } else {
      var c = s.charCodeAt(i);
      if (c < 0x800) {
        out += String.fromCharCode(0xC0 +  (c >>  6)        ) +
               String.fromCharCode(0x80 + ( c        & 0x3F));
      } else if (c < 0x10000) {
        out += String.fromCharCode(0xE0 +  (c >> 12)        ) +
               String.fromCharCode(0x80 + ((c >>  6) & 0x3F)) +
               String.fromCharCode(0x80 + ( c        & 0x3F));
      } else if (c < 0x110000) {
        out += String.fromCharCode(0xF0 +  (c >> 18)        ) +
               String.fromCharCode(0x80 + ((c >> 12) & 0x3F)) +
               String.fromCharCode(0x80 + ((c >>  6) & 0x3F)) +
               String.fromCharCode(0x80 + ( c        & 0x3F));
      }
    }
  }
  this.vt100(out);
};

Demo.prototype.doReadLine = function() {
  this.gotoState(STATE_READLINE);
  var keys  = this.keys;
  this.keys = '';
  for (var i = 0; i < keys.length; i++) {
    var ch  = keys.charAt(i);
    if (ch == '\u0008' || ch == '\u007F') {
      if (this.line.length > 0) {
        this.line = this.line.substr(0, this.line.length - 1);
        if (this.cursorX == 0) {
          var x = this.terminalWidth - 1;
          var y = this.cursorY - 1;
          this.gotoXY(x, y);
          this.vt100(' ');
          this.gotoXY(x, y);
        } else {
          this.vt100('\u0008 \u0008');
        }
      } else {
        this.vt100('\u0007');
      }
    } else if (ch >= ' ') {
      this.line += ch;
      this.printUnicode(ch);
    } else if (ch == '\r' || ch == '\n') {
      this.vt100('\r\n');
      this.gotoState(STATE_COMMAND);
      return false;
    } else if (ch == '\u001B') {
      // This was probably a function key. Just eat all of the following keys.
      break;
    }
  }
  return true;
};

Demo.prototype.doCommand = function() {
  this.gotoState(STATE_PROMPT);
  var tokens              = new this.Tokens(this.line);
  this.line               = '';
  var cmd                 = tokens.nextToken();
  if (cmd) {
    cmd                   = cmd;
    if (cmd.match(/^[0-9]+$/)) {
      tokens.removeLineNumber();
      var lineNumber        = parseInt(cmd);
      var index             = this.findLine(lineNumber);
      if (tokens.nextToken() == null) {
        if (index > 0) {
          // Delete line from program
          this.program.splice(index, 1);
        }
      } else {
        tokens.reset();
        if (index >= 0) {
          // Replace line in program
          this.program[index].setTokens(tokens);
        } else {
          // Add new line to program
          this.program.splice(-index - 1, 0,
                              new this.Line(lineNumber, tokens));
        }
      }
    } else {
      this.currentLineIndex = -1;
      this.evalLineIndex    = -1;
      tokens.reset();
      this.tokens           = tokens;
      return this.doEval();
    }
  }
  return false;
};

Demo.prototype.doEval = function() {
  var token                 = this.tokens.peekToken();
  if (token == "DIM") {
    this.tokens.consume();
    this.doDim();
  } else if (token == "END") {
    this.tokens.consume();
    this.doEnd();
  } else if (token == "GOTO") {
    this.tokens.consume();
    this.doGoto();
  } else if (token == "HELP") {
    this.tokens.consume();
    if (this.tokens.nextToken() != undefined) {
      this.error('HELP does not take any arguments');
    } else {
      this.vt100('Supported commands:\r\n' +
               'DIM END GOTO HELP LET LIST NEW PRINT RUN\r\n'+
               '\r\n'+
               'Supported functions:\r\n'+
               'ABS() ASC() ATN() CHR$() COS() EXP() INT() LEFT$() LEN()\r\n'+
               'LOG() MID$() POS() RIGHT$() RND() SGN() SIN() SPC() SQR()\r\n'+
               'STR$() TAB() TAN() TI VAL()\r\n');
    }
  } else if (token == "LET") {
    this.tokens.consume();
    this.doAssignment();
  } else if (token == "LIST") {
    this.tokens.consume();
    this.doList();
  } else if (token == "NEW") {
    this.tokens.consume();
    if (this.tokens.nextToken() != undefined) {
      this.error('NEW does not take any arguments');
    } else if (this.currentLineIndex >= 0) {
      this.error('Cannot call NEW from a program');
    } else if (this.program.length == 0) {
      this.ok();
    } else {
      this.vt100('Do you really want to delete the program (y/N) ');
      this.gotoState(STATE_NEW_Y_N);
    }
  } else if (token == "PRINT" || token == "?") {
    this.tokens.consume();
    this.doPrint();
  } else if (token == "RUN") {
    this.tokens.consume();
    if (this.tokens.nextToken() != null) {
      this.error('RUN does not take any parameters');
    } else if (this.program.length > 0) {
      this.currentLineIndex = 0;
      this.vars = new Object();
      this.gotoState(STATE_EXEC);
    } else {
      this.ok();
    }
  } else {
    this.doAssignment();
  }
  return false;
};

Demo.prototype.arrayIndex = function() {
  var token   = this.tokens.peekToken();
  var arr     = '';
  if (token == '(') {
    this.tokens.consume();
    do {
      var idx = this.expr();
      if (idx == undefined) {
        return idx;
      } else if (idx.type() != TYPE_NUMBER) {
        return this.error('Numeric value expected');
      }
      idx     = Math.floor(idx.val());
      if (idx < 0) {
        return this.error('Indices have to be positive');
      }
      arr    += ',' + idx;
      token   = this.tokens.nextToken();
    } while (token == ',');
    if (token != ')') {
      return this.error('")" expected');
    }
  }
  return arr;
};

Demo.prototype.toInt = function(v) {
  if (v < 0) {
    return -Math.floor(-v);
  } else {
    return  Math.floor( v);
  }
};

Demo.prototype.doAssignment = function() {
  var id       = this.tokens.nextToken();
  if (!id || !id.match(/^[A-Za-z][A-Za-z0-9_]*$/)) {
    return this.error('Identifier expected');
  }
  var token = this.tokens.peekToken();
  var isString = false;
  var isInt    = false;
  if (token == '$') {
    isString   = true;
    this.tokens.consume();
  } else if (token == '%') {
    isInt      = true;
    this.tokens.consume();
  }
  var arr      = this.arrayIndex();
  if (arr == undefined) {
    return arr;
  }
  token        = this.tokens.nextToken();
  if (token != '=') {
    return this.error('"=" expected');
  }
  var value    = this.expr();
  if (value == undefined) {
    return value;
  }
  if (isString) {
    if (value.type() != TYPE_STRING) {
      return this.error('String expected');
    }
    this.vars['str_' + id + arr] = value;
  } else {
    if (value.type() != TYPE_NUMBER) {
      return this.error('Numeric value expected');
    }
    if (isInt) {
      value    = this.toInt(value.val());
      value    = new this.Value(TYPE_NUMBER, '' + value, value);
      this.vars['int_' + id + arr] = value;
    } else {
      this.vars['var_' + id + arr] = value;
    }
  }
};

Demo.prototype.doDim = function() {
  for (;;) {
    var token = this.tokens.nextToken();
    if (token == undefined) {
      return;
    }
    if (!token || !token.match(/^[A-Za-z][A-Za-z0-9_]*$/)) {
      return this.error('Identifier expected');
    }
    token     = this.tokens.nextToken();
    if (token == '$' || token == '%') {
      token   = this.tokens.nextToken();
    }
    if (token != '(') {
      return this.error('"(" expected');
    }
    do {
      var size = this.expr();
      if (!size) {
        return size;
      }
      if (size.type() != TYPE_NUMBER) {
        return this.error('Numeric value expected');
      }
      if (Math.floor(size.val()) < 1) {
        return this.error('Range error');
      }
      token    = this.tokens.nextToken();
    } while (token == ',');
    if (token != ')') {
      return this.error('")" expected');
    }
    if (this.tokens.peekToken() != ',') {
      break;
    }
    this.tokens.consume();
  }
  if (this.tokens.peekToken() != undefined) {
    return this.error();
  }
};

Demo.prototype.doEnd = function() {
  if (this.evalLineIndex < 0) {
    return this.error('Cannot use END interactively');
  }
  if (this.tokens.nextToken() != undefined) {
    return this.error('END does not take any arguments');
  }
  this.currentLineIndex = this.program.length;
};

Demo.prototype.doGoto = function() {
  if (this.evalLineIndex < 0) {
    return this.error('Cannot use GOTO interactively');
  }
  var value = this.expr();
  if (value == undefined) {
    return;
  }
  if (value.type() != TYPE_NUMBER) {
    return this.error('Numeric value expected');
  }
  if (this.tokens.nextToken() != undefined) {
    return this.error('GOTO takes exactly one numeric argument');
  }
  var number = this.toInt(value.val());
  if (number <= 0) {
    return this.error('Range error');
  }
  var idx = this.findLine(number);
  if (idx < 0) {
    return this.error('No line number ' + line);
  }
  this.currentLineIndex = idx;
};

Demo.prototype.doList = function() {
  var start        = undefined;
  var stop         = undefined;
  var token        = this.tokens.nextToken();
  if (token) {
    if (token != '-' && !token.match(/[0-9]+/)) {
      return this.error('LIST can optional take a start and stop line number');
    }
    if (token != '-') {
      start        = parseInt(token);
      token        = this.tokens.nextToken();
    }
    if (!token) {
      stop         = start;
    } else {
      if (token != '-') {
        return this.error('Dash expected');
      }
      token        = this.tokens.nextToken();
      if (token) {
        if (!token.match(/[0-9]+/)) {
          return this.error(
                      'LIST can optionally take a start and stop line number');
        }
        stop       = parseInt(token);
        if (start && stop < start) {
          return this.error('Start line number has to come before stop');
        }
      }
      if (this.tokens.peekToken()) {
        return this.error('Unexpected trailing arguments');
      }
    }
  }

  var listed       = false;
  for (var i = 0; i < this.program.length; i++) {
    var line       = this.program[i];
    var lineNumber = line.lineNumber();
    if (start != undefined && start > lineNumber) {
      continue;
    }
    if (stop != undefined && stop < lineNumber) {
      break;
    }

    listed         = true;
    this.vt100('' + line.lineNumber() + ' ');
    line.tokens().reset();
    var space      = true;
    var id         = false;
    for (var token; (token = line.tokens().nextToken()) != null; ) {
      switch (token) {
        case '=':
        case '+':
        case '-':
        case '*':
        case '/':
        case '\\':
        case '^':
          this.vt100((space ? '' : ' ') + token + ' ');
          space    = true;
          id       = false;
          break;
        case '(':
        case ')':
        case '$':
        case '%':
        case '#':
          this.vt100(token);
          space    = false;
          id       = false;
          break;
        case ',':
        case ';':
        case ':':
          this.vt100(token + ' ');
          space    = true;
          id       = false;
          break;
        case '?':
          token    = 'PRINT';
          // fall thru
        default:
          this.printUnicode((id ? ' ' : '') + token);
          space    = false;
          id       = true;
          break;
      }
    }
    this.vt100('\r\n');
  }
  if (!listed) {
    this.ok();
  }
};

Demo.prototype.doPrint = function() {
  var tokens    = this.tokens;
  var last      = undefined;
  for (var token; (token = tokens.peekToken()); ) {
    last        = token;
    if (token == ',') {
      this.vt100('\t');
      tokens.consume();
    } else if (token == ';') {
      // Do nothing
      tokens.consume();
    } else {
      var value = this.expr();
      if (value == undefined) {
        return;
      }
      this.printUnicode(value.toString());
    }
  }
  if (last != ';') {
    this.vt100('\r\n');
  }
};

Demo.prototype.doExec = function() {
  this.evalLineIndex = this.currentLineIndex++;
  this.tokens        = this.program[this.evalLineIndex].tokens();
  this.tokens.reset();
  this.doEval();
  if (this.currentLineIndex < 0) {
    return false;
  } else if (this.currentLineIndex >= this.program.length) {
    this.currentLineIndex = -1;
    this.ok();
    return false;
  } else {
    this.gotoState(STATE_EXEC, 20);
    return true;
  }
};

Demo.prototype.doNewYN = function() {
  for (var i = 0; i < this.keys.length; ) {
    var ch = this.keys.charAt(i++);
    if (ch == 'n' || ch == 'N' || ch == '\r' || ch == '\n') {
      this.vt100('N\r\n');
      this.keys = this.keys.substr(i);
      this.error('Aborted');
      return false;
    } else if (ch == 'y' || ch == 'Y') {
      this.vt100('Y\r\n');
      this.vars = new Object();
      this.program.splice(0, this.program.length);
      this.keys = this.keys.substr(i);
      this.ok();
      return false;
    } else {
      this.vt100('\u0007');
    }
  }
  this.gotoState(STATE_NEW_Y_N);
  return true;
};

Demo.prototype.findLine = function(lineNumber) {
  var l   = 0;
  var h   = this.program.length;
  while (h > l) {
    var m = Math.floor((l + h) / 2);
    var n = this.program[m].lineNumber();
    if (n == lineNumber) {
      return m;
    } else if (n > lineNumber) {
      h   = m;
    } else {
      l   = m + 1;
    }
  }
  return -l - 1;
};

Demo.prototype.expr = function() {
  var value   = this.term();
  while (value) {
    var token = this.tokens.peekToken();
    if (token != '+' && token != '-') {
      break;
    }
    this.tokens.consume();
    var v     = this.term();
    if (!v) {
      return v;
    }
    if (value.type() != v.type()) {
      if (value.type() != TYPE_STRING) {
        value = new this.Value(TYPE_STRING, ''+value.val(), ''+value.val());
      }
      if (v.type() != TYPE_STRING) {
        v     = new this.Value(TYPE_STRING, ''+v.val(), ''+v.val());
      }
    }
    if (token == '-') {
      if (value.type() == TYPE_STRING) {
        return this.error('Cannot subtract strings');
      }
      v       = value.val() - v.val();
    } else {
      v       = value.val() + v.val();
    }
    if (v == NaN) {
      return this.error('Numeric range error');
    }
    value     = new this.Value(value.type(), ''+v, v);
  }
  return value;
};

Demo.prototype.term = function() {
  var value   = this.expn();
  while (value) {
    var token = this.tokens.peekToken();
    if (token != '*' && token != '/' && token != '\\') {
      break;
    }
    this.tokens.consume();
    var v     = this.expn();
    if (!v) {
      return v;
    }
    if (value.type() != TYPE_NUMBER || v.type() != TYPE_NUMBER) {
      return this.error('Cannot multiply or divide strings');
    }
    if (token == '*') {
      v       = value.val() * v.val();
    } else {
      v       = value.val() / v.val();
      if (token == '\\') {
        v     = this.toInt(v);
      }
    }
    if (v == NaN) {
      return this.error('Numeric range error');
    }
    value     = new this.Value(TYPE_NUMBER, ''+v, v);
  }
  return value;
};

Demo.prototype.expn = function() {
  var value = this.intrinsic();
  var token = this.tokens.peekToken();
  if (token == '^') {
    this.tokens.consume();
    var exp = this.intrinsic();
    if (exp == undefined || exp.val() == NaN) {
      return exp;
    }
    if (value.type() != TYPE_NUMBER || exp.type() != TYPE_NUMBER) {
      return this.error("Numeric value expected");
    }
    var v   = Math.pow(value.val(), exp.val());
    value   = new this.Value(TYPE_NUMBER, '' + v, v);
  }
  return value;
};

Demo.prototype.intrinsic = function() {
  var token         = this.tokens.peekToken();
  var args          = undefined;
  var value, v, fnc, arg1, arg2, arg3;
  if (!token) {
    return this.error('Unexpected end of input');
  } else if (token.match(/^(?:ABS|ASC|ATN|CHR\$|COS|EXP|INT|LEN|LOG|POS|RND|SGN|SIN|SPC|SQR|STR\$|TAB|TAN|VAL)$/)) {
    fnc             = token;
    args            = 1;
  } else if (token.match(/^(?:LEFT\$|RIGHT\$)$/)) {
    fnc             = token;
    args            = 2;
  } else if (token == 'MID$') {
    fnc             = token;
    args            = 3;
  } else if (token == 'TI') {
    this.tokens.consume();
    v               = (new Date()).getTime() / 1000.0;
    return new this.Value(TYPE_NUMBER, '' + v, v);
  } else {
    return this.factor();
  }
  this.tokens.consume();
  token             = this.tokens.nextToken();
  if (token != '(') {
    return this.error('"(" expected');
  }
  arg1              = this.expr();
  if (!arg1) {
    return arg1;
  }
  token             = this.tokens.nextToken();
  if (--args) {
    if (token != ',') {
      return this.error('"," expected');
    }
    arg2            = this.expr();
    if (!arg2) {
      return arg2;
    }
    token = this.tokens.nextToken();
    if (--args) {
      if (token != ',') {
        return this.error('"," expected');
      }
      arg3          = this.expr();
      if (!arg3) {
        return arg3;
      }
      token         = this.tokens.nextToken();
    }
  }
  if (token != ')') {
    return this.error('")" expected');
  }
  switch (fnc) {
  case 'ASC':
    if (arg1.type() != TYPE_STRING || arg1.val().length < 1) {
      return this.error('Non-empty string expected');
    }
    v               = arg1.val().charCodeAt(0);
    value           = new this.Value(TYPE_NUMBER, '' + v, v);
    break;
  case 'LEN':
    if (arg1.type() != TYPE_STRING) {
      return this.error('String expected');
    }
    v               = arg1.val().length;
    value           = new this.Value(TYPE_NUMBER, '' + v, v);
    break;
  case 'LEFT$':
    if (arg1.type() != TYPE_STRING || arg2.type() != TYPE_NUMBER ||
        arg2.type() < 0) {
      return this.error('Invalid arguments');
    }
    v               = arg1.val().substr(0, Math.floor(arg2.val()));
    value           = new this.Value(TYPE_STRING, v, v);
    break;
  case 'MID$':
    if (arg1.type() != TYPE_STRING || arg2.type() != TYPE_NUMBER ||
        arg3.type() != TYPE_NUMBER || arg2.val() < 0 || arg3.val() < 0) {
      return this.error('Invalid arguments');
    }
    v               = arg1.val().substr(Math.floor(arg2.val()),
                                        Math.floor(arg3.val()));
    value           = new this.Value(TYPE_STRING, v, v);
    break;
  case 'RIGHT$':
    if (arg1.type() != TYPE_STRING || arg2.type() != TYPE_NUMBER ||
        arg2.type() < 0) {
      return this.error('Invalid arguments');
    }
    v               = Math.floor(arg2.val());
    if (v > arg1.val().length) {
      v             = arg1.val().length;
    }
    v               = arg1.val().substr(arg1.val().length - v);
    value           = new this.Value(TYPE_STRING, v, v);
    break;   
  case 'STR$':
    value           = new this.Value(TYPE_STRING, arg1.toString(),
                                     arg1.toString());
    break;
  case 'VAL':
    if (arg1.type() == TYPE_NUMBER) {
      value         = arg1;
    } else {
      if (arg1.val().match(/^[0-9]+$/)) {
        v           = parseInt(arg1.val());
      } else {
        v           = parseFloat(arg1.val());
      }
      value         = new this.Value(TYPE_NUMBER, '' + v, v);
    }
    break;
  default:
    if (arg1.type() != TYPE_NUMBER) {
      return this.error('Numeric value expected');
    }
    switch (fnc) {
    case 'CHR$':
      if (arg1.val() < 0 || arg1.val() > 65535) {
        return this.error('Invalid Unicode range');
      }
      v             = String.fromCharCode(arg1.val());
      value         = new this.Value(TYPE_STRING, v, v);
      break;
    case 'SPC': 
      if (arg1.val() < 0) {
        return this.error('Range error');
      }
      v             = arg1.val() >= 1 ?
                      '\u001B[' + Math.floor(arg1.val()) + 'C' : '';
      value         = new this.Value(TYPE_STRING, v, v);
      break;
    case 'TAB':
      if (arg1.val() < 0) {
        return this.error('Range error');
      }
      v             = '\r' + (arg1.val() >= 1 ?
                      '\u001B[' + (Math.floor(arg1.val())*8) + 'C' : '');
      value         = new this.Value(TYPE_STRING, v, v);
      break;
    default:
      switch (fnc) {
      case 'ABS': v = Math.abs(arg1.val());                     break;
      case 'ATN': v = Math.atan(arg1.val());                    break;
      case 'COS': v = Math.cos(arg1.val());                     break;
      case 'EXP': v = Math.exp(arg1.val());                     break;
      case 'INT': v = Math.floor(arg1.val());                   break;
      case 'LOG': v = Math.log(arg1.val());                     break;
      case 'POS': v = this.cursorX;                             break;
      case 'SGN': v = arg1.val() < 0 ? -1 : arg1.val() ? 1 : 0; break;
      case 'SIN': v = Math.sin(arg1.val());                     break;
      case 'SQR': v = Math.sqrt(arg1.val());                    break;
      case 'TAN': v = Math.tan(arg1.val());                     break;
      case 'RND':
        if (this.prng == undefined) {
          this.prng = 1013904223;
        }
        if (arg1.type() == TYPE_NUMBER && arg1.val() < 0) {
          this.prng = Math.floor(1664525*arg1.val()) & 0xFFFFFFFF;
        }
        if (arg1.type() != TYPE_NUMBER || arg1.val() != 0) {
          this.prng = Math.floor(1664525*this.prng + 1013904223) &
                      0xFFFFFFFF;
        }
        v           = ((this.prng & 0x7FFFFFFF) / 65536.0) / 32768;
        break;
      }
      value         = new this.Value(TYPE_NUMBER, '' + v, v);
    }
  }
  if (v == NaN) {
    return this.error('Numeric range error');
  }
  return value;
};

Demo.prototype.factor = function() {
  var token    = this.tokens.nextToken();
  var value;
  if (token == '-') {
    value      = this.expr();
    if (!value) {
      return value;
    }
    if (value.type() != TYPE_NUMBER) {
      return this.error('Numeric value expected');
    }
    return new this.Value(TYPE_NUMBER, '' + -value.val(), -value.val());
  }
  if (!token) {
    return this.error();
  }
  if (token == '(') {
    value      = this.expr();
    token      = this.tokens.nextToken();
    if (token != ')' && value != undefined) {
      return this.error('")" expected');
    }
  } else {
    var str;
    if ((str = token.match(/^"(.*)"/)) != null) {
      value    = new this.Value(TYPE_STRING, str[1], str[1]);
    } else if (token.match(/^[0-9]/)) {
      var number;
      if (token.match(/^[0-9]*$/)) {
        number = parseInt(token);
      } else {
        number = parseFloat(token);
      }
      if (number == NaN) {
        return this.error('Numeric range error');
      }
      value    = new this.Value(TYPE_NUMBER, token, number);
    } else if (token.match(/^[A-Za-z][A-Za-z0-9_]*$/)) {
      if (this.tokens.peekToken() == '$') {
        this.tokens.consume();
        var arr= this.arrayIndex();
        if (arr == undefined) {
          return arr;
        }
        value  = this.vars['str_' + token + arr];
        if (value == undefined) {
          value= new this.Value(TYPE_STRING, '', '');
        }
      } else {
        var n  = 'var_';
        if (this.tokens.peekToken() == '%') {
          this.tokens.consume();
          n    = 'int_';
        }
        var arr= this.arrayIndex();
        if (arr == undefined) {
          return arr;
        }
        value  = this.vars[n + token + arr];
        if (value == undefined) {
          value= new this.Value(TYPE_NUMBER, '0', 0);
        }
      }
    } else {
      return this.error();
    }
  }

  return value;
};

Demo.prototype.Tokens = function(line) {
  this.line   = line;
  this.tokens = line;
  this.len    = undefined;
};

Demo.prototype.Tokens.prototype.peekToken = function() {
  this.len      = undefined;
  this.tokens   = this.tokens.replace(/^[ \t]*/, '');
  var tokens    = this.tokens;
  if (!tokens.length) {
    return null;
  }
  var token     = tokens.charAt(0);
  switch (token) {
  case '<':
    if (tokens.length > 1) {
      if (tokens.charAt(1) == '>') {
        token   = '<>';
      } else if (tokens.charAt(1) == '=') {
        token   = '<=';
      }
    }
    break;
  case '>':
    if (tokens.charAt(1) == '=') {
      token     = '>=';
    }
    break;
  case '=':
  case '+':
  case '-':
  case '*':
  case '/':
  case '\\':
  case '^':
  case '(':
  case ')':
  case '?':
  case ',':
  case ';':
  case ':':
  case '$':
  case '%':
  case '#':
    break;
  case '"':
    token       = tokens.match(/"((?:""|[^"])*)"/); // "
    if (!token) {
      token     = undefined;
    } else {
      this.len  = token[0].length;
      token     = '"' + token[1].replace(/""/g, '"') + '"';
    }
    break;
  default:
    if (token >= '0' && token <= '9' || token == '.') {
      token     = tokens.match(/^[0-9]*(?:[.][0-9]*)?(?:[eE][-+]?[0-9]+)?/);
      if (!token) {
        token   = undefined;
      } else {
        token   = token[0];
      }
    } else if (token >= 'A' && token <= 'Z' ||
               token >= 'a' && token <= 'z') {
      token     = tokens.match(/^(?:CHR\$|STR\$|LEFT\$|RIGHT\$|MID\$)/i);
      if (token) {
        token   = token[0].toUpperCase();
      } else {
        token   = tokens.match(/^[A-Za-z][A-Za-z0-9_]*/);
        if (!token) {
          token = undefined;
        } else {
          token = token[0].toUpperCase();
        }
      }
    } else {
      token     = '';
    }
  }

  if (this.len == undefined) {
    if (token) {
      this.len  = token.length;
    } else {
      this.len  = 1;
    }
  }

  return token;
};

Demo.prototype.Tokens.prototype.consume = function() {
  if (this.len) {
    this.tokens = this.tokens.substr(this.len);
    this.len    = undefined;
  }
};

Demo.prototype.Tokens.prototype.nextToken = function() {
  var token = this.peekToken();
  this.consume();
  return token;
};

Demo.prototype.Tokens.prototype.removeLineNumber = function() {
  this.line = this.line.replace(/^[0-9]*[ \t]*/, '');
};

Demo.prototype.Tokens.prototype.reset = function() {
  this.tokens = this.line;
};

Demo.prototype.Line = function(lineNumber, tokens) {
  this.lineNumber_ = lineNumber;
  this.tokens_     = tokens;
};

Demo.prototype.Line.prototype.lineNumber = function() {
  return this.lineNumber_;
};

Demo.prototype.Line.prototype.tokens = function() {
  return this.tokens_;
};

Demo.prototype.Line.prototype.setTokens = function(tokens) {
  this.tokens_ = tokens;
};

Demo.prototype.Line.prototype.sort = function(a, b) {
  return a.lineNumber_ - b.lineNumber_;
};

Demo.prototype.Value = function(type, str, val) {
  this.t = type;
  this.s = str;
  this.v = val;
};

Demo.prototype.Value.prototype.type = function() {
  return this.t;
};

Demo.prototype.Value.prototype.val = function() {
  return this.v;
};

Demo.prototype.Value.prototype.toString = function() {
  return this.s;
};

