/*
  Copyright (c) 2004 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 providing language services like tokenizing JavaScript code
    or converting JavaScript objects to and from JSON (see json.org).
    To customize JSON serialization of Objects just overwrite the _toJSON method in your class.
*/
Module("lang", "0.2.2", function(thisMod){

    Object.prototype.isin = function(){
        if(arguments.length == 1){
            var a = arguments[0];
        }else{
            var a=arguments;
        }
        for(var i=0;i<a.length;i++){
            if(a[i] == this){
                return true;
            }
        }
        return false;
    }
        
    var ISODate = function(d){
        if(/^(\d{4})(\d{2})(\d{2})T(\d{2}):(\d{2}):(\d{2})/.test(d)){
            return new Date(Date.UTC(RegExp.$1, RegExp.$2-1, RegExp.$3, RegExp.$4, RegExp.$5, RegExp.$6));
        }else{ //todo error message
            throw "Not an ISO date: " + d;
        }
    }
        
    thisMod.JSONParser=Class("JSONParser", function(thisClass, Super){
        thisClass.prototype.init=function(){
            this.libs = {};
            var sys = {"ISODate" : ISODate};
            this.addLib(sys, "sys", ["ISODate"]);
        }
        
        thisClass.prototype.addLib = function(obj, name, exports){
            if(exports == null){
                this.libs[name] = obj;
            }else{
                for(var i=0;i<exports.length;i++){
                    this.libs[name + "." + exports[i]] = obj[exports[i]];
                }
            }
        }
        
        var EmptyValue = {};
        var SeqSep = {};
        
        var parseValue = function(tkns, libs){
            var tkn = tkns.nextNonWS();
            switch(tkn[0]){
                case thisMod.tokens.STR:
                case thisMod.tokens.NUM:
                    return eval(tkn[1]);
                case thisMod.tokens.NAME:
                    return parseName(tkn[1]);
                case thisMod.tokens.OP:
                    switch(tkn[1]){
                        case "[":
                            return parseArray(tkns, libs);
                            break;
                        case "{":
                            return parseObj(tkns, libs);
                            break;
                        case "}": case "]":
                            return EmptyValue;
                        case ",":
                            return SeqSep;
                        default:
                            throw "expected '[' or '{' but found: '" + tkn[1] + "'";
                    }
            }
            return EmptyValue;
        }
        
        var parseArray = function(tkns, libs){
            var a = [];
            while(! tkns.finished()){
                var v = parseValue(tkns, libs);
                if(v == EmptyValue){
                    return a;
                }else{
                    a.push(v);
                    v = parseValue(tkns, libs);
                    if(v == EmptyValue){
                        return a;
                    }else if(v != SeqSep){
                        throw "',' expected but found: '" + v + "'";
                    }
                }
            }
            throw "']' expected";
        }
                     
        var parseObj = function(tkns, libs){
            var obj = {};
            var nme =""
            while(! tkns.finished()){
                var tkn = tkns.nextNonWS();
                if(tkn[0] == thisMod.tokens.STR){
                    var nme =  eval(tkn[1]);
                    tkn = tkns.nextNonWS();
                    if(tkn[1] == ":"){
                        var v = parseValue(tkns, libs);
                        if(v == SeqSep || v == EmptyValue){
                            throw "value expected";
                        }else{
                            obj[nme] = v;
                            v = parseValue(tkns, libs);
                            if(v == EmptyValue){
                                return transformObj(obj, libs);
                            }else if(v != SeqSep){
                                throw "',' expected";
                            }
                        }
                    }else{
                        throw "':' expected but found: '" + tkn[1] + "'";
                    }
                }else if(tkn[1] == "}"){
                    return transformObj(obj, libs);
                }else{
                    throw "String expected";
                }
            }
            throw "'}' expected."
        }
        
        var transformObj = function(obj, libs){
            var o2;
            if(obj.jsonclass != null){
                var clsName = obj.jsonclass[0];
                var params = obj.jsonclass[1]
                if(libs[clsName]){
                    o2 = libs[clsName].apply(this, params);
                    for(var nme in obj){
                        if(nme != "jsonclass"){
                            if(typeof obj[nme] != "function"){
                                o2[nme] = obj[nme];
                            }
                        }
                    }
                }else{
                    throw "jsonclass not found: " + clsName;
                }
            }else{
                o2 = obj;
            }
            return o2;
        }
        
        var parseName = function(name){
            switch(name){
                case "null":
                    return null;
                case "true":
                    return true;
                case "false":
                    return false;
                default:
                    throw "'null', 'true', 'false' expected but found: '" + name + "'";
            }
        }
        
        thisClass.prototype.jsonToObj = function(data){
            var t = new thisMod.Tokenizer(data);
            return parseValue(t, this.libs);
        }
                
        thisClass.prototype.objToJson=function(obj){
            if(obj == null){
                return "null";
            }else{
                return obj._toJSON();
            }
        }
    })
        
    thisMod.parser = new thisMod.JSONParser();
    
    /**
        Turns JSON code into JavaScript objects.
        @param src  The source as a String.
    */
    thisMod.jsonToObj=function(src){
        return thisMod.parser.jsonToObj(src);
    }
    
    /**
        Turns an object into JSON.
        This is the same as calling obj._toJSON();
        @param obj  The object to marshall.
    */
    thisMod.objToJson=function(obj){
        return thisMod.parser.objToJson(obj);
    }
    
    ///Token constants for the tokenizer.
    thisMod.tokens = {};
    thisMod.tokens.WSP = 0;
    thisMod.tokens.OP =1;
    thisMod.tokens.STR = 2;
    thisMod.tokens.NAME = 3;
    thisMod.tokens.NUM = 4;
    thisMod.tokens.ERR = 5;
    thisMod.tokens.NL = 6;
    thisMod.tokens.COMMENT = 7;
    thisMod.tokens.DOCCOMMENT = 8;
    thisMod.tokens.REGEXP = 9;
    
    
    /**
        Tokenizer Class which incrementally parses JavaScript code and returns the language tokens.
    */
    thisMod.Tokenizer=Class("Tokenizer", function(thisClass, Super){
        thisClass.prototype.init=function(s){
            this._working = s;
            this._pos = 0;
        }
        
        /**
            Returns weather or not the code was parsed.
            @return True if the complete code was parsed, false otherwise.
        */
        thisClass.prototype.finished=function(){
            return this._working.length == 0;
        }
        
        thisClass.prototype.nextNonWS = function(){
            var tkn = this.next();
            while(tkn[0] == thisMod.tokens.WSP){
                tkn = this.next();
            }
            return tkn;
        }
        
        /**
            Returns the next token.
            @return The next token.
        */
        thisClass.prototype.next = function(){
            if(this._working ==""){
                throw "Empty";
            } 
            var s1 = this._working.charAt(0);
            var s2 = s1 + this._working.charAt(1);
            var s3 = s2 + this._working.charAt(2);
            var rslt=[];
            switch(s1){
                case '"': case "'":
                    try{
                        s1 = extractQString(this._working);
                        rslt= [thisMod.tokens.STR, s1, this._pos];
                    }catch(e){
                        rslt= [thisMod.tokens.ERR, s1, this._pos, e];
                    }
                    break;
                case "\n": case "\r":
                    rslt =[thisMod.tokens.NL, s1, this._pos];
                    break;
                case "{": case "}": case "[": case "]": case "(": case ")":
                case ":": case ",": case ".": case ";":
                case "*": case "-": case "+":
                case "=": case "<": case ">":  case "!":   
                    switch(s2){
                        case "==": case "!=": case "<>":  case "<=": case ">=":
                            rslt = [thisMod.tokens.OP, s2, this._pos];
                            break;
                        default:
                            rslt = [thisMod.tokens.OP, s1, this._pos];
                    }
                    break;
                case "/":
                    if(s2 == "//" || s3 =="///"){
                        s1 = extractSLComment(this._working);
                        rslt = [s1.charAt(2) != "/" ? thisMod.tokens.COMMENT:thisMod.tokens.DOCCOMMENT, s1, this._pos];
                    }else if(s2 == "/*" || s3 =="/**"){
                        try{
                            s1 = extractMLComment(this._working);
                            rslt = [s3 !="/**" ? thisMod.tokens.COMMENT: thisMod.tokens.DOCCOMMENT, s1, this._pos];
                        }catch(e){
                            rslt= [thisMod.tokens.ERR, s3 != "/**" ? s2 : s3, this._pos, e];
                        }
                    }else{
                        rslt = [thisMod.tokens.OP, s1, this._pos, e];
                    }
                    break;
                default:
                    s1=this._working.match(/\d+\.\d+|\d+|\w+|\s+/)[0];
                    if(s1.replace(/\s+/g,"") == ""){ //whitespace
                        rslt = [thisMod.tokens.WSP, s1, this._pos];
                    }else if(/^\d|\d\.\d/.test(s1)){//number
                        rslt =  [thisMod.tokens.NUM, s1, this._pos];
                    }else{//name
                        rslt = [thisMod.tokens.NAME, s1, this._pos];
                    }
            }
            
            this._working=this._working.slice(rslt[1].length);
            this._pos += rslt[1].length;
            return rslt;
        }
        
        var searchQoute = function(s, q){
            if(q=="'"){
                return s.search(/[\\']/);
            }else{
                return s.search(/[\\"]/);
            }
        }
        
        var extractQString=function(s){
            if(s.charAt(0) == "'"){
                var q="'";
            }else{
                var q='"';
            }
            s=s.slice(1);
            var rs="";
            var p= searchQoute(s, q);
            while(p >= 0){
                if(p >=0){
                    if(s.charAt(p) == q){
                        rs += s.slice(0, p+1);
                        s = s.slice(p+1);
                        return q + rs;
                    }else{
                        rs+=s.slice(0, p+2);
                        s = s.slice(p+2);
                    }
                }
                p = searchQoute(s, q);
            }
            throw "End of String expected.";
        }
        
        var extractSLComment=function(s){
            var p = s.search(/\n/);
            if(p>=0){
                return s.slice(0,p+1);
            }else{
                return s;
            }
        }
        
        var extractMLComment=function(s){
            var p = s.search(/\*\//);
            if(p>=0){
                return s.slice(0,p+2);
            }else{
                throw "End of comment expected.";
            }
        }
    })
    
    /**
        Converts an object to JSON.
    */
    Object.prototype._toJSON = function(){
        var v=[];
        for(attr in this){
            if(typeof this[attr] != "function"){
                v.push('"' + attr + '": ' + thisMod.objToJson(this[attr]));
            }
        }
        return "{" + v.join(", ") + "}";
    }
    
    /**
        Converts a String to JSON.
    */
    String.prototype._toJSON = function(){
        var s = '"' + this.replace(/(["\\])/g, '\\$1') + '"';
        s = s.replace(/(\n)/g,"\\n");
        return s;
    }
    
    /**
        Converts a Number to JSON.
    */
    Number.prototype._toJSON = function(){
        return this.toString();
    }
    
    /**
        Converts a Boolean to JSON.
    */
    Boolean.prototype._toJSON = function(){
        return this.toString();
    }
    
    /**
        Converts a Date to JSON.
        Date representation is not defined in JSON.
    */
    Date.prototype._toJSON= function(){
        var padd=function(s, p){
            s=p+s
            return s.substring(s.length - p.length)
        }
        var y = padd(this.getUTCFullYear(), "0000");
        var m = padd(this.getUTCMonth() + 1, "00");
        var d = padd(this.getUTCDate(), "00");
        var h = padd(this.getUTCHours(), "00");
        var min = padd(this.getUTCMinutes(), "00");
        var s = padd(this.getUTCSeconds(), "00");
        
        var isodate = y +  m  + d + "T" + h +  ":" + min + ":" + s
        
        return '{"jsonclass":["sys.ISODate", ["' + isodate + '"]]}';
    }
    
    /**
        Converts an Array to JSON.
    */
    Array.prototype._toJSON = function(){
        var v = [];
        for(var i=0;i<this.length;i++){
            v.push(thisMod.objToJson(this[i])) ;
        }
        return "[" + v.join(", ") + "]";
    }
})

