/*
  Copyright (c) 2003 Jan-Klaas Kollhof
  
  This file is part of the JavaScript o lait library(jsolait).
  
  jsolait is free software; you can redistribute it and/or modify
  it under the terms of the GNU Lesser General Public License as published by
  the Free Software Foundation; either version 2.1 of the License, or
  (at your option) any later version.
 
  This software is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU Lesser General Public License for more details.
 
  You should have received a copy of the GNU Lesser General Public License
  along with this software; if not, write to the Free Software
  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

Module=function(){
    /**
        Creates a new module and registers it.
        @param name              The name of the module.
        @param version            The version of a module.
        @param moduleScope    A function which is executed for module creation.
    */
    var Module = function(name, version, moduleScope){
        var newModule = new Object();
        newModule.version = version;
        newModule.name = name;
        newModule.toString=function(){
            return "[module %s version: %s]".format(newModule.name, newModule.version);
        }
        moduleScope(newModule);
        if(name != "jsolait"){
            jsolait.registerModule(newModule);
        }
        return newModule;
    }
    
    Module.toString = function(){
        return "[object Module]";
    }
    Module.createPrototype=function(){ 
        throw "Can't use Module as a super class.";
    }
    
    return Module;
}();

Class = function(){
    var forPrototyping = new Object();
    
    /**
        Creates a new class object which inherits from superClass.
        @param className="anonymous"  The name of the new class.
        @param superClass=Object        The class to inherit from.
        @param classScope                  A function which is executed for class construction.
    */
    var Class = function(className, superClass, classScope){
        if(arguments.length == 1){
            classScope=className;
            className = "anonymous";
            superClass = Object;
        }else if(arguments.length == 2){
            className = arguments[0];
            classScope=superClass;
            superClass = Object;
        }
        
        //this is the constructor for the new objects created from the new class.
        //if and only if it is NOT used for prototyping/subclassing the init method of the newly created object will be called.
        var NewClass = function(calledFor){
            if(calledFor !== forPrototyping){
                if(this.init){
                    return this.init.apply(this, arguments);
                }
            }
        }
        //This will create a new prototype object of the new class.
        NewClass.createPrototype = function(){
            return new NewClass(forPrototyping);
        }
        //setting class properties for the new class.
        NewClass.superClass = superClass;
        NewClass.className=className; 
        NewClass.toString = function(){
            return "[class %s]".format(NewClass.className);
        };
        if(superClass.createPrototype){//see if the super class can create prototypes. (creating an object without calling init())
            NewClass.prototype = superClass.createPrototype();
        }else{//just create an object of the super class
            NewClass.prototype = new superClass();
        }
        //reset the constructor for new objects to the actual constructor.
        NewClass.prototype.constructor = NewClass;
        
        if(superClass == Object){//all other objects already have a nice toString method.
            NewClass.prototype.toString = function(){
                return "[object %s]".format(this.constructor.className);
            };
        }
        
        //execute the scope of the class
        classScope(NewClass, superClass);
        return NewClass;
    }    
    Class.toString = function(){
        return "[object Class]";
    }
    Class.createPrototype=function(){ 
        throw "Can't use Class as a super class.";
    }
    return Class;
}();



/**
    The root module for jsolait.
    It provides some global functionality for loading modules,
    some String enhancements.
*/
jsolait=Module("jsolait", "0.8.5", function(thisMod){
    ///base url for user modules.
    thisMod.baseURL=".";
    ///The URL where jsolait is installed.
    thisMod.libURL ="./jsolait";
    ///Collection of all loaded modules.(module cache)
    thisMod.modules = new Array();
   
    thisMod.moduleURLs = {urllib:"%(libURL)s/lib/urllib.js",
                                      xml:"%(libURL)s/lib/xml.js",
                                      crypto:"%(libURL)s/lib/crypto.js",
                                      xmlrpc:"%(libURL)s/lib/xmlrpc.js"};
   
    thisMod.init=function(){
        //make jsolait work with WScript
        var ws = null;
        try{//see if WScript is available
            ws = WScript
        }catch(e){
        }
        if(ws != null){
            print=function(msg){
                WScript.echo(msg);
            }
            var args = WScript.arguments;
            try{
                //get script to execute
                var url = args(0);
                url = url.replace(/\\/g, "/");
                url = url.split("/");
                url = url.slice(0, url.length-1);
                //set base for user module loading
                thisMod.baseURL = url.join("/");
            }catch(e){
                throw new Exception(thisMod, "No script to execute was specified.");
            }
            //location of jsolait/init.js
            url = WScript.ScriptFullName;
            url = url.replace(/\\/g, "/");
            url = url.split("/");
            url = url.slice(0, url.length-1);
            thisMod.libURL = "file://" + url.join("/");
            try{
                thisMod.loadScript(args(0));
            }catch(e){
                WScript.stdErr.write("%s(1,1) jsolait runtime error:\n%s\n".format(args(0).replace("file://",""), e.toTraceString()));
            }
        }
    }
    
    /**
       Imports a module given its name(someModule.someSubModule).
       A module's file location is determined by treating each module name as a directory.
       Only the last one points to a file.
       If the module's URL is not known to jsolait then it will first be searched for in the jsolait/ext path.
       If that fails then it will be searched for in the baseURL(jsolait.baseURL which is "./" by default).
       @param name   The name of the module to load.
       @return           The module object.
    */
    thisMod.importModule = function(name){
        if (thisMod.modules[name]){ //module already loaded
            return thisMod.modules[name];
        }else{
            var src,modURL;
            //check if jsolait allready knows the url of the module(moduleURLs contains urls to modules)
            if(thisMod.moduleURLs[name]){
                modURL = thisMod.moduleURLs[name].format(thisMod);
            }else{//assume the module is an ext module located in jsolait/ext.
                modURL = "%s/ext/%s.js".format(thisMod.libURL, name.split(".").join("/"));
            }  
            try{//to load module from location calculated above
                src = getFile(modURL);
            }catch(e){//module could not be found at the location.
                try{//assume it's a user module and located at baseURL
                    modURL = "%s/%s.js".format(thisMod.baseURL, name.split(".").join("/"));
                    src = getFile(modURL);
                }catch(e){//the module realy was not found or there was a server problem
                    throw new thisMod.ModuleImportFailed(name, modURL, e);
                }
            }
            
            try{//interpret the script
                evalScript(src);
            }catch(e){
                throw new thisMod.ModuleImportFailed(name, modURL, e);
            }
            //the module should have registered itself
            //todo: what if not ???
            return thisMod.modules[name]; 
        }
    }
    //make it global
    importModule =thisMod.importModule;
    
    /**
        Loads and interprets a script file.
        @param url  The url of the script to load.
    */
    thisMod.loadScript=function(url){
        var src = getFile(url);
        try{//to interpret the source 
            evalScript(src);
        }catch(e){
            throw new thisMod.EvalFailed(url, e);
        }
    }
    /**
        Registers a new module. 
        Registered modules can be imported with importModule(...).
        @param module  The module to register.
    */
    thisMod.registerModule = function(module){
        this.modules[module.name] = module;
    }
    /**
        Creates an HTTP request object for retreiving files.
        @return HTTP request object.
    */
    var getHTTP=function() {
        var obj;
        try{ //to get the mozilla httprequest object
            obj = new XMLHttpRequest();
        }catch(e){
            try{ //to get MS HTTP request object
                obj=new ActiveXObject("Msxml2.XMLHTTP.4.0");
            }catch(e){
                try{ //to get MS HTTP request object
                    obj=new ActiveXObject("Msxml2.XMLHTTP");
                }catch(e){
                    try{// to get the old MS HTTP request object
                        obj = new ActiveXObject("microsoft.XMLHTTP"); 
                    }catch(e){
                        throw new Exception(thisMod, "Unable to get an HTTP request object.");
                    }
                }    
            }
        }
        return obj;
    }
    /**
        Retrieves a file given its URL.
        @param url             The url to load.
        @param headers=[]  The headers to use.
        @return                 The content of the file.
    */
    var getFile=function(url, headers) { 
        //if callback is defined then the operation is done async
        headers = (headers != null) ? headers : [];
        //setup the request
        try{
            var xmlhttp= getHTTP();
            xmlhttp.open("GET", url, false);
            for(var i=0;i< headers.length;i++){
                xmlhttp.setRequestHeader(headers[i][0], headers[i][1]);    
            }
            xmlhttp.send("");
        }catch(e){
            throw new Exception(thisMod, "Unable to load URL: '%s'.".format(url));
        }
        if(xmlhttp.status == 200 || xmlhttp.status == 0){
            return xmlhttp.responseText;
        }else{
             throw new Exception(thisMod, "File not loaded: '%s'.".format(url));
        }
    }
    
    Error.prototype.toTraceString = function(){
        if(this.message){
            return "%s\n".format(this.message);
        }
        if (this.description){
           return "%s\n".format(this.description);
        }
        return "unknown error\n"; 
    }
    
    /**
        BaseClass for all Exceptions.
    */
    thisMod.Exception=Class("Exception", function(thisClass){
        /**
            Initializes a new Exception.
            @param module       The module the Exception belongs to.
            @param msg           The error message for the user.
            @param trace=null   The error causing this Exception if available.
        */
        thisClass.prototype.init=function(module, msg, trace){
            this.name = this.constructor.className;
            this.message = msg;
            this.module = module;
            this.trace = trace;
        }
        
        thisClass.prototype.toString=function(){
            var s = "%s thrown in: %s\n\n".format(this.name, this.module);
            s += this.message;
            return s;
        }
        /**
            Returns the complete trace of the exception.
            @return The error trace.
        */
        thisClass.prototype.toTraceString=function(){
            var s = "%s in %s:\n    ".format(this.name, this.module );
            s+="%s\n\n".format(this.message);
            if(this.trace){
                if(this.trace.toTraceString){
                    s+= this.trace.toTraceString();
                }else{
                    s+= this.trace;
                }
            }
            return s;
        }
        ///The name of the Exception(className).
        thisClass.prototype.name;
        ///The error message.
        thisClass.prototype.message;
        ///The module the Exception belongs to.
        thisClass.prototype.module;
        ///The error which caused the Exception or null.
        thisClass.prototype.trace;      
    })
    Exception = thisMod.Exception;
        
    /**
        A BaseClass for all Import errors.
     */
    thisMod.ImportError=Class("ImportError", Exception, function(thisClass, Super){
        /**
            Creates a new ImportError.
            @param msg          The error message.
            @param trace=null  The error causing the Exception
        */
        thisClass.prototype.init=function(msg, trace){
            Super.prototype.init.call(this, thisMod, msg, trace);
        }
    })
    
    /**
        Thrown when a module could not be found.
    */
    thisMod.ModuleImportFailed=Class("ModuleImportFailed", thisMod.ImportError, function(thisClass,Super){
        /**
            Initializes a new ModuleImportFailed Exception.
            @param name      The name of the module.
            @param url          The url of the module.
            @param trace      The error cousing this Exception.
        */
        thisClass.prototype.init=function(moduleName, url, trace){
            Super.prototype.init.call(this, "Failed to import module: '%s' from URL:'%s'".format(moduleName, url), trace);
            this.moduleName = moduleName;
            this.url = url;
        }
        ///The  name of the module that was not found.
        thisClass.prototype.moduleName;
        ///The url the module was expected to be found at.
        thisClass.prototype.url;
    })
    
    /**
        Thrown when a source could not be loaded due to an interpretation error.
    */
    thisMod.EvalFailed=Class("EvalFailed", thisMod.ImportError,function(thisClass, Super){
        /**
            Initializes a new EvalFailed exception.
            @param url                   The url of the module.
            @param trace               The exception that was thrown while interpreting the module's source code.
        */
        thisClass.prototype.init=function(url, trace){
            Super.prototype.init.call(this,"File %s(1,1) Eval of script failed.".format(url), trace);
            this.url = url;
        }
        ///The url the module was expected to be found at.
        thisClass.prototype.url;
    })
    
    /**
        Displays an exception and it's trace.
        This works better than alert(e) because traces are taken into account.
        @param exception  The exception to display.
    */
    thisMod.reportException=function(exception){
        if(exception.toTraceString){
            var s= exception.toTraceString();
        }else{
            var s = exception.toString();
        }
        var ws = null;
        try{//see if WScript is available
            ws = WScript;
        }catch(e){
        }
        if(ws != null){
            WScript.stderr.write(s);
        }else{
            alert(s);
        }
    }    
    //make this global;
    reportException = thisMod.reportException;

    /**
        Creates a format specifier object. 
    */
    var FormatSpecifier=function(s){
        var s = s.match(/%(\(\w+\)){0,1}([ 0-]){0,1}(\+){0,1}(\d+){0,1}(\.\d+){0,1}(.)/);
        if(s[1]){
            this.key=s[1].slice(1,-1);
        }else{
            this.key = null;
        }
        this.paddingFlag = s[2];
        if(this.paddingFlag==""){
            this.paddingFlag =" " 
        }
        this.signed=(s[3] == "+");
        this.minLength = parseInt(s[4]);
        if(isNaN(this.minLength)){
            this.minLength=0;
        }
        if(s[5]){
            this.percision = parseInt(s[5].slice(1,s[5].length));
        }else{
            this.percision=-1;
        }
        this.type = s[6];
    }
    
    /**
        Formats a string replacing formatting specifiers with values provided as arguments
        which are formatted according to the specifier.
        This is an implementation of  python's % operator for strings and is similar to sprintf from C.
        Usage:
            resultString = formatString.format(value1, v2, ...);
        
        Each formatString can contain any number of formatting specifiers which are
        replaced with the formated values.
        
        specifier([...]-items are optional): 
            "%(key)[flag][sign][min][percision]typeOfValue"
            
            (key)  If specified the 1st argument is treated as an objec/associative array and the formating values 
                     are retrieved from that object using the key.
                
            flag:
                0      Use 0s for padding.
                -      Left justify result, padding it with spaces.
                        Use spaces for padding.
            sign:
                +      Numeric values will contain a +|- infront of the number.
            min:
                l      The string will be padded with the padding character until it has a minimum length of l. 
            percision:
               .x     Where x is the percision for floating point numbers and the lenght for 0 padding for integers.
            typeOfValue:
                d    Signed integer decimal.  	 
                i     Signed integer decimal. 	 
                b    Unsigned binary.                       //This does not exist in python!
                o    Unsigned octal. 	
                u    Unsigned decimal. 	 
                x    Unsigned hexidecimal (lowercase). 	
                X   Unsigned hexidecimal (uppercase). 	
                e   Floating point exponential format (lowercase). 	 
                E   Floating point exponential format (uppercase). 	 
                f    Floating point decimal format. 	 
                F   Floating point decimal format. 	 
                c   Single character (accepts byte or single character string). 	 
                s   String (converts any object using object.toString()). 	
        
        Examples:
            "%02d".format(8) == "08"
            "%05.2f".format(1.234) == "01.23"
            "123 in binary is: %08b".format(123) == "123 in binary is: 01111011"
            
        @param *  Each parameter is treated as a formating value. 
    */
    String.prototype.format=function(){
        var sf = this.match(/(%(\(\w+\)){0,1}[ 0-]{0,1}(\+){0,1}(\d+){0,1}(\.\d+){0,1}[dibouxXeEfFgGcrs%])|([^%]+)/g);
        if(sf){
            if(sf.join("") != this){
                throw new Exception(thisMod, "Unsupported formating string.");
            }
        }else{
            throw new Exception(thisMod, "Unsupported formating string.");
        }
        var rslt ="";
        var s;
        var obj;
        var cnt=0;
        var frmt;
        var sign="";
        
        for(var i=0;i<sf.length;i++){
            s=sf[i];
            if(s == "%%"){
                s = "%";
            }else if(s.slice(0,1) == "%"){
                frmt = new FormatSpecifier(s);//get the formating object
                if(frmt.key){//an object was given as formating value
                    if((typeof arguments[0]) == "object" && arguments.length == 1){
                        obj = arguments[0][frmt.key];
                    }else{
                        throw new Exception(thisMod, "Object or associative array expected as formating value.");
                    }
                }else{//get the current value
                    if(cnt>=arguments.length){
                        throw new Exception(thisMod, "Not enough arguments for format string");
                    }else{
                        obj=arguments[cnt];
                        cnt++;
                    }
                }
                    
                if(frmt.type == "s"){//String
                    s=obj.toString().pad(frmt.paddingFlag, frmt.minLength);
                    
                }else if(frmt.type == "c"){//Character
                    if(frmt.paddingFlag == "0"){
                        frmt.paddingFlag=" ";//padding only spaces
                    }
                    if(typeof obj == "number"){//get the character code
                        s = String.fromCharCode(obj).pad(frmt.paddingFlag , frmt.minLength) ;
                    }else if(typeof obj == "string"){
                        if(obj.length == 1){//make sure it's a single character
                            s=obj.pad(frmt.paddingFlag, frmt.minLength);
                        }else{
                            throw new Exception(thisMod, "Character of length 1 required.");
                        }
                    }else{
                        throw new Exception(thisMod, "Character or Byte required.");
                    }
                }else if(typeof obj == "number"){
                    //get sign of the number
                    if(obj < 0){
                        obj = -obj;
                        sign = "-"; //negative signs are always needed
                    }else if(frmt.signed){
                        sign = "+"; // if sign is always wanted add it 
                    }else{
                        sign = "";
                    }
                    //do percision padding and number conversions
                    switch(frmt.type){
                        case "f": //floats
                        case "F":
                            if(frmt.percision > -1){
                                s = obj.toFixed(frmt.percision).toString();
                            }else{
                                s = obj.toString();
                            }
                            break;
                        case "E"://exponential
                        case "e":
                            if(frmt.percision > -1){
                                s = obj.toExponential(frmt.percision);
                            }else{
                                s = obj.toExponential();
                            }
                            s = s.replace("e", frmt.type);
                            break;
                        case "b"://binary
                            s = obj.toString(2);
                            s = s.pad("0", frmt.percision);
                            break;
                        case "o"://octal
                            s = obj.toString(8);
                            s = s.pad("0", frmt.percision);
                            break;
                        case "x"://hexadecimal
                            s = obj.toString(16).toLowerCase();
                            s = s.pad("0", frmt.percision);
                            break;
                        case "X"://hexadecimal
                            s = obj.toString(16).toUpperCase();
                            s = s.pad("0", frmt.percision);
                            break;
                        default://integers
                            s = parseInt(obj).toString();
                            s = s.pad("0", frmt.percision);
                            break;
                    }
                    if(frmt.paddingFlag == "0"){//do 0-padding
                        //make sure that the length of the possible sign is not ignored
                        s=s.pad("0", frmt.minLength - sign.length);
                    }
                    s=sign + s;//add sign
                    s=s.pad(frmt.paddingFlag, frmt.minLength);//do padding and justifiing
                }else{
                    throw new Exception(thisMod, "Number required.");
                }
            }
            rslt += s;
        }
        return rslt;
    }
    
    /**
        Padds a String with a character to have a minimum length.
        
        @param flag   "-":      to padd with " " and left justify the string.
                            Other: the character to use for padding. 
        @param len    The minimum length of the resulting string.
    */
    String.prototype.pad = function(flag, len){
        var s = "";
        if(flag == "-"){
            var c = " ";
        }else{
            var c = flag;
        }
        for(var i=0;i<len-this.length;i++){
            s += c;
        }
        if(flag == "-"){
            s = this + s;
        }else{
            s += this;
        }
        return s;
    }
    
    /**
        Repeats a string.
        @param c  The count how often the string should be repeated.
    */
    String.prototype.mul = function(c){
        var a = new Array(this.length * c);
        var s=""+ this;
        for(var i=0;i<c;i++){
            a[i] = s;
        }
        return a.join("");
    }
    
    /**
        Decodes an encoded string.
        Parameters but the codec parameter are forwardet to the codec.
        @param codec  The codec to use.
    */
    String.prototype.decode = function(codec){
        var n ="decode_" + codec;
        if(String.prototype[n]){
            var args=[];
            for(var i=1;i<arguments.length;i++){
                args[i-1] = arguments[i];
            }
            return String.prototype[n].apply(this, args);
        }else{
            throw new Exception(thisMod, "Decoder '%s' not found.".format(codec));
        }
    }
    
    /**
        Encodes a string.
        Parameters but the codec parameter are forwardet to the codec.
        @param codec  The codec to use.
    */
    String.prototype.encode = function(codec){
        var n ="encode_" + codec;
        if(String.prototype[n]){
            var args=[];
            for(var i=1;i<arguments.length;i++){
                args[i-1] = arguments[i];
            }
            return String.prototype[n].apply(this, args);
        }else{
            throw new Exception(thisMod, "Ecnoder '%s' not found.".format(codec));
        }
    }
    
    /**
        Decodes a Base64 encoded string to a byte string.
    */
    String.prototype.decode_base64=function(){
         if((this.length % 4) == 0){
             if(typeof(atob) != "undefined"){//try using mozillas builtin codec
                 return atob(this);
             }else{
                 var nBits;
                 //create a result buffer, this is much faster than having strings concatinated.
                 var sDecoded = new Array(this.length /4);
                 var base64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
                 for(var i=0; i < this.length; i+=4){
                     nBits = (base64.indexOf(this.charAt(i))   & 0xff) << 18 |
                                (base64.indexOf(this.charAt(i+1)) & 0xff) << 12 |
                                (base64.indexOf(this.charAt(i+2)) & 0xff) <<  6 |
                                base64.indexOf(this.charAt(i+3)) & 0xff;
                    sDecoded[i] = String.fromCharCode((nBits & 0xff0000) >> 16, (nBits & 0xff00) >> 8, nBits & 0xff);
                }
                //make sure padding chars are left out.
                sDecoded[sDecoded.length-1] = sDecoded[sDecoded.length-1].substring(0, 3 - ((this.charCodeAt(i - 2) == 61) ? 2 : (this.charCodeAt(i - 1) == 61 ? 1 : 0)));
                return sDecoded.join("");
             }
         }else{
             throw new Exception(thisMod, "String length must be divisible by 4.");
         }
    }
    
    /**
        Encodes a string using Base64.
    */
    String.prototype.encode_base64=function(){
        if(typeof(btoa) != "undefined"){//try using mozillas builtin codec
            return btoa(this);
        }else{
            var base64 = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
                                'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z',
                                '0','1','2','3','4','5','6','7','8','9','+','/'];
            var sbin;
            var pad=0;
            var s="" + this;
            if((s.length % 3) == 1){
                s+=String.fromCharCode(0);
                s+=String.fromCharCode(0);
                pad=2;
            }else if((s.length % 3) == 2){
                s+=String.fromCharCode(0);
                pad=1;
            }
            //create a result buffer, this is much faster than having strings concatinated.
            var rslt=new Array(s.length / 3);
            var ri=0;
            for(var i=0;i<s.length; i+=3){
                sbin=((s.charCodeAt(i) & 0xff) << 16) | ((s.charCodeAt(i+1) & 0xff ) << 8) | (s.charCodeAt(i+2) & 0xff);    
                rslt[ri] = (base64[(sbin >> 18) & 0x3f] + base64[(sbin >> 12) & 0x3f] + base64[(sbin >>6) & 0x3f] + base64[sbin & 0x3f]);
                ri++;
            }
            if(pad>0){
                rslt[rslt.length-1] = rslt[rslt.length-1].substr(0, 4-pad) +  ((pad==2) ? "==" : (pad==1) ? "=" : "");
            }
            return rslt.join("");
        }
    }
    /**
        Decodes a URI using decodeURI.
    */
    String.prototype.decode_uri=function(){
        return decodeURI(this);
    }
    
    /**
        Encodes a URI using encodeURI.
    */
    String.prototype.encode_uri=function(){
        return encodeURI(this);
    }
})
 

/**    
    Evaluates a script in a scope with only global objects.
    @param [0]  The source of the module.
*/
evalScript=function(){
    eval(arguments[0]);
}


//let jsolait do some startup initialization
jsolait.init();
