//
// Applesoft BASIC Interpreter in Javascript
// DOS 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.


// Usage:
//   var dos = new DOS()
//   consumed = dos.readChar( callback )         // GET hook
//   consumed = dos.readLine( callback, prompt ) // INPUT hook
//   consumed = dos.writeChar( c )               // COUT hook
//   dos.reset()                                 // Close all open buffers
// Example:
//   if( !dos.writeChar( c ) ) { displayChar( c ); }
//   if( !dos.readLine( cb ) ) { promptUser( cb ); }

/*extern ActiveXObject */ // for jslint.com

function DOS( tty )
{
    // For references to "this" within callbacks and closures
    var self = this;

    var DOSErrors = {};
    DOSErrors.LANGUAGE_NOT_AVAILABLE = 1;
    DOSErrors.RANGE_ERROR            = 2;
    DOSErrors.WRITE_PROTECTED        = 4;
    DOSErrors.END_OF_DATA            = 5;
    DOSErrors.FILE_NOT_FOUND         = 6;
    DOSErrors.VOLUME_MISMATCH        = 7;
    DOSErrors.IO_ERROR               = 8;
    DOSErrors.DISK_FULL              = 9;
    DOSErrors.FILE_LOCKED           = 10;
    DOSErrors.SYNTAX_ERROR          = 11;
    DOSErrors.NO_BUFFERS_AVAILABLE  = 12;
    DOSErrors.FILE_TYPE_MISMATCH    = 13;
    DOSErrors.PROGRAM_TOO_LARGE     = 14;
    DOSErrors.NOT_DIRECT_COMMAND    = 15;

    // For MON/NOMON
    var MON_I = 1;
    var MON_C = 2;
    var MON_O = 4;
    

    //----------------------------------------------------------------------
    // Internal - crack arguments e.g. ",S6,D1"
    function parseArgs( str )
    //----------------------------------------------------------------------
    {
        // TODO: Take in list of supported arguments
        
        // Set these to zero so they're always defined when passed into command handlers
        var args = 
        {
            V: 0, // Volume
            D: 0, // Drive
            S: 0, // Slot
            L: 0, // Length
            R: 0, // Record/Relative
            B: 0, // Byte
            A: 0, // Address
            C: undefined, // Echo Commands
            I: undefined, // Echo Input
            O: undefined  // Echo Output
        };
        
        while( str.match( /^,?\s*([VDSLRBACIO])\s*([0-9]+|\$[0-9A-Fa-f]+)?\s*(.*)/ ) )
        {
            args[RegExp.$1] = parseInt( RegExp.$2, 10 ) || 0;
            str = RegExp.$3;    
        }
        
        if( str.length > 0 )
        {
            doserror( DOSErrors.SYNTAX_ERROR, "Unexpected arguments: " + str );
        }

        return args;
        
    } // parseArgs


    //----------------------------------------------------------------------
    function doserror( code, reason )
    //----------------------------------------------------------------------
    {
        throw { catchableError: true, errorMessage:  "DOS Error ("+code+"): " + reason };
    
    } // error

    //----------------------------------------------------------------------
    function executeCommand( command )
    //----------------------------------------------------------------------
    {
        // Delegate to various commands
        // http://www.xs4all.nl/~fjkraan/comp/apple2faq/app2doscmdfaq.html
        // http://www.textfiles.com/apple/ANATOMY/
        
        var filename, args;
        
        if( self.monico & MON_C )
        {
            tty.putString( command + "\r" );
        }
        
        if( command.match( /^MON(.*)/ ) )
        {
            // MON[,C][,I][,O]                 Traces DOS 3.3 commands ('Commands', 'Input' and 'Output')
            args = parseArgs( RegExp.$1 );

            if( args.I !== undefined )
            {
                self.monico |= MON_I;
            }
            if( args.C !== undefined )
            {
                self.monico |= MON_C;
            }
            if( args.O !== undefined )
            {
                self.monico |= MON_O;
            }
            
        }
        else if( command.match( /^NOMON(.*)/ ) )
        {
            // NOMON[,C][,I][,O]               Cancels tracing of DOS 3.3 commands ('Commands', 'Input' and 'Output')
            args = parseArgs( RegExp.$1 );
            if( args.I !== undefined )
            {
                self.monico &= ~MON_I;
            }
            if( args.C !== undefined )
            {
                self.monico &= ~MON_C;
            }
            if( args.O !== undefined )
            {
                self.monico &= ~MON_O;
            }
        }
        else if( command.match( /^OPEN\s*([^,]+)(,.*)?/ ) )
        {
            // OPEN filename[,Llen]            Opens a text file.
            filename = RegExp.$1;
            args = parseArgs( RegExp.$2 );
            open( filename, args.L );
        }
        else if( command.match( /^APPEND\s*([^,]+)(,.*)?/ ) )
        {
            // APPEND filename                 Appends to a text file.
            filename = RegExp.$1;
            args = parseArgs( RegExp.$2 );
            append( filename, args.L );
        }
        else if( command.match( /^CLOSE\s*([^,]+)?(,.*)?/ ) )
        {
            // CLOSE [filename]                Closes specified (or all) open text files.
            filename = RegExp.$1;
            close( filename );
        }
        else if( command.match( /^POSITION\s*([^,]+)(,.*)?/ ) )
        {
            // POSITION filename[,Rnum]        Advances position in text file.
            filename = RegExp.$1;
            args = parseArgs( RegExp.$2 );
            position( filename, args.R );
        }
        else if( command.match( /^READ\s*([^,]+)(,.*)?/ ) )
        {
            // READ filename[,Rnum][,Bbyte]    Reads from a text file.
            filename = RegExp.$1;
            args = parseArgs( RegExp.$2 );
            read( filename, args.R, args.B );
        }
        else if( command.match( /^WRITE\s*([^,]+)(,.*)?/ ) )
        {
            // WRITE filename[,Rnum][,Bbyte]   Writes to a text file.
            filename = RegExp.$1;
            args = parseArgs( RegExp.$2 );
            write( filename, args.R, args.B );
        }
        else if( command.match( /^DELETE\s*([^,]+)(,.*)?/ ) )
        {
            // DELETE filename                 Delete a file
            filename = RegExp.$1;
            args = parseArgs( RegExp.$2 );
            unlink( filename );
        }
        else if( command.match( /^RENAME\s*([^,]+),\s*([^,]+)(,.*)?/ ) )
        {
            // RENAME filename,filename        Rename a file
            filename1 = RegExp.$1;
            filename2 = RegExp.$2;
            args = parseArgs( RegExp.$3 );
            rename( filename1, filename2 );
        }
        else if( command.match( /^$/ ) )
        {
            // Null command - terminates a READ/WRITE, but doesn't CLOSE
            // (leaves record length intact on open buffer)
            self.activebuffer = null;
            self.mode = "";
        }
        else
        {
            doserror( DOSErrors.SYNTAX_ERROR, "Syntax Error (or command not supported): " + command );
        }    
    }
    
    
    self.files = {};
    self.buffers = {};
    self.activebuffer = null;
    self.mode = "";
    self.monico = 0;
    
    //----------------------------------------------------------------------
    this.reset = function()
    //----------------------------------------------------------------------
    {
        self.buffers = {};
        self.activebuffer = null;
        self.mode = "";

    }; // reset
    
    //----------------------------------------------------------------------
    function unlink( filename )
    //----------------------------------------------------------------------
    {
        if( self.files[filename] === undefined )
        {
            doserror( DOSErrors.FILE_NOT_FOUND, "File not found: " + filename );
        }

        delete self.files[filename];
        
    } // unlink

    //----------------------------------------------------------------------
    function rename( oldname, newname )
    //----------------------------------------------------------------------
    {
        if( self.files[oldname] === undefined )
        {
            doserror( DOSErrors.FILE_NOT_FOUND, "File not found: " + filename );
        }

        self.files[newname] = self.files[oldname];
        delete self.files[oldname];
        
    } // rename

    //----------------------------------------------------------------------
    function open( filename, recordlength )
    //----------------------------------------------------------------------
    {
        if( recordlength === 0 )
        {
            // Sequential access
            recordlength = 1;
        }

        // Peek in the VFS cache first
        var file = self.files[filename];
        if( file === undefined )
        {
            // Not cached - do a synchronous XmlHttpRequest for the file here
            var req = window.XMLHttpRequest ? new XMLHttpRequest() : window.ActiveXObject ? new ActiveXObject("Microsoft.XMLHTTP") : null;
            if( req )
            {
                try
                {
                    var url = "vfs/" + escape(filename.replace( /\./g, '_' )) + ".txt";
                    req.open( "GET", url, false ); // synchronous
                    req.send( null );
                    if( req.status === 200 || req.status === 0 ) // 0 for file:// protocol
                    {
                        file = { data: req.responseText.replace(/\r\n/g, "\r") };
                        self.files[filename] = file;
                    }
                }
                catch( e )
                {
                    // File doesn't exist - APPEND/READ will fail
                    throw e;
                }
            }            
        }

        // Create a buffer for the file        
        self.buffers[filename] = { file: file, recordlength: recordlength, recordnum: 0, filepointer: 0 };
    } // open

    //----------------------------------------------------------------------
    function append( filename, recordlength )
    //----------------------------------------------------------------------
    {
        // Normal open logic
        open( filename, recordlength );

        var buf = self.buffers[filename];

        if( buf.file === undefined )
        {
            doserror( DOSErrors.FILE_NOT_FOUND, "File not found: " + filename );
        }

        // Then seek to the end of the file
        buf.filepointer = buf.file.data.length;
        buf.recordnum = Math.floor( buf.filepointer / buf.recordlength );

    } // append

    //----------------------------------------------------------------------
    function close( filename )
    //----------------------------------------------------------------------
    {
        if( filename !== undefined && filename.length > 0 )
        {
            var buf = self.buffers[filename];
            if( buf !== undefined )
            {
                delete self.buffers[filename];
                if( buf === self.activebuffer )
                {
                    self.activebuffer = null;
                    self.mode = "";
                }
            }
        }
        else
        {
            self.buffers = {};
            self.activebuffer = null;
            self.mode = "";
        }
    } // close
    
    //----------------------------------------------------------------------
    function read( filename, recordnum, bytenum )
    //----------------------------------------------------------------------
    {
        var buf = self.buffers[filename];
        if( !buf )
        {
            // Open file if no such named buffer, but don't create it
            open( filename, 0 );
            buf = self.buffers[filename];
        }
        
        if( buf.file === undefined )
        {
            doserror( DOSErrors.FILE_NOT_FOUND, "File not found: " + filename );
        }

        // Set the file position
        buf.recordnum   = recordnum;
        buf.filepointer = buf.recordlength * recordnum + bytenum;

        // Set the active buffer into read mode
        self.activebuffer = buf;
        self.mode = "r";
    } // read

    //----------------------------------------------------------------------
    function write( filename, recordnum, bytenum )
    //----------------------------------------------------------------------
    {
        var buf = self.buffers[filename];
        
        if( !buf )
        {
            // Must open the file before writing
            doserror( DOSErrors.FILE_NOT_FOUND, "File not found: " + filename );
        }
        
        if( buf.file === undefined )
        {
            // If we still don't have it, create in VFS if necessary
            var file = { data: "" };
            self.files[filename] = file;
            buf.file = file;
        }
       
        
        // Set up the file position
        buf.recordnum = recordnum;
        if( buf.recordlength > 1 )
        {
            buf.filepointer = buf.recordlength * recordnum;
        }
        buf.filepointer += bytenum;

        // Set the active buffer into write mode
        self.activebuffer = buf;
        self.mode = "w";
    } // write
    
    //----------------------------------------------------------------------
    function position( filename, records )
    //----------------------------------------------------------------------
    {
        var buf = self.buffers[filename];
        if( !buf )
        {
            // Open file if no such named buffer, but don't create it
            open( filename, 0, false );
            buf = self.buffer[filename];
        }
        
        // Set up the file position
        buf.recordnum += records;
        buf.filepointer += buf.recordlength * records;

    } // position
    
    //----------------------------------------------------------------------
    this.readLine = function( callback, prompt )
    //----------------------------------------------------------------------
    {
        // Called by TTY; if a file is open, read and call back asynchronously and return true
        // Otherwise, return false to delegate to the TTY

        var string = "", c, data, len, fp, buffer; 
        if( self.mode === "r" )
        {
            // Cache for performance
            data = self.activebuffer.file.data;
            len = data.length;
            fp = self.activebuffer.filepointer;

            if( fp >= len )
            {
                doserror( DOSErrors.END_OF_DATA, "End of data" );
            }
            
            buffer = [];
            while( fp < len )
            {
                // Sequential Access
                c = data[ fp++ ];
                if( c === "\r" || c === "\n" || c === "\x00" )
                {
                    break;
                }
                else
                {
                    buffer.push( c );
                }
            }
            self.activebuffer.filepointer = fp;
            string = buffer.join( "" );
            
            if( self.monico & MON_I )
            {
                tty.putString( prompt + string + "\r" ); // TODO: Whoah, the real INPUT prompt appears here! Propagate that along, I guess!
            }

            // Non-blocking return            
            setTimeout( function() { callback( string ); }, 0 );
            return true;   
        }

        return false;
    }; // readLine
    
    //----------------------------------------------------------------------
    this.readChar = function( callback )
    //----------------------------------------------------------------------
    {
        // Called by TTY; if a file is open, read and call back asynchronously and return true
        // Otherwise, return false to delegate to the TTY

        var character = "";
        if( self.mode === "r" )
        {
            if( self.activebuffer.filepointer >= self.activebuffer.file.data.length )
            {
                doserror( DOSErrors.END_OF_DATA, "End of data");
            }

            character = self.activebuffer.file.data[ self.activebuffer.filepointer ];
            ++self.activebuffer.filepointer;

            if( self.monico & MON_I )
            {
                tty.putChar( character );
            }

            // Non-blocking return
            setTimeout( function() { callback( character ); }, 0 );
            return true;
        }

        return false;
    }; // readChar

    var commandBuffer = "";
    var commandMode = false;    

    //----------------------------------------------------------------------
    this.writeChar = function( c )
    //----------------------------------------------------------------------
    {
        if( commandMode )
        {
            if( c === "\r" )
            {
                commandMode = false;
                executeCommand( commandBuffer );
                commandBuffer = "";
                return true;
            }
            else
            {
                commandBuffer += c;
                return true;
            }
        } 
        else if( c === "\x04" )
        {
            commandBuffer = "";
            commandMode = true;
            return true;
        }

        if( self.mode === "w" )
        {        
            if( self.monico & MON_I )
            {
                // TODO: Make sure this doesn't recursively loop
                //tty.putChar( string )
            }

            var buf = self.activebuffer;
            
            // Extend file to necessary length
            while( buf.filepointer > buf.file.data.length )
            {
                buf.file.data += "\x00";
            }
            
            // Append or insert character
            if( buf.filepointer === buf.file.data.length )
            {            
                buf.file.data += c;
            }
            else
            {
                var d = buf.file.data.substring( 0, buf.filepointer );
                d += c;
                d += buf.file.data.substring( buf.filepointer + 1 );
                buf.file.data = d;
            }
            
            buf.filepointer += 1;
            
            return true;
        }
                
        // Don't consume
        return false;
    }; // writeChar
   

    //----------------------------------------------------------------------
    // Constructor Logic
    //----------------------------------------------------------------------


}
