//
// Applesoft BASIC Interpreter in Javascript
// TTY Emulation
//

// 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.


/*jslint browser: true */
/*global window */

// Usage:
//   
//   tty = new TTY( screen_element, keyboard_element, width, height, modeCallback, bell );
//   tty.clearScreen()
//   tty.scrollScreen()
//   tty.setTextStyle( textStyle )
//   tty.putChar( character, x, y )
//   tty.putString( string, x, y )
//   { width: w, height: h } = tty.getScreenSize()
//   { x: x, y: y } = tty.getCursorPosition()
//   tty.setCursorPosition( x, y )
//   tty.showCursor()
//   tty.hideCursor()
//   tty.focus()
//   tty.readLine( callback_function, prompt )
//   tty.readChar( callback_function )
//
//   tty.dos = new DOS( ... );
//
//   TEXT_STYLE_NORMAL  = 0
//   TEXT_STYLE_INVERSE = 1
//   TEXT_STYLE_FLASH   = 2
//
// Example:
//   
//   <script>
//     	tty = new TTY( document.getElementById( 'screen' ), document.getElementById( 'keyboard' ), 80, 24 );
//   </script>
//   <style>
//      #screen { font-size: 10pt; font-family: monospace; font-weight: bold; background-color: black; color: #80ff80; }
//      .normal  { background-color: #000000; color: #80ff80; }
//      .inverse { background-color: #80ff80; color: #000000; }
//   </style>
//   <div id="screen" tabindex="0"></div>


// Cross browser event handler functions
// http://www.quirksmode.org/blog/archives/2005/10/_and_the_winner_1.html 
function addEvent( obj, type, fn )
{
    if( obj.addEventListener )
    {
        obj.addEventListener( type, fn, false );
    }
    else if( obj.attachEvent )
    {
        obj["e"+type+fn] = fn;
        obj[type+fn] = function() { obj["e"+type+fn]( window.event ); };
        obj.attachEvent( "on"+type, obj[type+fn] );
    }
} // addEvent

function removeEvent( obj, type, fn )
{
    if (obj.removeEventListener)
    {
        obj.removeEventListener( type, fn, false );
    }
    else if (obj.detachEvent)
    {
        obj.detachEvent( "on"+type, obj[type+fn] );
        obj[type+fn] = null;
        obj["e"+type+fn] = null;
    }
} // removeEvent



var TEXT_STYLE_NORMAL = 0,
    TEXT_STYLE_INVERSE = 1,
    TEXT_STYLE_FLASH = 2,
    TEXT_STYLE_MOUSETEXT = 3;

function TTY( screenElement, keyboardElement, columns, rows, modeCallback, bell )
{
    // For references to "this" within callbacks and closures
    var self = this;
    
    //----------------------------------------------------------------------
    // Internal Fields
    //----------------------------------------------------------------------

    var cursorX = 0;
    var cursorY = 0;
    var cursorVisible = false;
    var cursorElement = null;
    
    var screenGrid;

    var screenRow = [];
    var screenWidth = columns;
    var screenHeight = rows;
    var curStyle = TEXT_STYLE_NORMAL;
    var splitPos = 0;

    var lineCallback;
    var charCallback;
    var inputBuffer = [];
    var keyboardRegister = 0;
    var buttonState = [ 0, 0, 0, 0 ];

    var cursorState = true;
    var flashState = true;
    var flashCells = [];

    this.autoScroll = true;
    this.dos = null;

    self.textWindow = { left: 0, top: 0, width: columns, height: rows };

    //----------------------------------------------------------------------
    this.reset = function()
    //----------------------------------------------------------------------
    {
        this.hideCursor();
        lineCallback = undefined;
        charCallback = undefined;
        
        inputBuffer = [];
        keyboardRegister = 0;
        buttonState = [ 0, 0, 0, 0 ];
        
    }; // reset


    //----------------------------------------------------------------------
    function setCellStyle(element, code)
    //----------------------------------------------------------------------
    {
        // FUTURE: This shouldn't be hardcoded!
        var x = Math.floor(code / 16);
        var y = code % 16; 
        x = -( screenWidth > 40 ? 1 : 2 )* (3 + 13 * x);
        y = -2 * (4 + 13 * y);
        element.style.backgroundPosition = x.toString() + 'px ' + y.toString() + 'px';
    }

    //----------------------------------------------------------------------
    function init()
    //----------------------------------------------------------------------
    {
        var x, y, html = [];
        screenGrid = [];
        screenGrid.length = screenWidth * screenHeight;

        // Create screen - table display structure and grid state storage
        html.push('<table cellpadding="0" cellspacing="0" style="table-layout: fixed;">');
        html.push('<tbody id="_tty_tbody">');
        for( y = 0; y < screenHeight; ++y )
        {
            html.push('<tr class="tty_tr">');

            for (x = 0; x < screenWidth; ++x)
            {
                screenGrid[x + screenWidth * y] = { c: ' ', s: TEXT_STYLE_NORMAL };
                html.push('<td class="a2c"></td>');
            }
            
            html.push('</tr>');
        }
        html.push('</tbody>');
        html.push('</table>');
        
        screenElement.innerHTML = html.join("");

        // Cache rows and cells for performance
        var tbody = document.getElementById('_tty_tbody');
        for (y = 0; y < screenHeight; ++y) {
            var row = tbody.childNodes[y];
            screenRow[y] = row;
            
            for (x = 0; x < screenWidth; ++x) {
                var elem = row.childNodes[x]; //document.getElementById('_tty_' + x + '_' + y);
                screenGrid[x + screenWidth * y].elem = elem;
                setCellStyle(elem, 0x20);
            }

            row.style.visibility = (y < splitPos) ? "hidden" : "";            
        }

        // Create cursor
        cursorElement = document.createElement('span');
        cursorElement.className = 'a2c a2c-cursor';
        setCellStyle(cursorElement, 0x7f);

        flashCells = [];

    } // init

    //----------------------------------------------------------------------
    // Internal
    function updateCell(x, y)
    //----------------------------------------------------------------------
    {
        var idx = (y * screenWidth) + x;
        var cell = screenGrid[idx];

        // TODO: Mousetext!

        var code = cell.c.charCodeAt(0);
        if (cell.s === TEXT_STYLE_INVERSE) { // TODO: FLASH
            code += 128;
        }
        else if (cell.s === TEXT_STYLE_FLASH && flashState) {
            code += 128;
        }
        else if (cell.s === TEXT_STYLE_MOUSETEXT) {
            code += 64;
        }
        //code = code % 256;

        setCellStyle(cell.elem, code);

        
        if (cell.s === TEXT_STYLE_FLASH) {
            flashCells[idx] = cell;
        }
        else {
            delete flashCells[idx];
        }

        // TODO: Selectable text - have an inner <SPAN STYLE="display: none;">X</SPAN> within each cell containing the actual text
        /*
        // Need to set font-size to 0. Unfortunately, this makes it slower and
        // you still can't obviously select the text (and it's spaced out on the clipboard)
        // Want to keep this around for accessibility in the future, though.
        var c = cell.c;
        if (c === '&') { c = '&amp;'; }
        else if (c === '<') { c = '&lt;'; }
        else if (c === '>') { c = '&gt;'; }
        else if (c === ' ') { c = '&nbsp;'; }
        elem.innerHTML = c;
        */
    }

    //----------------------------------------------------------------------
    this.clearScreen = function()
    //----------------------------------------------------------------------
    {
        var x, y;

        for (y = 0; y < screenHeight; ++y) {
            for (x = 0; x < screenWidth; ++x) {
                var cell = screenGrid[x + screenWidth * y];
                cell.c = ' ';
                cell.s = TEXT_STYLE_NORMAL;
                updateCell(x, y);
            }
        }

    };     // clearScreen

    //----------------------------------------------------------------------
    this.setColumns = function(columns)
    //----------------------------------------------------------------------
    {
        modeCallback(columns); // Update CSS
        screenWidth = columns;

        // Reset parameters
        this.textWindow.width = columns;
        this.textWindow.left = 0;
        this.textWindow.top = 0;
        this.textWindow.height = screenHeight;
        this.setTextStyle(TEXT_STYLE_NORMAL);
        this.setCursorPosition(0, 0);        

        // Rebuild screen
        init();
    };

    //----------------------------------------------------------------------
    this.scrollScreen = function()
    //----------------------------------------------------------------------
    {
        var i, j, x, y, c1, c2;

        //
        // Move rows up
        //
        for (j = 0; j < self.textWindow.height - 1; ++j) {
            y = j + self.textWindow.top;

            for (i = 0; i < self.textWindow.width; ++i) {
                x = i + self.textWindow.left;

                c1 = screenGrid[x + screenWidth * y];
                c2 = screenGrid[x + screenWidth * (y + 1)];
                
                c1.c = c2.c;
                c1.s = c2.s;
                updateCell(x, y);
            }
        }

        //
        // New last row
        // 
        y = self.textWindow.top + (self.textWindow.height - 1);
        for (i = 0; i < self.textWindow.width; ++i) {
            x = i + self.textWindow.left;

            c1 = screenGrid[x + screenWidth * y];
            c1.c = ' ';
            c1.s = curStyle;
            updateCell(x, y);
        }
    };   // scrollScreen    

    //----------------------------------------------------------------------
    this.setTextStyle = function( style )
    //----------------------------------------------------------------------
    {
        curStyle = style;
    }; // setTextStyle


    //----------------------------------------------------------------------
    // Internal
    function updateCursor()
    //----------------------------------------------------------------------
    {
        if (cursorVisible && cursorState) {
            var elem = screenGrid[cursorY * screenWidth + cursorX].elem;
            if (elem !== cursorElement.parentNode) {
                elem.appendChild(cursorElement);
            }
        }
        else if (cursorElement.parentNode) {
            cursorElement.parentNode.removeChild(cursorElement);
        }
    }

    //----------------------------------------------------------------------
    // Internal
    function lineFeed()
    //----------------------------------------------------------------------
    {
        ++cursorY;
        if (cursorY >= self.textWindow.top + self.textWindow.height) {
            cursorY = self.textWindow.top + self.textWindow.height - 1;

            if (self.autoScroll) {
                self.scrollScreen();
            }
        }

        updateCursor();

    } // lineFeed

    //----------------------------------------------------------------------
    // Internal
    function advanceCursor()
    //----------------------------------------------------------------------
    {
        // Advance the cursor
        ++cursorX;
        
        if( cursorX >= self.textWindow.left + self.textWindow.width )
        {
            cursorX = self.textWindow.left;
            lineFeed();
        }

        updateCursor();

    } // advanceCursor



    //----------------------------------------------------------------------
    // Internal
    function writeChar(c)
    //----------------------------------------------------------------------
    {
        var cell;

        // Support DOS commands via CHR$(4) hook
        if (self.dos && self.dos.writeChar(c)) {
            return;
        }

        switch( c.charCodeAt(0) )
        {
        case 7: // BEL - bell
            // BEL
            if (bell) {
                bell();
            }
            break;
            
        case 8: // BS - backspace
            cursorX -= 1;
            if (cursorX < self.textWindow.left) {
                cursorX += self.textWindow.width;
                cursorY -= 1;
                if (cursorY < self.textWindow.top) {
                    cursorY = self.textWindow.top;
                }
            }
            break;
            
        case 10: // LF line feed
            lineFeed();
            break;

        case 11: // VT - clear EOS
        case 12: // FF - clear
            // NYI
            break;

        case 13: // CR - return
        /*
            // Not sure why I put this in... doesn't repro on the Apple
            for (; cursorX < self.textWindow.left + self.textWindow.width; cursorX += 1) {
                cell = screenGrid[cursorX + screenWidth * cursorY];
                if (cell.c !== ' ' || cell.s !== curStyle) {
                    cell.c = ' ';
                    cell.s = curStyle;
                    updateCell(cursorX, cursorY);
                }
            }
        */
            cursorX = self.textWindow.left;
            lineFeed();
            break;

        case 14: // SO - normal
            curStyle = TEXT_STYLE_NORMAL;
            break;

        case 15: // SI - inverse
            curStyle = TEXT_STYLE_INVERSE;
            break;

        case 17: // DC1 - 40-column
        case 18: // DC1 - 40-column
        case 21: // NAK - quit (disable 80-col card)
        case 22: // SYN - scroll
        case 23: // ETB - scroll up
            // NYI
            break;

        case 24: // CAN - enable mousetext
            // TODO: should act as charset toggle, not require preceding INVERSE
            // http://www.umich.edu/~archive/apple2/technotes/tn/mous/TN.MOUS.006
            if (curStyle === TEXT_STYLE_MOUSETEXT) {
                curStyle = TEXT_STYLE_INVERSE;
            }
            break;

        case 25: // EM - home
            // TODO: Test w/ textWindow set on real Apple
            self.setCursorPosition(0, 0);
            break;
            
        case 26: // SUB - clear line
            // NYI
            break;

        case 27: // ESC - enable mousetext
            // TODO: should act as charset toggle, not require preceding INVERSE
            // http://www.umich.edu/~archive/apple2/technotes/tn/mous/TN.MOUS.006
            if (curStyle === TEXT_STYLE_INVERSE) {
                curStyle = TEXT_STYLE_MOUSETEXT;
            }
            break;

        case 28: // FS - forward space
        case 29: // GS - clear EOL
        case 30: // RS - gotoXY
            // NYI
            break;

        default:
            cell = screenGrid[cursorX + screenWidth * cursorY];
            if (cell.c !== c || cell.s !== curStyle) {
                cell.c = c;
                cell.s = curStyle;

                updateCell(cursorX, cursorY);
            }
            advanceCursor();
            break;
        }

    } // writeChar

    //----------------------------------------------------------------------
    this.putChar = function(c, x, y)
    //----------------------------------------------------------------------
    {
        if (x !== undefined || y !== undefined) {
            self.setCursorPosition(x, y);
        }

        writeChar(c);

    };  // putChar


    //----------------------------------------------------------------------
    this.putString = function( s, x, y )
    //----------------------------------------------------------------------
    {
        if( x !== undefined || y !== undefined )
        {
            self.setCursorPosition( x, y );
        }
        
        var i, c;
        for( i = 0; i < s.length; ++i )
        {
            c = s.charAt(i);

            writeChar( c );
        }

    }; // putString


    //----------------------------------------------------------------------
    this.getScreenSize = function()
    //----------------------------------------------------------------------
    {
        return { width: screenWidth, height: screenHeight };
    }; // getScreenSize

    //----------------------------------------------------------------------
    this.getCursorPosition = function()
    //----------------------------------------------------------------------
    {
        return { x: cursorX, y: cursorY };
    }; // getCursorPosition

    //----------------------------------------------------------------------
    this.setCursorPosition = function(x, y)
    //----------------------------------------------------------------------
    {
        if (x !== undefined) {
            x = Math.min(Math.max(Math.floor(x), 0), screenWidth - 1);
        }
        else {
            x = cursorX;
        }

        if (y !== undefined) {
            y = Math.min(Math.max(Math.floor(y), 0), screenHeight - 1);
        }
        else {
            y = cursorY;
        }

        if (x === cursorX && y === cursorY) {
            // no-op
            return;
        }

        cursorX = x;
        cursorY = y;
        updateCursor();
        
    };         // setCursorPosition



    var cursorTimeout;
    //----------------------------------------------------------------------
    // Internal
    function blinkCursor()
    //----------------------------------------------------------------------
    {
        cursorState = !cursorState;
        updateCursor();
        cursorTimeout = setTimeout(blinkCursor, 500);

    } // blinkCursor

    //----------------------------------------------------------------------
    this.showCursor = function()
    //----------------------------------------------------------------------
    {
        cursorVisible = true;
        blinkCursor();

    };    // showCursor

    //----------------------------------------------------------------------
    this.hideCursor = function()
    //----------------------------------------------------------------------
    {
        clearTimeout(cursorTimeout);
        cursorVisible = false;
        updateCursor();

    };        // hideCursor


    //----------------------------------------------------------------------
    // Internal
    function blinkFlash()
    //----------------------------------------------------------------------
    {
        flashState = !flashState;
        for (var idx in flashCells) {
            if (flashCells.hasOwnProperty(idx)) {
                var cell = flashCells[idx];
                var code = cell.c.charCodeAt(0);
                if (flashState) {
                    code += 128;
                }

                setCellStyle(cell.elem, code);
            }
        }

        setTimeout(blinkFlash, 250);
    } // blinkFlash

    // TODO: Add numeric keypad keys
    var KEY_CANCEL        =   3,
        KEY_HELP          =   6,
        KEY_BACK_SPACE    =   8,
        KEY_TAB           =   9,
        KEY_RETURN        =  13,
        KEY_SHIFT         =  16,
        KEY_CONTROL       =  17,
        KEY_ALT           =  18,
        KEY_PAUSE         =  19,
        KEY_CAPSLOCK      =  20,
        KEY_ESCAPE        =  27,
        KEY_SPACE         =  32,
        KEY_PAGEUP        =  33,
        KEY_PAGEDOWN      =  34,
        KEY_END           =  35,
        KEY_HOME          =  36,
        KEY_LEFT          =  37,
        KEY_UP            =  38,
        KEY_RIGHT         =  39,
        KEY_DOWN          =  40,
        KEY_PRINTSCREEN   =  44,
        KEY_INSERT        =  45,
        KEY_DELETE        =  46,
        KEY_0             =  48,
        // ...        
        KEY_9             =  57,
        KEY_SEMICOLON     =  59, // ; : // Gecko
        KEY_EQUALS        =  61, // = + // Gecko
        KEY_A             =  65,
        // ...
        KEY_Z             =  90,
        KEY_WINDOWS       =  91,
        KEY_CONTEXT_MENU  =  93,
        KEY_NUMPAD_0      =  96,
        // ...
        KEY_NUMPAD_9      = 107,
        KEY_SUBTRACT      = 109, // - _ // Gecko
        KEY_F1            = 112,
        KEY_F2            = 113,
        KEY_F3            = 114,
        KEY_F4            = 115,
        KEY_F5            = 116,
        KEY_F6            = 117,
        KEY_F7            = 118,
        KEY_F8            = 119,
        KEY_F9            = 120,
        KEY_F10           = 121,
        KEY_F11           = 122,
        KEY_F12           = 123,
        KEY_SEMICOLON2    = 186, // ; :  // IE
        KEY_EQUALS2       = 187, // = +  // IE
        KEY_COMMA         = 188, // , <
        KEY_SUBTRACT2     = 189, // - _  // IE
        KEY_PERIOD        = 190, // . >
        KEY_SLASH         = 191, // / ?
        KEY_BACK_QUOTE    = 192, // ` ~
        KEY_OPEN_BRACKET  = 219, // [ {
        KEY_BACKSLASH     = 220, // \ |
        KEY_CLOSE_BRACKET = 221, // ] }
        KEY_QUOTE         = 222; // ' "

    //----------------------------------------------------------------------
    // Internal
    function onKey(code)
    //----------------------------------------------------------------------
    {
        var cb, c, s;

        keyboardRegister = code | 0x80;

        if (charCallback) {
            keyboardRegister = keyboardRegister & 0x7f;

            cb = charCallback;
            charCallback = undefined;
            self.hideCursor();
            cb(String.fromCharCode(code));
        }
        else if (lineCallback) {
            keyboardRegister = keyboardRegister & 0x7f;

            if (code >= 32 && code <= 127) {
                c = String.fromCharCode(code);
                inputBuffer.push(c);
                self.putChar(c); // echo
            }
            else {
                switch (code) {
                    case 8:  // Left Arrow/Backspace	
                        if (inputBuffer.length > 0) {
                            inputBuffer.pop();
                            self.setCursorPosition(Math.max(self.getCursorPosition().x - 1, 0), self.getCursorPosition().y);
                        }
                        break;

                    case 13: // Enter
                        // Respond to INPUT callback, if defined
                        s = inputBuffer.join("");
                        inputBuffer = [];
                        self.putString("\r");

                        cb = lineCallback;
                        lineCallback = undefined;
                        self.hideCursor();
                        cb(s);
                        break;
                }
            }
        }
        else {
            // nothing - key stays in the keyboard register
        }
    } // onKey


    // Caps lock state is tracked unique to the TTY
    var capsLock = true; 
    //----------------------------------------------------------------------
    // Internal
    function handleKeyDown( e )
    //----------------------------------------------------------------------
    {
        // s = []; for( k in e ){ s.push( k + ": " + e[k] + "," ); }  alert( s.join("") );

        if( !e.keyCode )
        {
            return true;
        }
        
        // Allow default processing of Tab and F5, for keyboard accessibility of page
        if( e.keyCode === KEY_TAB || e.keyCode === KEY_F5 )
        {
            return true;
        }
        var handled = false;

        function key( code )
        {
            onKey( code );
            handled = true;
        }
        function button( index )
        {
            buttonState[index] = 255;
            handled = true;
        }

        if( e.keyCode === KEY_CAPSLOCK )
        {
            capsLock = !capsLock;
            handled = true;
        }
        else if( KEY_A <= e.keyCode && e.keyCode <= KEY_Z )
        {
            // Alphabet keys
            if( e.ctrlKey )
            {
                key( e.keyCode - 64 ); // Control keys, Apple II-style
            }
            else if( capsLock || e.shiftKey )
            {
                key( e.keyCode ); // Upper case
            }
            else
            {
                key( e.keyCode + 32 ); // Lower case
            }
        }
        else if( KEY_0 <= e.keyCode && e.keyCode <= KEY_9 )
        {
            // Number keys
            if( e.shiftKey )
            {
                key( ")!@#$%^&*()".charCodeAt( e.keyCode - KEY_0 ) );
            }
            else
            {
                key( e.keyCode );
            }
        }
        else if( KEY_NUMPAD_0 <= e.keyCode && e.keyCode <= KEY_NUMPAD_9 )
        {
            // Numeric keypad number keys
            key( e.keyCode - KEY_NUMPAD_0 + KEY_0 );
        }
        else
        {
            switch( e.keyCode )
            {
                // Punctuation
                case KEY_SPACE:         key( e.keyCode ); break;

                case KEY_SEMICOLON:     // IE/Firefox send different codes
                case KEY_SEMICOLON2:    key( ";:".charCodeAt(e.shiftKey?1:0) ); break;
                case KEY_EQUALS:        // IE/Firefox send different codes
                case KEY_EQUALS2:       key( "=+".charCodeAt(e.shiftKey?1:0) ); break;
                case KEY_SUBTRACT:      // IE/Firefox send different codes
                case KEY_SUBTRACT2:     key( "-_".charCodeAt(e.shiftKey?1:0) ); break;

                case KEY_COMMA:         key( ",<".charCodeAt(e.shiftKey?1:0) ); break;
                case KEY_PERIOD:        key( ".>".charCodeAt(e.shiftKey?1:0) ); break;
                case KEY_SLASH:         key( "/?".charCodeAt(e.shiftKey?1:0) ); break;
                case KEY_BACK_QUOTE:    key( "`~".charCodeAt(e.shiftKey?1:0) ); break;
                case KEY_OPEN_BRACKET:  key( "[{".charCodeAt(e.shiftKey?1:0) ); break;
                case KEY_BACKSLASH:     key("\\|".charCodeAt(e.shiftKey?1:0) ); break;
                case KEY_CLOSE_BRACKET: key( "]}".charCodeAt(e.shiftKey?1:0) ); break;
                case KEY_QUOTE:         key("'\"".charCodeAt(e.shiftKey?1:0) ); break;
                
                // Non-Printables
                case KEY_BACK_SPACE: key(   8 ); break; // Not a real Apple II key, behave as left arrow
              //case KEY_TAB:        key(   9 ); break; // NOTE: Blocked above, for web page accessibility
                case KEY_RETURN:     key(  13 ); break;
                case KEY_ESCAPE:     key(  27 ); break;
                case KEY_LEFT:       key(   8 ); break;
                case KEY_UP:         key(  11 ); break;
                case KEY_RIGHT:      key(  21 ); break;
                case KEY_DOWN:       key(  10 ); break;
                case KEY_DELETE:     key( 127 ); break;
                    
                // Meta Keys
                case KEY_SHIFT:
                case KEY_CONTROL:
                case KEY_ALT:
                    break;

                // Not present on Apple II
                case KEY_CANCEL:
                case KEY_HELP:
                case KEY_PRINTSCREEN:
                case KEY_PAUSE:
                case KEY_INSERT:
                    break;

                // Not present, used as paddle buttons (0=Open Apple, 1=Solid Apple)
                case KEY_HOME:     button( 0 ); break;
                case KEY_END:      button( 1 ); break;
                case KEY_PAGEUP:   button( 2 ); break;
                case KEY_PAGEDOWN: button( 3 ); break;
            }
        }

        if( handled )
        {
            e.cancelBubble = true; // IE
            e.returnValue = false;
            if( e.stopPropagation ) { e.stopPropagation(); } // W3C
            if( e.preventDefault  ) { e.preventDefault();  } // e.g. to block arrows from scrolling the page
            return false;
        }
        else
        {            
            return true;
        }
        
    } // handleKeyDown

    //----------------------------------------------------------------------
    // Internal
    function handleKeyUp( e )
    //----------------------------------------------------------------------
    {
        if( !e.keyCode )
        {
            return true;
        }

        // Allow default processing of Tab and F5, for keyboard accessibility of page
        if( e.keyCode === KEY_TAB || e.keyCode === KEY_F5 )
        {
            return true;
        }
        
        var handled = false;
        function button( index )
        {
            buttonState[index] = 0;
            handled = true;
        }
        
        if( e.keyCode )
        {
            switch( e.keyCode )
            {
                case KEY_HOME:     button( 0 ); break;
                case KEY_END:      button( 1 ); break;
                case KEY_PAGEUP:   button( 2 ); break;
                case KEY_PAGEDOWN: button( 3 ); break;
            }
        }

        if( handled )
        {
            e.cancelBubble = true; // IE
            e.returnValue = false;
            if( e.stopPropagation ) { e.stopPropagation(); } // W3C
            if( e.preventDefault  ) { e.preventDefault();  } // e.g. to block arrows from scrolling the page
            return false;
        }
        else
        {            
            return true;
        }
        
    } // handleKeyUp

    
    //----------------------------------------------------------------------
    this.getButtonState = function( btn )
    //----------------------------------------------------------------------
    {
        return buttonState[btn];
        
    }; // getButtonState


    //----------------------------------------------------------------------
    this.focus = function()
    //----------------------------------------------------------------------
    {
        keyboardElement.focus();
    }; // focus


    //----------------------------------------------------------------------
    this.splitScreen = function(splitAt)
    //----------------------------------------------------------------------
    {
        splitPos = splitAt;
        
        var y;

        for (y = 0; y < screenHeight; ++y) {
            screenRow[y].style.visibility = (y < splitPos) ? "hidden" : "";
        }

    };   // splitScreen

    //----------------------------------------------------------------------
    this.getScreenSize = function()
    //----------------------------------------------------------------------
    {
        return { width: screenWidth, height: screenHeight };
    }; // getScreenSize

 
    //----------------------------------------------------------------------
    this.readLine = function( callback, prompt )
    //----------------------------------------------------------------------
    {
        if( self.dos && self.dos.readLine( callback, prompt ) )
        {
            return;
        }
    
        self.putString( prompt );
        
        lineCallback = callback;
        self.showCursor();
        self.focus();
        
    }; // readLine

    //----------------------------------------------------------------------
    this.readChar = function( callback, hidePrompt )
    //----------------------------------------------------------------------
    {
        if( self.dos && self.dos.readChar( callback ) )
        {
            return;
        }
        
        // If there is a key ready, deliver it immediately
        if( keyboardRegister & 0x80 )
        {
            keyboardRegister = keyboardRegister & 0x7f;
            
            // Non-blocking return
            setTimeout( function() { callback( String.fromCharCode(keyboardRegister) ); }, 0 );
        }
        else
        {
            charCallback = callback;
            self.showCursor();
            self.focus();
        }
        
    }; // readChar

    
    //----------------------------------------------------------------------
    this.getKeyboardRegister = function()
    //----------------------------------------------------------------------
    {
        return keyboardRegister;
    }; // getKeyboardRegister

    //----------------------------------------------------------------------
    this.clearKeyboardStrobe = function()
    //----------------------------------------------------------------------
    {
        keyboardRegister = keyboardRegister & 0x7f;
    }; // clearKeyboardStrobe
    
    
    this.print = function( s ) { self.putString( s ); };
    this.printError = function( s ) { window.alert( s ); };

    //----------------------------------------------------------------------
    // Constructor Logic
    //----------------------------------------------------------------------
        
    screenElement.innerHTML = "";
    //screenElement.style.whiteSpace = 'pre';
        
    init();
    self.setCursorPosition( 0, 0 );

    addEvent( keyboardElement, 'keydown',  handleKeyDown );
    addEvent( keyboardElement, 'keyup',    handleKeyUp );
    blinkFlash();
}


