//
// Applesoft BASIC Interpreter in Javascript
// BASIC Interpreter and WSH wrapper
//

// Copyright 2009 Joshua Bell
// 
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// 
// http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


/*extern WScript, ActiveXObject, TEXT_STYLE_NORMAL, TEXT_STYLE_INVERSE, TEXT_STYLE_FLASH */
/*global console */
// TODO: Lines aren't reordered based on numbers! Numbers are just goto targets/if delimiters, really!

// Exported Constants
var STATE_STOPPED = 0;
var STATE_RUNNING = 1;
var STATE_BLOCKED = 2;

// Usage:
//   var interpreter = new BasicInterpreter( tty [, lores [, hires [, function readPDL(n) { ... } ] ] ] )
//   interpreter.load( string )            // Load code
//   interpreter.run( [line_num] )         // Start execution (at specified line)
//   state = interpreter.step( callback )  // Execute one statement; call asynchronously while 
//                                         // STATE_RUNNING is returned. Callback will be
//                                         // called after blocking input.
//   string = interpreter.list()           // Serialize the program as a string

// Example:
//   function step() { 
//       if( interpreter.step( step ) === STATE_RUNNING ) {
//           setTimeout( step, 0 );
//       }
//   }

if (typeof console === 'undefined') { console = {}; }
if (typeof console.log === 'undefined') { console.log = function() { }; }


if (!Array.prototype.map) {
    Array.prototype.map = function(func) {
        var a = [];
        for (var i = 0; i < this.length; ++i) {
            a.push(func(this[i]));
        }
        return a;
    };
}

var SHOW_TRACE_OUTPUT = false;
var SHOW_DEBUG_OUTPUT = false;

function BasicInterpreter( terminal, lores_graphics, hires_graphics, paddle )
{
    // For references to "this" within callbacks and closures
    var self = this;


    //----------------------------------------------------------------------
    function error( reason )
    //----------------------------------------------------------------------
    {
        throw { 
            catchableError: true,
            errorMessage:  reason + ", line: " + self.context.current_line_number 
        };
    
    } // error

    //----------------------------------------------------------------------
    function abort( reason )
    //----------------------------------------------------------------------
    {
        throw { errorMessage: reason };
        
    } // abort

    var RESERVED_WORDS = 
    [
        // NOTE: keywords that are stems of other words need to go after (e.g. "NOTRACE", "NOT)
        "ABS", "AND", "ASC", "ATN", "AT", "CALL", "CHR$", "CLEAR", "COLOR\\s*=", "CONT", "COS",
        "DATA", "DEF", "DEL", "DIM", "DRAW", "END", "EXP", "FLASH", "FN", "FOR", "FRE", "GET",
        "GOSUB", "GOTO", "GR", "HCOLOR\\s*=", "HGR2", "HGR", "HIMEM\\s*:", "HLIN", "HOME", "HPLOT",
        "HTAB", "IF", "IN#", "INPUT", "INT", "INVERSE", "LEFT$", "LEN", "LET", "LIST",
        "LOAD", "LOG", "LOMEM\\s*:", "MID$", "NEW", "NEXT", "NORMAL", "NOTRACE", "NOT", "ONERR", 
        "ON", "OR", "PDL", "PEEK", "PLOT", "POKE", "POP", "POS", "PRINT", "PR#", "READ",
        "RECALL", "REM", "RESTORE", "RESUME", "RETURN", "RIGHT$", "RND", "ROT\\s*=", "RUN",
        "SAVE", "SCALE\\s*=", "SCRN", "SGN", "SHLOAD", "SIN", "SPC", "SPEED\\s*=", "SQR", "STEP",
        "STOP", "STORE", "STR$", "TAB", "TAN", "TEXT", "THEN", "TO", "TRACE", "USR", "VAL",
        "VLIN", "VTAB", "WAIT", "XDRAW", "&", "\\?"
    ];

    var regexReservedWords = new RegExp( "^\\s*("+RESERVED_WORDS.join("|").replace(/\$/g,"\\$")+")\\s*(.*?)\\s*$", "i" );
    var regexIdentifier    = /^\s*([A-Za-z][A-Za-z0-9]?)[A-Za-z0-9]*(\$|\%)?\s*(.*?)\s*$/;
    var regexStringLiteral = /^\s*"([^"]*)(?:"|$)(.*?)\s*$/;
    var regexNumberLiteral = /^\s*([0-9]*\.?[0-9]+(?:[eE]\s*[\-+]?\s*[0-9]+)?)\s*(.*?)\s*$/;
    var regexOperator      = /^\s*([;=<>+\-*\/\^(),\$\%\.])\s*(.*?)\s*$/;
    var regexSeparator     = /^\s*([:])\s*(.*?)\s*$/;
    
    //----------------------------------------------------------------------
    function Token()
    //----------------------------------------------------------------------
    {
        this.toString = function()
        {
            if( this.reserved !== undefined )
            {
                return "[RESERVED: " + this.reserved + " ]";
            }
            else if( this.identifier !== undefined )
            {
                return "[IDENTIFIER: " + this.identifier + " ]";
            }
            else if( this.string !== undefined )
            {
                return "[STRING: " + this.string + " ]";
            }
            else if( this.number !== undefined )
            {
                return "[NUMBER: " + this.number + " ]";
            }
            else if( this.operator !== undefined )
            {
                return "[OPERATOR: " + this.operator + " ]";
            }
            else if( this.statementSeparator !== undefined )
            {
                return "[:]";
            }
            else if( this.lineNumber !== undefined )
            {
                return "[LINE: "+this.lineNumber+" ]";
            }
            else
            {
                abort( "Unknown token type!" );
                return undefined;
            }
        };
        
        this.list = function()
        {
            if( this.reserved !== undefined )
            {
                return " " + this.reserved + " ";
            }
            else if( this.identifier !== undefined )
            {
                return this.identifier;
            }
            else if( this.string !== undefined )
            {
                return '"' + this.string + '"';
            }
            else if( this.number !== undefined )
            {
                return this.number.toString();
            }
            else if( this.operator !== undefined )
            {
                if( /[+\-*\/\^=]/.test(this.operator) )
                {
                    return " " + this.operator + " ";
                }
                else
                {
                    return this.operator;
                }
            }
            else if( this.statementSeparator !== undefined )
            {
                return ":";
            }
            else if( this.lineNumber !== undefined )
            {
                return "\r" + this.lineNumber + " ";
            }
            else if( this.comment !== undefined )
            {
                return this.comment;
            }
            else
            {
                abort( "Unknown token type!" );
                return undefined;
            }
        };
        
        this.endOfStatement = function()
        {
            return ( this.statementSeparator !== undefined ) || ( this.lineNumber !== undefined );
        };
    }
    
    //----------------------------------------------------------------------
    function Tokenizer( string )
    //----------------------------------------------------------------------
    {
        this.hasMoreTokens = function()
        {
            return string !== undefined && string !== "";
            
        }; // hasMoreTokens
        
        this.nextToken = function()
        {
            if( string === undefined )
            {
                return undefined;
            }
            
            var token = new Token();

            if( string.match( regexReservedWords ) )
            {
                // Reserved word
                token.reserved = RegExp.$1.toUpperCase();
                string = RegExp.$2;
                
                if( token.reserved === "?" ) { token.reserved = "PRINT"; }// HACK
                
                token.reserved = token.reserved.replace(/\s+/g,""); // HACK: for "COLOR =" support; dirties RegExp
            } 
            else if( string.match( regexIdentifier ) )
            {
                // Identifier
                token.identifier = RegExp.$1.toUpperCase() + RegExp.$2; // Canonicalize identifier name
                string = RegExp.$3;
            } 
            else if( string.match( regexStringLiteral ) )
            {
                // String literal
                token.string = RegExp.$1;
                string = RegExp.$2;
            } 
            else if( string.match( regexNumberLiteral ) )
            {
                // Number literal
                token.number = RegExp.$1;
                string = RegExp.$2;

                // The following dirties RegExp.$n so it is kept separate
                token.number = parseFloat( token.number.replace( /\s+/g, ''), 10 );
            } 
            else if( string.match( regexOperator ) )
            {
                // Operator
                token.operator = RegExp.$1;
                string = RegExp.$2;
            } 
            else if( string.match( regexSeparator ) )
            {
                // Statement separator
                token.statementSeparator = RegExp.$1;
                string = RegExp.$2;
            } 
            else
            {
                abort( "Couldn't match token: '" + string + "'" );
            }

            if( string === "" )
            {
                string = undefined;
            }
            
            return token;
            
        }; // nextToken
        
        this.nextAsComment = function()
        {
            var token = new Token();
            token.comment = string;

            string = undefined;

            return token;
            
        }; // nextAsComment
    }

    //----------------------------------------------------------------------
    function Context( tokens )
    //----------------------------------------------------------------------
    {
        var self = this;
        
        self.tokens = tokens;
        self.current_token       = undefined;
        self.current_statement   = undefined; // Start token of current statement
        self.current_line_number = undefined;
        
        this.nextToken = function( type )
        {
            if( !self.hasMoreTokens() )
            {
                error( "Syntax error: Unexpected end of file" );
            }
            var token = self.tokens[ self.current_token++ ];
            
            if( type !== undefined && token[ type ] === undefined )
            {
                error( "Syntax error: Expected " + type + ", saw " + token );
            }
            
            return token;
        };

        this.peekToken = function()
        {
            return self.tokens[ self.current_token ];
        };
        
        this.requireToken = function( type, value )
        {
            if( !self.hasMoreTokens() )
            {
                error( "Syntax error: Unexpected end of file" );
            }
            
            var token = self.nextToken();
            if( token[ type ] === undefined || token[ type ] !== value )
            {
                error( "Syntax error: Expected: '" + value + "', saw " + token);
            }
            
            return true;
        };

        this.endOfStatement = function()
        {
            if( !self.hasMoreTokens() )
            {
                return true;
            }
            
            return self.tokens[ self.current_token ].endOfStatement();
        };

        this.hasMoreTokens = function()
        {
            return self.tokens.length > self.current_token;
        };
        
        this.toString = function()
        {
            var list = [];
            for( var i = self.current_token; i < self.tokens.length && self.tokens[i]; ++i )
            {
                if( self.tokens[i].endOfStatement() )
                {
                    break;
                }
                    
                list.push( self.tokens[i] );
            }
            
            return list.join( ", " );
        };
    }
    
    //----------------------------------------------------------------------
    // Internal Fields
    //----------------------------------------------------------------------
    // Exposed on object for easier debugging; these should be hidden
    self.tty = terminal;
    self.lores = lores_graphics;
    self.hires = hires_graphics;
    self.variables = {};
    self.arrays = {};
    self.functions = {};
    self.stack = [];

    self.trace_mode = false;
    self.tokens = [];
    self.line_offsets = [];
    self.context = new Context( self.tokens );
    self.data_token = undefined;
    self.data_line_number = undefined;
    self.data_statement = false;

    self.onerr_handler = undefined;
    self.onerr_resume_token = undefined;
    self.onerr_resume_line_number = undefined;
    
    //----------------------------------------------------------------------
    // Internal Methods
    //----------------------------------------------------------------------

    //----------------------------------------------------------------------
    function trace( comment )
    //----------------------------------------------------------------------
    {
        if( SHOW_TRACE_OUTPUT )
        {
            self.tty.printError( "<"+self.context.current_line_number+">  " + comment + "\n" );
        }
        
    } // trace

    //----------------------------------------------------------------------
    function debug( comment )
    //----------------------------------------------------------------------
    {
        if( SHOW_DEBUG_OUTPUT )
        {
            self.tty.printError( "["+self.context.current_line_number+"]  " + comment + "\n" );
        }
        
    } // debug

    //----------------------------------------------------------------------
    function dimArray( subscripts, p )
    //----------------------------------------------------------------------
    {
        if( p === undefined )
        {
            p = 0;
        }
        
        var l = subscripts[ p ] + 1; // DIM A(10) makes 11 slots!
        var a = [];
        a.length = l;
        var i;
        p += 1;

        if( p < subscripts.length  )
        {
            for( i = 0; i < l; ++i )
            {
                a[i] = dimArray( subscripts, p );
            }
        }

        return a;
    } // dimArray


    //----------------------------------------------------------------------
    function clampTo16Bits( n )
    //----------------------------------------------------------------------
    {
        n = Math.floor(n) % 65536;
        if( n < 0 )
        {
            return 65536 + n;
        }
        else
        {
            return n;
        }
    } // clampTo16Bits


    var displayState = { graphics: false, full: true, lores: true };

    //----------------------------------------------------------------------
    function setDisplayState( state, value )
    //----------------------------------------------------------------------
    {
        if( displayState[ state ] === value )
        {
            return;
        }
        
        displayState[ state ] = value;
        
        if( displayState.graphics )
        {
            if( self.lores ) { self.lores.show( displayState.lores ); }
            if( self.hires ) { self.hires.show( !displayState.lores ); }
            if( self.tty.splitScreen ) { self.tty.splitScreen( self.tty.getScreenSize().height - ( displayState.full ? 0 : 4 ) ); }
        }
        else
        {
            if( self.lores ) { self.lores.show( false ); }
            if( self.hires ) { self.hires.show( false ); }
            if( self.tty.splitScreen ) { self.tty.splitScreen( 0 ); }
        }
    } // setDisplayState


    //----------------------------------------------------------------------
    // Peeks, Pokes and Pointers
    //----------------------------------------------------------------------

    var peek_table = [];
    var poke_table = [];

    peek_table[0xC000] = function() { return self.tty.getKeyboardRegister ? self.tty.getKeyboardRegister() : 0; };
    peek_table[0xC010] = function() { return self.tty.clearKeyboardStrobe ? self.tty.clearKeyboardStrobe() : 0; };
    peek_table[0xC030] = function() { return 0; }; // speaker toggle
    peek_table[0xC060] = function() { return self.tty.getButtonState ? self.tty.getButtonState(3) : 0; };
    peek_table[0xC061] = function() { return self.tty.getButtonState ? self.tty.getButtonState(0) : 0; };
    peek_table[0xC062] = function() { return self.tty.getButtonState ? self.tty.getButtonState(1) : 0; };
    peek_table[0xC063] = function() { return self.tty.getButtonState ? self.tty.getButtonState(2) : 0; };

    // Text window       
    poke_table[0x0020] = function(v) { if (self.tty.textWindow) { self.tty.textWindow.left = v; } };
    poke_table[0x0021] = function(v) { if (self.tty.textWindow) { self.tty.textWindow.width = v; } };
    poke_table[0x0022] = function(v) { if (self.tty.textWindow) { self.tty.textWindow.top = v; } };
    poke_table[0x0023] = function(v) { if (self.tty.textWindow) { self.tty.textWindow.height = v - self.tty.textWindow.top; } };

    // ONERR flag       
    poke_table[0x00D8] = function(v) { if (v < 0x80) { self.onerr_handler = undefined; } };

    // Keyboard strobe             
    poke_table[0xC010] = function(v) { if (self.tty.clearKeyboardStrobe) { self.tty.clearKeyboardStrobe(); } };

    // Display switches         
    poke_table[0xC050] = function(v) { setDisplayState("graphics", true); }; // Graphics                        
    poke_table[0xC051] = function(v) { setDisplayState("graphics", false); }; // Text
    poke_table[0xC052] = function(v) { setDisplayState("full", true); }; // Full Graphics
    poke_table[0xC053] = function(v) { setDisplayState("full", false); }; // Split Screen
    poke_table[0xC056] = function(v) { setDisplayState("lores", true); }; // Lo-Res
    poke_table[0xC057] = function(v) { setDisplayState("lores", false); }; // Hi-Res

    // Speaker toggle          
    poke_table[0xC030] = function(v) { }; // no-op


    //----------------------------------------------------------------------
    function findVariable( name, subscripts, is_get )
    //----------------------------------------------------------------------
    {
        //trace("findVariable: " + name );
        
        var variable, i, array, dims;

        if( subscripts !== undefined && subscripts.length > 0 )
        {
            array = self.arrays[ name ];
            if( array === undefined )
            {
                // New array - DIM A(10,...) for each dimension
                dims = [];
                for( i = 0; i < subscripts.length; ++i )
                {
                    dims.push( 10 );
                }
                
                array = dimArray( dims );
                self.arrays[ name ] = array;
            }

            // Walk the array chain
            while( subscripts.length > 1 && array !== undefined )
            {
                array = array[ subscripts.shift() ];
            }
            if( array === undefined )
            {
                error( "Bad subscript error" );
            }
            
            i = subscripts.shift();
            if( i >= array.length )
            {
                error( "Bad subscript error" );
            }
            variable = array[ i ];
            if( variable === undefined )
            {
                if (is_get) { console.log("Access to undefined variable: " + name + "() at line: " + self.context.current_line_number); }
                //if( !is_get ) { console.log("First set: " + name + "() at line: " + self.context.current_line_number ); }
                variable = { value: (name.charAt(name.length - 1) === "$") ? "" : 0 };
                array[i] = variable;
            }
        }
        else
        {
            variable = self.variables[ name ];
            
            if( variable === undefined )
            {
                if( is_get ) { console.log("Access to undefined variable: " + name + " at line: " + self.context.current_line_number ); }
                //if( !is_get ) { console.log("First set: " + name + " at line: " + self.context.current_line_number ); }
                variable = { value: (name.charAt(name.length - 1) === "$") ? "" : 0 };
                self.variables[name] = variable;
            }
        }
                    
        return variable;

    } // findVariable


    //----------------------------------------------------------------------
    function getVariable( name, subscripts )
    //----------------------------------------------------------------------
    {
        //trace("getVariable: " + name );

        var variable = findVariable( name, subscripts, true );
        return variable.value;
        
    } // getVariable
    
    //----------------------------------------------------------------------
    function setVariable( name, subscripts, value )
    //----------------------------------------------------------------------
    {
        //trace( "name: " + name  + " = " + value );
        
        var variable = findVariable( name, subscripts, false );
        
        if( name.charAt(name.length-1) === "$" )
        {
            variable.value = value.toString();
        }
        else if( name.charAt(name.length-1) === "%" )
        {
            variable.value = Math.floor(value);
        }
        else
        {
            variable.value = Number(value);
        }
       
    } // setVariable



    // Grammar for recursive descent parser/evaluator
    //
    // Expression               := OrExpression
    // OrExpression             := AndExpression [ 'OR' AndExpression ... ]
    // AndExpression            := RelationalExpression [ 'AND' RelationalExpression ... ]
    // RelationalExpression     := AdditiveExpression [ ( '=' | '<' | '>' | '<=' | '=<' | '>=' | '=>' | '<>' | '><' ) AdditiveExpression ... ]
    // AdditiveExpression       := MultiplicativeExpression [ ( '+' | '-' ) MultiplicativeExpression ... ]
    // MultiplicativeExpression := PowerExpression [ ( '*' | '/' ) PowerExpression ... ]
    // PowerExpression          := UnaryExpression [ '^' UnaryExpression ]
    // UnaryExpression          := ( '+' | '-' | 'NOT' ) UnaryExpression 
    //                           | FinalExpression
    // FinalExpression          := '(' Expression ')' 
    //                           | function_name '(' Expression ')' 
    //                           | 'FN' user_function_name '(' Expression ')' 
    //                           | identifier [ '(' Expression [, Expression ...] ')' ]
    //                           | string-literal 
    //                           | number-literal
    
    
    

    //----------------------------------------------------------------------
    function parseExpression()
    //----------------------------------------------------------------------
    {
        //trace( "parseExpression: " + self.context );

        var f = parseOrExpression();
        return function() {
            var result = f();

            // HACK: Round numbers to 6 decimal places, to avoid 1.000000001-style results
            // since Javascript has much higher precision than Applesoft. The real fix
            // would be to round to significant figures instead.
            if (typeof result === "number") {
                result = Math.round(result * 1e8) / 1e8;
            }

            return result;
        };
                
    } // parseExpression

    //----------------------------------------------------------------------
    function evaluateExpression()
    //----------------------------------------------------------------------
    {
        var f;

        // Use the previously compiled version of the expression, if present
        var expr_token = self.tokens[self.context.current_token];
        if (expr_token && expr_token.expr_func && expr_token.post_expr_token) {
            // Yay, it's there - call it and skip past the expression tokens
            f = expr_token.expr_func;
            self.context.current_token = expr_token.post_expr_token;
        }
        else {
            // Nope, haven't compiled it yet - do so, and save for later
            f = parseExpression();
            expr_token.expr_func = f;
            expr_token.post_expr_token = self.context.current_token;
        }
        return f();
    }

    //----------------------------------------------------------------------
    function parseStringExpression()
    //----------------------------------------------------------------------
    {
        var f = parseExpression();
        return function() {
            var result = f();
            if (typeof result !== "string") {
                error("Type mismatch error");
            }

            return result;
        };
        
    } // parseStringExpression

    //----------------------------------------------------------------------
    function stringExpression()
    //----------------------------------------------------------------------
    {
        var result = evaluateExpression();
        if (typeof result !== "string") {
            error("Type mismatch error");
        }

        return result;
    }

    
    //----------------------------------------------------------------------
    function parseArithmeticExpression()
    //----------------------------------------------------------------------
    {
        var f = parseExpression();
        return function() {
            var result = f();
            if (typeof result !== "number") {
                error("Type mismatch error");
            }

            return result;
        };
                
    } // parseArithmeticExpression

    //----------------------------------------------------------------------
    function arithmeticExpression()
    //----------------------------------------------------------------------
    {
        var result = evaluateExpression();
        if (typeof result !== "number") {
            error("Type mismatch error");
        }

        return result;        
    }

    //----------------------------------------------------------------------
    function parseUserFunction()
    //----------------------------------------------------------------------
    { 
        var name = self.context.nextToken( "identifier" ).identifier;
        var fn = self.functions[ name ];
        if( fn === undefined )
        {
            error( "Undefined function: " + name );
        }

        // Parse the function
        var oldToken = self.context.current_token;
        self.context.current_token = fn.token;
        var userfunc = parseExpression();
        self.context.current_token = oldToken;

        // Determine the function argument
        self.context.requireToken( "operator", "(" );
        var argfunc = parseExpression();
        self.context.requireToken( "operator", ")" );

        return function() {
            // Save the current context/variable so we can evaluate
            var oldValue = getVariable(fn.param);

            // Swap in the argument
            setVariable(fn.param, undefined, argfunc());
            var result = userfunc();

            // Restore
            setVariable(fn.param, undefined, oldValue);

            return result;
        };
        
    } // parseUserFunction

    //----------------------------------------------------------------------
    function parseSubscripts()
    //----------------------------------------------------------------------
    { 
        var subscripts; // undefined = no subscripts
        
        if( self.context.hasMoreTokens() && self.context.peekToken().operator !== undefined && self.context.peekToken().operator === "(" )
        {
            self.context.nextToken(); // consume "("

            subscripts = [];

            var floor_of = function(f) { return function() { return Math.floor(f()); }; };

            while (true) {

                var ssexpr = parseArithmeticExpression();

                subscripts.push(floor_of(ssexpr));
                
                if( !( self.context.hasMoreTokens() && self.context.peekToken().operator && self.context.peekToken().operator === "," ) )
                {
                    break;
                }
                self.context.nextToken(); // consume ","
            }
            
            self.context.requireToken( "operator", ")" );		
        }
        
        return subscripts;
        
    } // parseSubscripts

    function getSubscripts() {

        var subscripts = parseSubscripts();
        if (subscripts !== undefined) {
            subscripts = subscripts.map(function(x) { return x(); });
        }
        return subscripts;
    }

    // Adapted from:
    // http://stackoverflow.com/questions/424292/how-to-create-my-own-javascript-random-number-generator-that-i-can-also-set-the-s
    function PRNG() {
        this.S = 2345678901; // seed
        this.A = 48271; // const
        this.M = 2147483647; // const
        this.Q = this.M / this.A; // const
        this.R = this.M % this.A; // const
        this.next = function() {
            var hi = this.S / this.Q;
            var lo = this.S % this.Q;
            var t = this.A * lo - this.R * hi;
            this.S = (t > 0) ? t : t + this.M;
            return this.S / this.M;
        };
        this.seed = function(x) {
            this.S = x;
        };
    }
    var prng = new PRNG();
    var randomNumberField = prng.next();


    //----------------------------------------------------------------------
    function parseFunction( name )
    //----------------------------------------------------------------------
    {
        //trace( "evaluateFunction: " + name + " : " + self.context );

        var result = {};
        var a1, a2, a3;
        
        var pa = parseArithmeticExpression;
        var ps = parseStringExpression;
                
        self.context.requireToken( "operator", "(" );

        switch( name )
        {
            case "ABS": a1 = pa(); result = function() { return Math.abs(a1()); }; break;
            case "ASC": a1 = ps(); result = function() { return a1().charCodeAt(0); }; break;
            case "ATN": a1 = pa(); result = function() { return Math.atan(a1()); };  break;
            case "CHR$": a1 = pa(); result = function() { return String.fromCharCode(a1()); }; break;
            case "COS": a1 = pa(); result = function() { return Math.cos(a1()); }; break;
            case "EXP": a1 = pa(); result = function() { return Math.exp(a1()); }; break;
            case "INT": a1 = pa(); result = function() { return Math.floor(a1()); }; break;
            case "LEN": a1 = ps(); result = function() { return a1().length; }; break;
            case "LOG": a1 = pa(); result = function() { return Math.log(a1()); }; break;
            case "SGN": a1 = pa(); result = function() { var r = a1(); return r > 0 ? 1 : r < 0 ? -1 : 0; }; break;
            case "SIN": a1 = pa(); result = function() { return Math.sin(a1()); }; break;
            case "SQR": a1 = pa(); result = function() { return Math.sqrt(a1()); }; break;
            case "STR$": a1 = pa(); result = function() { return a1().toString(); }; break;
            case "TAN": a1 = pa(); result = function() { return Math.tan(a1()); }; break;
            case "VAL": a1 = ps(); result = function() { return parseFloat(a1()); }; break;

            case "RND":
                a1 = pa();
                result = function() {
                    var aexpr = a1();
                    if (aexpr > 0) {
                        // Next in PRNG sequence
                        randomNumberField = prng.next();  //Math.random();
                    }
                    else if (aexpr < 0) {
                        // Re-seed. NOTE: Not predictable as in Applesoft
                        prng.seed(aexpr);
                        randomNumberField = prng.next();  //Math.random(aexpr);
                    }
                    return randomNumberField;
                };
                break;

            case "LEFT$":
                a1 = ps();
                self.context.requireToken("operator", ",");
                a2 = pa();
                result = function() { return a1().substr(0, a2()); };
                break;

            case "MID$":
                a1 = ps();
                self.context.requireToken("operator", ",");
                a2 = pa();
                if (self.context.peekToken().operator === ",") {
                    self.context.requireToken("operator", ",");
                    a3 = pa();
                    result = function() { return a1().substr(a2() - 1, a3()); };
                }
                else {
                    result = function() { return a1().substr(a2() - 1); };
                }
                //debug( "MID$('"+expr+"',"+start+","+len+") = '" + result + "'" );
                break;

            case "RIGHT$":
                a1 = ps();
                self.context.requireToken("operator", ",");
                a2 = pa();
                result = function() { var s = a1(), len = a2(); return (s.length <= len) ? s : s.substr(s.length - len); };
                break;

            case "POS": 	// Returns horizontal cursor position
                parseExpression(); // Result ignored
                result = function() { return self.tty.getCursorPosition().x; };
                break;

            case "SCRN": // Return lores color at SCRN(x,y)
                a1 = pa();
                self.context.requireToken("operator", ",");
                a2 = pa();
                result = function() {
                    var x = Math.floor(a1());
                    var y = Math.floor(a2());

                    if (self.lores) {
                        if (x < 0 ||
                            y < 0 ||
                            x >= self.lores.getScreenSize().width ||
                            y >= self.lores.getScreenSize().height) {
                            error("Illegal quantity error");
                        }

                        return self.lores.getPixel(x, y);
                    }
                    else {
                        error("Graphics not supported");
                    }
                };
                break;

            case "PDL": 	// Paddle position
                a1 = parseExpression(); // Applesoft allows for string expressions here
                result = function() {
                    if (paddle) {
                        return Math.floor(paddle(a1()) * 255);
                    }
                    else {
                        error("Paddles not attached");
                    }
                };
                break;

            case "FRE":     // Garbage collection strings
                parseExpression(); // Result ignored
                result = function() { return 0; }; // TODO: Return more useful data here
                break;

            case "PEEK": // Read memory location
                a1 = pa();
                result = function() {
                    var aexpr = clampTo16Bits(a1());
                    var peek_func = peek_table[aexpr];
                    if (peek_func) {
                        return peek_func();
                    }
                    else {
                        error("Unsupported PEEK location: " + aexpr);
                    }
                };
                break;                

            // Not supported
            case "USR":		// Call hooked routine
                abort( "Function not supported: " + name );
                break;
        }
        
        self.context.requireToken( "operator", ")" );
        
        return result;
        
    } // parseFunction

    //----------------------------------------------------------------------
    function parseFinalExpression()
    //----------------------------------------------------------------------
    {
        //trace( "parseFinalExpression: " + self.context );

        var token = self.context.nextToken();
        var result = {};
        var name, subscripts;
        
        if( token.number !== undefined ) {
            var n = token.number;
            result = function() { return n; };
        }
        else if( token.string !== undefined ) {
            var s = token.string;
            result = function() { return s; };
        }
        else if( token.reserved !== undefined && token.reserved === "FN" )
        {
            result = parseUserFunction();
        }
        else if( token.reserved !== undefined )
        {
            result = parseFunction( token.reserved );
        }
        else if( token.identifier !== undefined )
        {
            name = token.identifier;

            subscripts = parseSubscripts();

            result = function() {
                var ss = subscripts ? subscripts.map(function(x) { return x(); }) : undefined;
                return getVariable(name, ss);
            };
        }
        else if( token.operator !== undefined && token.operator === "(" )
        {
            result = parseExpression();
            self.context.requireToken( "operator", ")" );
        }
        else
        {
            error( "Syntax error: saw " + token );
        }
        
        return result;
        
    } // parseFinalExpression

    //----------------------------------------------------------------------
    function parseUnaryExpression()
    //----------------------------------------------------------------------
    {
        //trace( "parseUnaryExpression: " + self.context );

        var rhs, token, op;
        if( self.context.hasMoreTokens() && ( 
            self.context.peekToken().operator && self.context.peekToken().operator === "+" || 
            self.context.peekToken().operator && self.context.peekToken().operator === "-" || 
            self.context.peekToken().reserved && self.context.peekToken().reserved === "NOT" ) )
        {
            token = self.context.nextToken();
            op = token.operator !== undefined ? token.operator : token.reserved;
            rhs = parseUnaryExpression();

            rhs = function(rhs) {
                switch (op) { 
                    case "+": return rhs;
                    case "-": return function() { return -rhs(); };
                    case "NOT": return function() { return (!rhs()) ? 1 : 0; };
                }
            }(rhs);
        }
        else
        {
            rhs = parseFinalExpression();
        }
        
        //trace( "<< parseUnaryExpression: " + self.context );
        return rhs;
    } // parseUnaryExpression
    

    //----------------------------------------------------------------------
    function parsePowerExpression()
    //----------------------------------------------------------------------
    {
        //trace( "parsePowerExpression: " + self.context );

        var lhs = parseUnaryExpression();
        var op;
        while( self.context.hasMoreTokens() && self.context.peekToken().operator && 
            ( self.context.peekToken().operator === "^" ) )
        {
            op = self.context.nextToken().operator;

            lhs = function(lhs) {
                var rhs = parseUnaryExpression();
                //switch (op) {
                //    case "^": 
                    return function() { return Math.pow(lhs(), rhs()); };
                //}
            }(lhs);
        }
        
        //trace( "<< parsePowerExpression: " + self.context );
        return lhs;
    } // parsePowerExpression

    function div(n, d) {
        if (d === 0) {
            error("Division by zero");
        }
        return n / d;
    }

    //----------------------------------------------------------------------
    function parseMultiplicativeExpression()
    //----------------------------------------------------------------------
    {
        //trace( "parseMultiplicativeExpression: " + self.context );

        var lhs = parsePowerExpression();
        var op;
        while( self.context.hasMoreTokens() && self.context.peekToken().operator && ( 
            self.context.peekToken().operator === "*" || 
            self.context.peekToken().operator === "/" ) )
        {
            op = self.context.nextToken().operator;

            lhs = function(lhs) {
                var rhs = parsePowerExpression();
                switch (op) {
                    case "*": return function() { return lhs() * rhs(); };
                    case "/": return function() { return div(lhs(), rhs()); };
                }
            }(lhs);
        }

        //trace( "<< parseMultiplicativeExpression: " + self.context );	
        return lhs;
    } // parseMultiplicativeExpression
    
    //----------------------------------------------------------------------
    function parseAdditiveExpression()
    //----------------------------------------------------------------------
    {
        //trace( "parseAdditiveExpression: " + self.context );

        var lhs = parseMultiplicativeExpression();
        var op;
        while( self.context.hasMoreTokens() && self.context.peekToken().operator && ( 
            self.context.peekToken().operator === "+" || 
            self.context.peekToken().operator === "-" ) )
        {
            op = self.context.nextToken().operator;
            lhs = function(lhs) {
                var rhs = parseMultiplicativeExpression();
                switch (op) {
                    case "+": return function() { return lhs() + rhs(); };
                    case "-": return function() { return lhs() - rhs(); };
                }
            }(lhs);
        }
        
        //trace( "<< parseAdditiveExpression: " + self.context );	
        return lhs;
    } // parseAdditiveExpression
    
    
    //----------------------------------------------------------------------
    function parseRelationalExpression()
    //----------------------------------------------------------------------
    {
        //trace( "parseRelationalExpression: " + self.context );

        var lhs = parseAdditiveExpression();
        var op, op2;
        while( self.context.hasMoreTokens() && self.context.peekToken().operator && ( 
            self.context.peekToken().operator === "<" || 
            self.context.peekToken().operator === ">" || 
            self.context.peekToken().operator === "=" ) )
        {
            op = self.context.nextToken().operator;

            // Handle digraphs
            if( self.context.hasMoreTokens() && self.context.peekToken().operator !== undefined && ( 
                self.context.peekToken().operator === "<" || 
                self.context.peekToken().operator === ">" || 
                self.context.peekToken().operator === "=" ) )
            {
                op2 = self.context.nextToken().operator;
                op += op2;
            }

            lhs = function(lhs) {
                var rhs = parseAdditiveExpression();
                switch (op) {
                    case "<": return function() { return (lhs() < rhs()) ? 1 : 0; };
                    case ">": return function() { return (lhs() > rhs()) ? 1 : 0; };
                    case "<=":
                    case "=<": return function() { return (lhs() <= rhs()) ? 1 : 0; };
                    case ">=":
                    case "=>": return function() { return (lhs() >= rhs()) ? 1 : 0; };
                    case "=":
                    case "==": return function() { return (lhs() === rhs()) ? 1 : 0; };
                    case "<>":
                    case "><": return function() { return (lhs() !== rhs()) ? 1 : 0; };
                }

            }(lhs);
            
        }
        
        //trace( "<< parseRelationalExpression: " + self.context );
        return lhs;
    } // parseRelationalExpression
    
    
    //----------------------------------------------------------------------
    function parseAndExpression()
    //----------------------------------------------------------------------
    {
        //trace( "parseAndExpression: " + self.context );

        var lhs = parseRelationalExpression();
        var op;
        while( self.context.hasMoreTokens() && ( self.context.peekToken().reserved && 
            self.context.peekToken().reserved === "AND" ) )
        {
            op = self.context.nextToken().reserved;

            lhs = function(lhs) {
                var rhs = parseRelationalExpression();
                //switch (op) {
                //    case "AND": 
                return function() { return (lhs() && rhs()) ? 1 : 0; };
                //}
            }(lhs);
        }
        
        //trace( "<< parseAndExpression: " + self.context );
        return lhs;
    } // parseAndExpression

    //----------------------------------------------------------------------
    function parseOrExpression()
    //----------------------------------------------------------------------
    {
        //trace( "parseOrExpression: " + self.context );

        var lhs = parseAndExpression();
        var op;
        while( self.context.hasMoreTokens() && ( self.context.peekToken().reserved && 
            self.context.peekToken().reserved === "OR" ) )
        {
            op = self.context.nextToken().reserved;

            lhs = function(lhs) {
                var rhs = parseAndExpression();
                //switch (op) {
                //    case "OR": 
                return function() { return (lhs() || rhs()) ? 1 : 0; };
                //}
            }(lhs);
        }
        
        //trace( "<< parseOrExpression: " + self.context );
        return lhs;
    } // parseOrExpression

    //----------------------------------------------------------------------
    function advanceToNextLine()
    //----------------------------------------------------------------------
    {
        // Look for next numbered statement
        while( self.context.current_token < self.tokens.length && self.tokens[self.context.current_token].lineNumber === undefined )
        {
            ++self.context.current_token;
        }
    } // advanceToNextLine
    
    //----------------------------------------------------------------------
    function jumpToLine( line_number )
    //----------------------------------------------------------------------
    {
        self.context.current_token = self.line_offsets[line_number];

        if( self.context.current_token === undefined )
        {
            error( "Undefined statement: " + line_number );
        }

        self.context.current_line_number = line_number;

    } // jumpToLine

    //----------------------------------------------------------------------
    function executeStatement()
    //----------------------------------------------------------------------
    {
        //trace( "executeStatement: " + self.context );
        
        self.context.current_statement = self.context.current_token;

        // IF ... THEN ... is treated as one statement for purposes of ONERR
        var extendedStatement;
        do
        {
            extendedStatement = false;
        
            if( self.context.endOfStatement() )
            {
                //trace( "empty statement" );
                return true;
            }

            var token, stack_record;
            var name, subscripts, result, r1, r2;
            var trailing_newline, prompt, index, value;
            
            token = self.context.nextToken();
                    
            var statement = token.reserved;

            if( statement === undefined && token.identifier !== undefined )
            {
                statement = "LET";
            }
            
            if( statement === undefined )
            {
                error( "Syntax error: Expected reserved word" );
            }
            
            //trace("Statement: " + statement);
            
            switch( statement )
            {
                //////////////////////////////////////////////////////////////////////
                //
                // Variable Statements
                //
                //////////////////////////////////////////////////////////////////////

                case "CLEAR":	// Clear all variables
                    self.variables = {};
                    self.arrays = {};
                    self.functions = {};
                    self.data_token = undefined;
                    self.data_line_number = undefined;
                    break;
                    
                case "LET":		// Assign a variable, LET x = expr
                    // Special case - implicit let gets overloaded token
                    if( token.identifier === undefined )
                    {
                        token = self.context.nextToken( "identifier" );
                    }
                    
                    name = token.identifier;
                    
                    subscripts = getSubscripts();
                    
                    self.context.requireToken( "operator", "=" );
                    
                    result = evaluateExpression();
                    setVariable( name, subscripts, result );
                    break;
        
                case "DIM":
                    do
                    {
                        name = self.context.nextToken( "identifier" ).identifier;
                        subscripts = getSubscripts();

                        if( subscripts !== undefined && subscripts.length > 0)
                        {
                            if( self.arrays[ name ] !== undefined )
                            {
                                error( "Redimensioned array: " + name );
                            }
                            
                            self.arrays[ name ] = dimArray( subscripts );
                        }
                    } 
                    while ( self.context.hasMoreTokens() && self.context.peekToken().operator && self.context.peekToken().operator === "," && self.context.nextToken() );
                    break;
                    
                case "DEF":     // DEF FN A(X) = expr
                    self.context.requireToken( "reserved", "FN" );
                    name = self.context.nextToken( "identifier" ).identifier;
                    
                    self.context.requireToken( "operator", "(" );
                    value = self.context.nextToken( "identifier" ).identifier;
                    self.context.requireToken( "operator", ")" );
                    self.context.requireToken( "operator", "=" );
                    index = self.context.current_token; 

                    //evaluateExpression(); // It's acceptable to have errors if FN is not used
                    while( !self.context.endOfStatement() )
                    {
                        self.context.nextToken();
                    }                    

                    self.functions[ name ] = { token: index, param: value };
                    break;

                //////////////////////////////////////////////////////////////////////
                //
                // Flow Control Statements
                //
                //////////////////////////////////////////////////////////////////////

                case "GOTO":	// GOTO linenum		
                    token = self.context.nextToken( "number" );
                    jumpToLine( token.number );
                    break;
                    
                case "ON":		// ON expr (GOTO|GOSUB) linenum[,linenum ... ]
                    result = Math.floor( arithmeticExpression() );
                    if( result < 0 )
                    {
                        error( "Illegal quantity error" );
                    }
                    
                    token = self.context.nextToken();
                    if( token.reserved !== "GOTO" && token.reserved !== "GOSUB" )
                    {
                        error( "Syntax error: Expected GOTO or GOSUB" );
                    }	

                    index = 0;
                    while( !self.context.endOfStatement() )
                    {
                        value = self.context.nextToken( "number" ).number;
                        ++index;
                        
                        if( index === result )
                        {
                            // Skip to end of statement
                            while( !self.context.endOfStatement() )
                            {
                                self.context.nextToken();
                            }                    
                            
                            if( token.reserved === "GOSUB" )
                            {
                                self.stack.push( 
                                    { 
                                        gosub_return: self.context.current_token, 
                                        line_number: self.context.current_line_number
                                    } 
                                ); 
                            }
                            
                            jumpToLine( value );
                            break;
                        }

                        if( !self.context.endOfStatement() )
                        {
                            self.context.requireToken( "operator", "," );
                        }
                    }
                    break;

                case "GOSUB":	// GOSUB linenum
                    token = self.context.nextToken( "number" );
                    self.stack.push( 
                        { 
                            gosub_return: self.context.current_token, 
                            line_number: self.context.current_line_number
                        } 
                    ); 
                    jumpToLine( token.number );
                    break;
                    
                case "RETURN": // Return from the last GOSUB
                    stack_record = self.stack.pop();
                    if( stack_record === undefined || stack_record.gosub_return === undefined )
                    {
                        error( "Return without gosub" );
                    }
                    
                    self.context.current_token = stack_record.gosub_return;
                    self.context.current_line_number = stack_record.line_number;
                    break;

                case "POP": // Turn last GOSUB into a GOTO
                    stack_record = self.stack.pop();
                    if( stack_record === undefined || stack_record.gosub_return === undefined )
                    {
                        error( "Return without gosub" );
                    }
                    break;

                case "FOR":		// FOR i = m TO n STEP s
                    stack_record = {};
                    
                    stack_record.index = self.context.nextToken( "identifier" ).identifier;
                    self.context.requireToken( "operator", "=" );
                    stack_record.from = arithmeticExpression();

                    self.context.requireToken( "reserved", "TO" );
                    stack_record.to = arithmeticExpression();

                    stack_record.step = 1;
                    if( self.context.hasMoreTokens() && self.context.peekToken().reserved !== undefined && self.context.peekToken().reserved === "STEP" )
                    {
                        self.context.nextToken(); // consume "STEP"
                        stack_record.step = arithmeticExpression();
                    }

                    stack_record.for_next = self.context.current_token;
                    stack_record.line_number = self.context.current_line_number;
                    
                    setVariable( stack_record.index, undefined, stack_record.from );
                    self.stack.push( stack_record );
                    //trace( "for "+stack_record.index+" = "+stack_record.from+" to "+stack_record.to+" step "+stack_record.step);
                    break;
                    
                case "NEXT":	// NEXT [i [,j ... ] ]
                    do
                    {
                        if( !self.context.endOfStatement() )
                        {
                            index = self.context.nextToken( "identifier" ).identifier;
                        }
                        
                        do
                        {
                            stack_record = self.stack.pop();
                            if( stack_record === undefined || stack_record.for_next === undefined )
                            {
                                error( "Next without for" );
                            }
                        }
                        while( index !== undefined && stack_record.index !== index );
                        
                        value = getVariable( stack_record.index );

                        if( stack_record.from < stack_record.to && stack_record.step < 0 )
                        {
                            // bail
                            debug( "from < to, negative step - bailing" );
                        }
                        else if( stack_record.from > stack_record.to && stack_record.step > 0 )
                        {
                            // bail
                            debug( "from > to, positive step - bailing" );
                        }

                        value = value + stack_record.step;
                        setVariable( stack_record.index, undefined, value );

                        if( ( stack_record.step > 0 && value > stack_record.to ) || 
                            ( stack_record.step < 0 && value < stack_record.to ) )
                        {
                            // done!
                            //trace( "done FOR " + stack_record.index + " ( = " + value + " )" );
                        }
                        else
                        {
                            //trace( "NEXT " + stack_record.index + " = " + value );
                            self.stack.push( stack_record );
                            
                            self.context.current_token = stack_record.for_next;
                            self.context.current_line_number = stack_record.line_number;
                            return true;
                        }
                    }
                    while( !self.context.endOfStatement() && self.context.requireToken( "operator", "," ) );
                    
                    break;
                
            
                case "IF":		// IF expr (GOTO linenum|THEN linenum|THEN statement [:statement ... ]
                    result = evaluateExpression();
                    //trace( "IF expression evaluates to: " + result );
                    
                    if( !result )
                    {
                        advanceToNextLine();
                        break;
                    }
                    
                    token = self.context.nextToken();
                    if( token.reserved !== "GOTO" && token.reserved !== "THEN" )
                    {
                        error( "Syntax error: Expected GOTO or THEN following IF expression" );
                    }	
                                
                    if( token.reserved === "GOTO" || self.context.peekToken().number !== undefined )
                    {
                        token = self.context.nextToken( "number" );
                        jumpToLine( token.number );						
                        break;
                    }
                    else
                    {
                        extendedStatement = true;
                    }
                    break;

                case "END":		// End program
                    return false;
                    
                case "STOP":	// Break, like an error
                    error( "Break" );
                    break;

                //////////////////////////////////////////////////////////////////////
                //
                // Error Handling Statements
                //
                //////////////////////////////////////////////////////////////////////

                case "ONERR":	// ONERR GOTO linenum
                    self.context.requireToken( "reserved", "GOTO" );
                    token = self.context.nextToken( "number" );
                    
                    self.onerr_handler = token.number;                    
                    break;            
                
                case "RESUME":                
                    self.context.current_line_number  = self.onerr_resume_line_number;
                    self.context.current_token        = self.onerr_resume_token;
                    self.context.current_statement    = self.onerr_resume_token; // Book-keeping only
                    break;

                //////////////////////////////////////////////////////////////////////
                //
                // Inline Data Statements
                //
                //////////////////////////////////////////////////////////////////////

                case "DATA":
                    // Don't validate, skip to next statement
                    while( !self.context.endOfStatement() )
                    {
                        self.context.nextToken();
                    }                    
                    break;
                    
                case "RESTORE":
                    self.data_token = undefined;
                    self.data_line_number = undefined;
                    self.data_statement = false;
                    break;

                case "READ":
                    do 
                    {
                        name = self.context.nextToken("identifier").identifier;
                        subscripts = getSubscripts();

                        if (self.data_token === undefined) 
                        {
                            self.data_token = 0;
                            self.data_line_number = undefined;
                            self.data_statement = false;
                        }

                        // TODO: Just use dedicated data self.context?
                        // Save self.context
                        var save_token = self.context.current_token;
                        var save_line = self.context.current_line_number;
                        self.context.current_token = self.data_token;
                        self.context.current_line_number = self.data_line_number;

                        // Find the next DATA statement
                        while (!self.data_statement && self.context.hasMoreTokens()) 
                        {
                            token = self.context.nextToken();
                            if (token.lineNumber !== undefined) 
                            {
                                continue;
                            }
                            else if (token.reserved !== undefined && token.reserved === "REM") 
                            {
                                advanceToNextLine();
                            }
                            else if (token.reserved !== undefined && token.reserved === "DATA") 
                            {
                                self.data_statement = true;
                            }
                            else 
                            {
                                while (!self.context.endOfStatement()) 
                                {
                                    self.context.nextToken();
                                }
                            }
                        }
                        if (!self.context.hasMoreTokens()) 
                        {
                            error("Out of data");
                        }

                        // TODO: This seems bogus
                        result = undefined;

                        // Read the next value
                        if (!self.context.endOfStatement()) 
                        {
                            // NOTE: Shouldn't evaluate expressions, but otherwise DATA needs a 
                            // custom parser to do number parsing
                            result = evaluateExpression();

                            if (!self.context.endOfStatement()) {
                                self.context.requireToken("operator", ",");
                            }
                            else {
                                self.data_statement = false;
                            }
                        }

                        // Restore self.context
                        self.data_token = self.context.current_token;
                        self.data_line_number = self.context.current_line_number;
                        self.context.current_token = save_token;
                        self.context.current_line_number = save_line;

                        setVariable(name, subscripts, result);
                    }
                    while (!self.context.endOfStatement() && self.context.requireToken("operator", ","));

                    break;

                //////////////////////////////////////////////////////////////////////
                //
                // I/O Statements
                //
                //////////////////////////////////////////////////////////////////////

                case "PRINT": // Output to the screen
                    trailing_newline = true;
                    while (!self.context.endOfStatement()) {
                        if (self.context.peekToken().operator && self.context.peekToken().operator === ";") {
                            self.context.nextToken(); // consume
                            trailing_newline = false;
                            continue;
                        }
                        else if (self.context.peekToken().operator && self.context.peekToken().operator === ",") {
                            self.context.nextToken(); // consume
                            self.tty.print("          "); // TODO: More of an HTAB, actually
                            continue;
                        }
                        else if (self.context.peekToken().reserved &&
                            (self.context.peekToken().reserved === "SPC" || self.context.peekToken().reserved === "TAB")) {
                            token = self.context.nextToken(); // consume
                            self.context.requireToken("operator", "(");
                            result = evaluateExpression();
                            self.context.requireToken("operator", ")");
                            for (index = 0; index < result; ++index) {
                                // TODO: Correct use of TAB
                                self.tty.print(" ");
                            }
                            continue;
                        }

                        trailing_newline = true;

                        result = evaluateExpression();
                        if (typeof result === "string") {
                            self.tty.print(result);
                        }
                        else if (typeof result === "number") {
                            self.tty.print(result.toString());
                        }
                    }
                    if (trailing_newline) {
                        self.tty.print("\r");  // NOTE: Matches Applesoft's use of CR/LF
                    }
                    break;

                case "INPUT":	// Read input from keyboard
                    if( self.context.peekToken().string !== undefined )
                    {
                        prompt = self.context.nextToken().string;
                        self.context.requireToken( "operator", ";" );
                    }
                    else
                    {
                        prompt = "?";
                    }
                    
                    var im = function( callback ) { self.tty.readLine( callback, prompt ); };
                    var ih = function( entry )
                    {
                        //trace( "INPUT handler: " + self.context + ", " + entry );
                        var name = self.context.nextToken( "identifier" ).identifier;
                        var subscripts = getSubscripts();
                        
                        if( name.match( /\$$/ ) )
                        {
                            setVariable( name, subscripts, entry );
                        }
                        else
                        {
                            setVariable( name, subscripts, parseFloat( entry, 10 ) );
                        }
                                        
                        if( !self.context.endOfStatement() )
                        {
                            self.context.requireToken( "operator", "," );

                            prompt = "?";
                            self.input_method = im;
                            self.input_handler = ih;
                        }
                    };
                    
                    self.input_method = im;
                    self.input_handler = ih;

                    return true;

                case "GET": // Read character from keyboard

                    self.input_method = self.tty.readChar;
                    self.input_handler = function( entry )
                    {
                        //trace( "GET handler: " + self.context + ", " + entry );

                        var name = self.context.nextToken( "identifier" ).identifier;
                        var subscripts = getSubscripts();
                        
                        setVariable( name, subscripts, entry );

                        if( !self.context.endOfStatement() )
                        {
                            error( "Syntax error: expecting end of statement" );
                        }
                    };

                    return true;

                case "HOME": 	// Clear text screen
                    if (self.tty.clearScreen) { self.tty.clearScreen(); }
                    if (self.tty.setCursorPosition) { self.tty.setCursorPosition(0, 0); }
                    break;

                case "HTAB":		// Set horizontal cursor position
                    result = evaluateExpression();
                    if( result < 1 || result >= self.tty.getScreenSize().width + 1 )
                    {
                        error( "Illegal quantity error: htab " + result );
                    }
                    
                    if( self.tty.textWindow )
                    {
                        result += self.tty.textWindow.left;
                    }
                    
                    self.tty.setCursorPosition( result - 1, undefined );
                    break;
                    
                case "VTAB":		// Set vertical cursor position
                    result = evaluateExpression();
                    if( result < 1 || result >= self.tty.getScreenSize().height + 1 )
                    {
                        error( "Illegal quantity error: vtab " + result );
                    }
                    self.tty.setCursorPosition( undefined, result - 1 );
                    break;

                case "INVERSE": 	// Inverse text
                    if (self.tty.setTextStyle) { self.tty.setTextStyle(TEXT_STYLE_INVERSE); }
                    break;

                case "FLASH": 	// Flashing text
                    if (self.tty.setTextStyle) { self.tty.setTextStyle(TEXT_STYLE_FLASH); }
                    break;
                    
                case "NORMAL":		// Normal text	
                    if (self.tty.setTextStyle) { self.tty.setTextStyle( TEXT_STYLE_NORMAL ); }
                    break;
                    
                case "TEXT":		// Set display mode to text
                    setDisplayState( "graphics", false );
                    
                    if( self.tty.textWindow ) 
                    { 
                        // Reset text window
                        self.tty.textWindow = 
                        {
                            left: 0, 
                            top: 0, 
                            width: self.tty.getScreenSize().width, 
                            height: self.tty.getScreenSize().height 
                        }; 
                    }
                    break;

                //////////////////////////////////////////////////////////////////////
                //
                // Miscellaneous Statements
                //
                //////////////////////////////////////////////////////////////////////

                case "REM":
                    advanceToNextLine(); // Skip rest of line
                    break;

                case "NOTRACE":		// Turn off line tracing
                    self.trace_mode = false;
                    break;
                    
                case "TRACE":		// Turn on line tracing
                    self.trace_mode = true;
                    break;

                //////////////////////////////////////////////////////////////////////
                //
                // NYI Statements
                //
                //////////////////////////////////////////////////////////////////////


                // Parts of other statements - AT, FN, STEP, TO, THEN, etc.
                default:
                    error( "Syntax error: " + token.reserved );
                    break;

                //////////////////////////////////////////////////////////////////////
                //
                // Lores Graphics
                //
                //////////////////////////////////////////////////////////////////////
                
                case "GR":			// Set display mode to lores graphics, clear screen
                    if( self.lores )
                    {
                        setDisplayState( "lores", true );
                        setDisplayState( "full", false );
                        setDisplayState( "graphics", true );
                        self.lores.clear(); 
                        
                        self.tty.setCursorPosition( 0, self.tty.getScreenSize().height );
                    }
                    else
                    {
                        error( "Graphics not supported" );
                    }
                    break;
                    
                case "COLOR=":		// Set lores color
                    result = arithmeticExpression();

                    if( self.lores )
                    {
                        self.lores.setColor( result );
                    }
                    else
                    {
                        error( "Graphics not supported" );
                    }
                    break;
                    
                case "PLOT":		// Plot lores point
                    r1 = arithmeticExpression();
                    self.context.requireToken( "operator", "," );
                    r2 = arithmeticExpression();
                    
                    r1 = Math.floor( r1 );
                    r2 = Math.floor( r2 );
                    
                    if( self.lores )
                    {
                        if( r1 < 0 || 
                            r2 < 0 || 
                            r1 >= self.lores.getScreenSize().width ||
                            r2 >= self.lores.getScreenSize().height )
                        {
                            error( "Illegal quantity error" );
                        }

                        self.lores.plot( r1, r2 );
                    }
                    else
                    {
                        error( "Graphics not supported" );
                    }
                    break;
                    
                case "HLIN":		// Draw lores horizontal line
                    r1 = arithmeticExpression();               
                    self.context.requireToken( "operator", "," );
                    r2 = arithmeticExpression();
                    self.context.requireToken( "reserved", "AT" );                
                    result = arithmeticExpression();
                    
                    r1 = Math.floor( r1 );
                    r2 = Math.floor( r2 );
                    result = Math.floor( result );
                    
                    if( self.lores )
                    {
                        if( r1 < 0 || 
                            r2 < 0 || 
                            result < 0 ||
                            r1 >= self.lores.getScreenSize().width ||
                            r2 >= self.lores.getScreenSize().width ||
                            result >= self.lores.getScreenSize().height )
                        {
                            error( "Illegal quantity error" );
                        }

                        self.lores.hlin( r1, r2, result );
                    }
                    else
                    {
                        error( "Graphics not supported" );
                    }
                    break;

                case "VLIN":		// Draw lores vertical line
                    r1 = arithmeticExpression();
                    self.context.requireToken( "operator", "," );
                    r2 = arithmeticExpression();
                    self.context.requireToken( "reserved", "AT" );
                    result = arithmeticExpression();

                    r1 = Math.floor( r1 );
                    r2 = Math.floor( r2 );
                    result = Math.floor( result );

                    if( self.lores )
                    {
                        if( r1 < 0 || 
                            r2 < 0 || 
                            result < 0 ||
                            r1 >= self.lores.getScreenSize().height ||
                            r2 >= self.lores.getScreenSize().height ||
                            result >= self.lores.getScreenSize().width )
                        {
                            error( "Illegal quantity error" );
                        }

                        self.lores.vlin( r1, r2, result );
                    }
                    else
                    {
                        error( "Graphics not supported" );
                    }
                    break;

                    
                //////////////////////////////////////////////////////////////////////
                //
                // Hires Graphics
                //
                //////////////////////////////////////////////////////////////////////
                
                // Hires Display Routines
                case "HGR":			// Set display mode to hires graphics, clear screen
                    if( self.hires )
                    {
                        setDisplayState( "lores", false );
                        setDisplayState( "full", false );
                        setDisplayState( "graphics", true );
                        self.hires.clear(); 
                    }
                    else
                    {
                        error( "Graphics not supported" );
                    }
                    break;
                    
                case "HGR2":		// Set display mode to hires graphics, page 2, clear screen
                    abort( "Display statement not supported: " + token.reserved );
                    break;

                case "HCOLOR=":		// Set hires color
                    result = arithmeticExpression();

                    if( self.hires )
                    {
                        self.hires.setColor( result );
                    }
                    else
                    {
                        error( "Graphics not supported" );
                    }
                    break;

                case "HPLOT":		// Draw hires line
                    result = ( self.context.hasMoreTokens() && self.context.peekToken().reserved && self.context.peekToken().reserved === "TO" && self.context.nextToken() );
                    do
                    {
                        r1 = arithmeticExpression();
                        self.context.requireToken( "operator", "," );
                        r2 = arithmeticExpression();

                        r1 = Math.floor( r1 );
                        r2 = Math.floor( r2 );
                          
                        if( self.hires )
                        {
                            if( result )
                            {
                                self.hires.plot_to( r1, r2 );
                            }               
                            else
                            {
                                self.hires.plot( r1, r2 );
                            }
                        }
                        else
                        {
                            error( "Graphics not supported" );
                        }
                        
                        if( !self.context.endOfStatement() )
                        {
                            self.context.nextToken( "reserved", "TO" );
                            result = true;
                        }
                    }
                    while ( !self.context.endOfStatement() );

                    break;
                    

                //////////////////////////////////////////////////////////////////////
                //
                // Compatibility shims
                //
                //////////////////////////////////////////////////////////////////////

                case "PR#": 		// Direct output to slot
                    result = arithmeticExpression();
                    if (result === 0) {
                        if (self.tty.setColumns) { self.tty.setColumns(40); }
                    }
                    else if (result === 3) {
                        if (self.tty.setColumns) { self.tty.setColumns(80); }
                    }

                    break;

                case "POKE": 	// Set memory value
                    r1 = arithmeticExpression();
                    self.context.requireToken("operator", ",");
                    r2 = arithmeticExpression();

                    r1 = clampTo16Bits(r1);

                    r2 = Math.floor(r2);
                    if (r2 < 0 || r2 > 255) {
                        error("Illegal quantity error");
                    }

                    var poke_func = poke_table[r1];
                    if( poke_func ) {
                        poke_func(r2);
                    }
                    else {
                        error("Unsupported POKE location: " + r1);
                    }

                    break;                
                    
                //////////////////////////////////////////////////////////////////////
                //
                // INTROSPECTION
                //
                //////////////////////////////////////////////////////////////////////

                case "LIST":		// List program statements
                    r1 = -1;
                    r2 = -1;
                                       
                    if( !self.context.endOfStatement() && self.context.peekToken().number !== undefined )
                    {
                        r1 = arithmeticExpression();
                        r2 = r1;
                    }
                    
                    if( !self.context.endOfStatement() )
                    {
                        self.context.requireToken( "operator", "," );
                        r2 = -1;
                        if( !self.context.endOfStatement() )
                        {
                            r2 = arithmeticExpression();
                        }
                    }

                    self.list( r1, r2 );
                    break;
                    
                //////////////////////////////////////////////////////////////////////
                //
                // Statements that will never be implemented
                //
                //////////////////////////////////////////////////////////////////////

                // Shape tables            
                case "ROT=":		// Set rotation angle for hires shape
                case "SCALE=":		// Set rotation angle for hires shape
                case "DRAW":		// Draw hires shape
                case "XDRAW":		// XOR draw hires shape
                    abort( "Display statement not supported: " + token.reserved );
                    break;

                // Interpreter Routines
                case "CONT":		// Continue stopped program (immediate mode)
                case "DEL":			// Deletes program statements
                case "NEW":			// Wipe program
                case "RUN":			// Execute program
                case "SPEED=":		// Output speed
                    abort( "Statement not supported: " + token.reserved );
                    break;
                
                // Native Routines
                case "CALL":		// Execute assembly routine
                case "HIMEM:":		// Set upper bound of variable memory
                case "IN#":			// Direct input from slot
                case "LOMEM:":		// Set low bound of variable memory
                case "WAIT":		// Wait for memory value to match a condition
                    abort( "Native interop statement not supported: " + token.reserved );
                    break;
                
                // Tape Routines
                case "LOAD":		// Load program from cassette port
                case "RECALL":		// Load array from cassette port
                case "SAVE":		// Save program to cassette port
                case "STORE":		// Store array to cassette port
                case "SHLOAD":		// Load shape table from cassette port
                    abort( "Tape statement not supported: " + token.reserved );
                    break;
            }
        }
        while( extendedStatement );

        return true;		
    
    } // executeStatement


    //----------------------------------------------------------------------
    function handleError( e )
    //----------------------------------------------------------------------
    {
        if( e.catchableError && self.onerr_handler )
        {
            self.onerr_resume_line_number = self.context.current_line_number;
            self.onerr_resume_token       = self.context.current_statement;
            jumpToLine( self.onerr_handler );
            
            return true;
        }
        else
        {
            return false;
        }
        
    } // handleError

    
    //----------------------------------------------------------------------
    function executeStep()
    //----------------------------------------------------------------------
    {
        while( self.context.hasMoreTokens() )
        {
            if( self.context.peekToken().lineNumber !== undefined )
            {
                self.context.current_line_number = self.context.nextToken().lineNumber;
            }
            else if( self.context.peekToken().statementSeparator !== undefined )
            {
                self.context.nextToken(); // consume
            }
            else
            {
                break;
            }
        }
        
        if( !self.context.hasMoreTokens() )
        {
            //trace( "executeStep: No more tokens" );
            return false;
        }

        //trace( "self.context.current_token: " + self.context.current_token );
        
        if( self.trace_mode )
        {
            self.tty.print( "#" + self.context.current_line_number + " " );
        }
        
        var result = executeStatement();
        if( !result )
        {
            //trace("executeStep: executeStatement returned false");
            return false;
        }
        
        return true;
                
    } // executeStep

    //----------------------------------------------------------------------
    // Public Methods
    //----------------------------------------------------------------------

    //----------------------------------------------------------------------
    function inputContinuation( input_buffer ) 
    //----------------------------------------------------------------------
    {
        //trace("inputContinuation: " + input_buffer );
        
        var ih;
        
        try
        {
            if( self.input_handler )
            {
                ih = self.input_handler;
                self.input_handler = undefined;

                //trace("back from blocking input");
                ih( input_buffer );
            }
            else
            {
                abort( "inputContinuation called without handler registered" );
            }

        }
        catch( e )
        {
            if( handleError(e) )
            {
                // Handled by the program itself
            }
            else
            {
                if( e.errorMessage !== undefined )
                {
                    self.tty.printError( e.errorMessage.toString() + "\n" );
                }
                
                throw e;
            }
        }
            
    } // inputContinuation
    
    //----------------------------------------------------------------------
    this.step = function( driverFunction ) 
    //----------------------------------------------------------------------
    {
        var im;
       
        try
        {
            // If a callback re-requests input (e.g. INPUT A,B,C), block 
            if( self.input_method )
            {
                im = self.input_method;
                self.input_method = undefined;
                //trace("blocking on input");

                im( function( input ) { inputContinuation( input ); driverFunction(); } );
                return STATE_BLOCKED;
            }

            if( !executeStep() )
            {
                return STATE_STOPPED;
            }
           
            // If the step requested input, block
            if( self.input_method )
            {
                im = self.input_method;
                self.input_method = undefined;
                //trace("blocking on input");

                im( function( input ) { inputContinuation( input ); driverFunction(); } );
                return STATE_BLOCKED;
            }

            return STATE_RUNNING;
        }
        catch( e )
        {
            if( handleError( e ) )
            {
                // Handled by the program itself
                return STATE_RUNNING;
            }
            else
            {
                if( e.errorMessage !== undefined )
                {
                    self.tty.printError( e.errorMessage.toString() + "\n" );
                    return STATE_STOPPED;
                }
                
                throw e;
            }
        }
            
    }; // step

    //----------------------------------------------------------------------
    this.run = function( line_num ) 
    //----------------------------------------------------------------------
    {
        if( self.tokens.length === 0 )
        {
            return false;
        }
        
        if( line_num === undefined )
        {
            line_num = self.tokens[0].lineNumber;
        }
        
        // Clear program state
        self.variables = {};
        self.arrays = {};
        self.functions = {};
        self.stack = [];
        self.data_token = undefined;
        self.data_line_number = undefined;
        self.onerr_handler = undefined;
        
        jumpToLine( line_num );
        return true;
            
    }; // run
    
    
    //----------------------------------------------------------------------
    this.load = function(codeString)
    //----------------------------------------------------------------------
    {
        //trace( "load" );

        self.tokens = [];
        self.line_offsets = [];
        self.context = new Context(self.tokens);
        var l, line, tokenizer, token;

        var lines = codeString.split(/[\r\n]+/);
        try {
            for (l = 0; l < lines.length; ++l) {
                line = lines[l];

                if (line.match(/^\s+$/)) {
                    // Ignore empty lines, as a convenience
                    continue;
                }

                if (line.match(/^\s*(\d+)\s*(.*)$/)) {
                    token = new Token();
                    token.lineNumber = parseInt(RegExp.$1, 10);
                    self.line_offsets[token.lineNumber] = self.tokens.length;
                    self.tokens.push(token);
                    line = RegExp.$2;
                    //trace( "parse line: " + token.lineNumber );
                }

                tokenizer = new Tokenizer(line);
                while (tokenizer.hasMoreTokens()) {
                    token = tokenizer.nextToken();
                    //trace( "parse token: " + token );
                    self.tokens.push(token);

                    if (token.reserved !== undefined && token.reserved === "REM" && tokenizer.hasMoreTokens()) {
                        self.tokens.push(tokenizer.nextAsComment());
                    }
                }
            }
        }
        catch (e) {
            if (e.errorMessage !== undefined) {
                self.tty.printError(e.errorMessage + "\n");
                return false;
            }

            throw e;
        }

        return true;

    };   // load

    //----------------------------------------------------------------------
    this.list = function( start, end )
    //----------------------------------------------------------------------
    {
        var t, token;

        // TODO: Support start, end!        
        for( t = 0; t < self.tokens.length; ++t )
        {
            token = self.tokens[t];
            
            self.tty.print( token.list() );
        }
        
        self.tty.print( "\r" );
        
    }; // list

    //----------------------------------------------------------------------
    // Constructor
    //----------------------------------------------------------------------

    // no-op

} // BasicInterpreter

//
// Command line execution environment, for use with WSH (cscript.exe) on Windows. 
// 
if( typeof WScript !== "undefined" ) // Guard for non-WSH usage
{
    //--------------------------------- -------------------------------------
    (function( args )
    //----------------------------------------------------------------------
    {
        if( args.length < 1 )
        {
            WScript.StdOut.WriteLine( "Usage: cscript basic.js [--trace] [--debug] [--list] programname" );	
            WScript.Quit( -1 );
        }

        var list = false, 
            i;

        for( i = 0; i < args.length; ++i )
        {
            if( args(i) === "--trace" )
            {
                SHOW_TRACE_OUTPUT = !SHOW_TRACE_OUTPUT;
            }
            else if( args(i) === "--debug" )
            {
                SHOW_DEBUG_OUTPUT = !SHOW_DEBUG_OUTPUT;
            }
            else if( args(i) === "--list" )
            {
                list = !list;
            }
            else
            {
                break;
            }
        }
        
        var code = "";
        var line;
        var fso = new ActiveXObject( "Scripting.FileSystemObject" );
        var stream = fso.OpenTextFile( args(i) );
        while( !stream.AtEndOfStream )
        {
            line = stream.ReadLine();
            code += line + "\n";
        }
        stream.Close();	

        // Provide the required I/O functions (mostly no-ops)
        var tty = 
        {
            putChar: function( c, x, y ) { WScript.StdOut.Write( c.replace(/\r/, "\n" ) ); },
            putString: function( s, x, y ) { WScript.StdOut.Write( s.replace(/\r/, "\n" ) ); },
            getScreenSize: function() { return { width: 80, height: 24 }; },
            getCursorPosition: function() { return { x: 0, y: 0 }; },
            setCursorPosition: function( x, y ) {},
            showCursor: function( updateScreen ) {},
            hideCursor: function( updateScreen ) {},
            readLine: function( callback, prompt ) { if( prompt !== undefined ) { WScript.StdOut.Write( prompt ); } var string = WScript.StdIn.ReadLine(); callback( string ); },
            readChar: function( callback ) { var string = WScript.StdIn.Read(1); callback( string.charAt(0) ); },
            print: function( s ) { WScript.StdOut.Write( s.replace(/\r/, "\n" ) ); },
            printError: function(s) { WScript.StdErr.Write(s.replace(/\r/, "\n")); }
        };

        var interpreter = new BasicInterpreter( tty );

        if( !interpreter.load( code ) )
        {
            WScript.Quit(-1);
        }

        if( list )
        {
            interpreter.list();
        }
        
        interpreter.run();
        
        function step()
        {
            while( interpreter.step( step ) === STATE_RUNNING )
            {
                // keep going
            }
        }
        step();
        
        WScript.Quit( 0 );
        
    })( WScript.Arguments );
}
