// This plugin wraps dojox.json.ref so we don't need dojo, requires jQuery (for map function)

/*
The "New" BSD License:
**********************

Copyright (c) 2005-2010, The Dojo Foundation
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

  * Redistributions of source code must retain the above copyright notice, this
    list of conditions and the following disclaimer.
  * Redistributions in binary form must reproduce the above copyright notice,
    this list of conditions and the following disclaimer in the documentation
    and/or other materials provided with the distribution.
  * Neither the name of the Dojo Foundation nor the names of its contributors
    may be used to endorse or promote products derived from this software
    without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

(function(){
  var dojo = {};
  var dojox = {};
  dojox.json = {};

  dojo.toJsonIndentStr = "\t";
  dojo._escapeString = function(/*String*/str){
   //summary:
   //    Adds escape sequences for non-visual characters, double quote and
   //    backslash and surrounds with double quotes to form a valid string
   //    literal.
   return ('"' + str.replace(/(["\\])/g, '\\$1') + '"').
     replace(/[\f]/g, "\\f").replace(/[\b]/g, "\\b").replace(/[\n]/g, "\\n").
     replace(/[\t]/g, "\\t").replace(/[\r]/g, "\\r"); // string
  }
  dojo.toJson = function(obj) {
    return JSON.stringify(obj);
  }
  dojo.map = jQuery.map;
  
  // Implement!
  // dojo.date.stamp.toISOString;
  // dojo.date.stamp.fromISOString;

  dojox.json.ref = {
   // summary:
   //    Adds advanced JSON {de}serialization capabilities to the base json library.
   //    This enhances the capabilities of dojo.toJson and dojo.fromJson,
   //    adding referencing support, date handling, and other extra format handling.
   //    On parsing, references are resolved. When references are made to
   //    ids/objects that have been loaded yet, the loader function will be set to
   //    _loadObject to denote a lazy loading (not loaded yet) object. 


   resolveJson: function(/*Object*/ root,/*Object?*/ args){
     // summary:
     //    Indexes and resolves references in the JSON object.
     // description:
     //    A JSON Schema object that can be used to advise the handling of the JSON (defining ids, date properties, urls, etc)
     //
     // root:
     //    The root object of the object graph to be processed
     // args:
     //    Object with additional arguments:
     //
     // The *index* parameter.
     //    This is the index object (map) to use to store an index of all the objects. 
     //    If you are using inter-message referencing, you must provide the same object for each call.
     // The *defaultId* parameter.
     //    This is the default id to use for the root object (if it doesn't define it's own id)
     //  The *idPrefix* parameter.
     //    This the prefix to use for the ids as they enter the index. This allows multiple tables 
     //    to use ids (that might otherwise collide) that enter the same global index. 
     //    idPrefix should be in the form "/Service/".  For example,
     //    if the idPrefix is "/Table/", and object is encountered {id:"4",...}, this would go in the
     //    index as "/Table/4".
     //  The *idAttribute* parameter.
     //    This indicates what property is the identity property. This defaults to "id"
     //  The *assignAbsoluteIds* parameter.
     //    This indicates that the resolveJson should assign absolute ids (__id) as the objects are being parsed.
     //  
     // The *schemas* parameter
     //    This provides a map of schemas, from which prototypes can be retrieved
     // The *loader* parameter
     //    This is a function that is called added to the reference objects that can't be resolved (lazy objects)
     // return:
     //    An object, the result of the processing
     args = args || {};
     var idAttribute = args.idAttribute || 'id';
     var refAttribute = this.refAttribute;
     var idAsRef = args.idAsRef;
     var prefix = args.idPrefix || ''; 
     var assignAbsoluteIds = args.assignAbsoluteIds;
     var index = args.index || {}; // create an index if one doesn't exist
     var timeStamps = args.timeStamps;
     var ref,reWalk=[];
     var pathResolveRegex = /^(.*\/)?(\w+:\/\/)|[^\/\.]+\/\.\.\/|^.*\/(\/)/;
     var addProp = this._addProp;
     var F = function(){};
     function walk(it, stop, defaultId, needsPrefix, schema, defaultObject){
       // this walks the new graph, resolving references and making other changes
       var i, update, val, id = idAttribute in it ? it[idAttribute] : defaultId;
       if(idAttribute in it || ((id !== undefined) && needsPrefix)){
         id = (prefix + id).replace(pathResolveRegex,'$2$3');
       }
       var target = defaultObject || it;
       if(id !== undefined){ // if there is an id available...
         if(assignAbsoluteIds){
           it.__id = id;
         }
         if(args.schemas && (!(it instanceof Array)) && // won't try on arrays to do prototypes, plus it messes with queries 
               (val = id.match(/^(.+\/)[^\.\[]*$/))){ // if it has a direct table id (no paths)
           schema = args.schemas[val[1]];
         } 
         // if the id already exists in the system, we should use the existing object, and just 
         // update it... as long as the object is compatible
         if(index[id] && ((it instanceof Array) == (index[id] instanceof Array))){ 
           target = index[id];
           delete target.$ref; // remove this artifact
           delete target._loadObject;
           update = true;
         }else{
           var proto = schema && schema.prototype; // and if has a prototype
           if(proto){
             // if the schema defines a prototype, that needs to be the prototype of the object
             F.prototype = proto;
             target = new F();
           }
         }
         index[id] = target; // add the prefix, set _id, and index it
         if(timeStamps){
           timeStamps[id] = args.time;
         }
       }
       while(schema){
         var properties = schema.properties;
         if(properties){
           for(i in it){
             var propertyDefinition = properties[i];
             if(propertyDefinition && propertyDefinition.format == 'date-time' && typeof it[i] == 'string'){
               it[i] = dojo.date.stamp.fromISOString(it[i]);
             }
           }
         }
         schema = schema["extends"];
       }
       var length = it.length;
       for(i in it){
         if(i==length){
           break;    
         }
         if(it.hasOwnProperty(i)){
           val=it[i];
           if((typeof val =='object') && val && !(val instanceof Date) && i != '__parent'){
             ref=val[refAttribute] || (idAsRef && val[idAttribute]);
             if(!ref || !val.__parent){
               if(it != reWalk){
                 val.__parent = target;
               }
             }
             if(ref){ // a reference was found
               // make sure it is a safe reference
               delete it[i];// remove the property so it doesn't resolve to itself in the case of id.propertyName lazy values
               var path = ref.toString().replace(/(#)([^\.\[])/,'$1.$2').match(/(^([^\[]*\/)?[^#\.\[]*)#?([\.\[].*)?/); // divide along the path
               if((ref = (path[1]=='$' || path[1]=='this' || path[1]=='') ? root : index[(prefix + path[1]).replace(pathResolveRegex,'$2$3')])){  // a $ indicates to start with the root, otherwise start with an id
                 // if there is a path, we will iterate through the path references
                 if(path[3]){
                   path[3].replace(/(\[([^\]]+)\])|(\.?([^\.\[]+))/g,function(t,a,b,c,d){
                     ref = ref && ref[b ? b.replace(/[\"\'\\]/,'') : d];
                   });
                 }
               }
               if(ref){
                 val = ref;
               }else{
                 // otherwise, no starting point was found (id not found), if stop is set, it does not exist, we have
                 // unloaded reference, if stop is not set, it may be in a part of the graph not walked yet,
                 // we will wait for the second loop
                 if(!stop){
                   var rewalking;
                   if(!rewalking){
                     reWalk.push(target); // we need to rewalk it to resolve references
                   }
                   rewalking = true; // we only want to add it once
                   val = walk(val, false, val[refAttribute], true, propertyDefinition);
                   // create a lazy loaded object
                   val._loadObject = args.loader;
                 }
               }
             }else{
               if(!stop){ // if we are in stop, that means we are in the second loop, and we only need to check this current one,
                 // further walking may lead down circular loops
                 val = walk(
                   val,
                   reWalk==it,
                   id === undefined ? undefined : addProp(id, i), // the default id to use
                   false,
                   propertyDefinition, 
                   // if we have an existing object child, we want to 
                   // maintain it's identity, so we pass it as the default object
                   target != it && typeof target[i] == 'object' && target[i] 
                 );
               }
             }
           }
           it[i] = val;
           if(target!=it && !target.__isDirty){// do updates if we are updating an existing object and it's not dirty        
             var old = target[i];
             target[i] = val; // only update if it changed
             if(update && val !== old && // see if it is different 
                 !target._loadObject && // no updates if we are just lazy loading
                 !(i.charAt(0) == '_' && i.charAt(1) == '_') && i != "$ref" &&  
                 !(val instanceof Date && old instanceof Date && val.getTime() == old.getTime()) && // make sure it isn't an identical date
                 !(typeof val == 'function' && typeof old == 'function' && val.toString() == old.toString()) && // make sure it isn't an indentical function
                 index.onUpdate){
               index.onUpdate(target,i,old,val); // call the listener for each update
             }
           }
         }
       }

       if(update && (idAttribute in it || target instanceof Array)){
         // this means we are updating with a full representation of the object, we need to remove deleted
         for(i in target){
           if(!target.__isDirty && target.hasOwnProperty(i) && !it.hasOwnProperty(i) && !(i.charAt(0) == '_' && i.charAt(1) == '_') && !(target instanceof Array && isNaN(i))){
             if(index.onUpdate && i != "_loadObject" && i != "_idAttr"){
               index.onUpdate(target,i,target[i],undefined); // call the listener for each update
             }
             delete target[i];
             while(target instanceof Array && target.length && target[target.length-1] === undefined){
               // shorten the target if necessary
               target.length--;
             }
           }
         }
       }else{
         if(index.onLoad){
           index.onLoad(target);
         }
       }
       return target;
     }
     if(root && typeof root == 'object'){
       root = walk(root,false,args.defaultId, true); // do the main walk through
       walk(reWalk,false); // re walk any parts that were not able to resolve references on the first round
     }
     return root;
   },


   fromJson: function(/*String*/ str,/*Object?*/ args){
   // summary:
   //    evaluates the passed string-form of a JSON object.
   //
   // str:
   //    a string literal of a JSON item, for instance:
   //      '{ "foo": [ "bar", 1, { "baz": "thud" } ] }'
   // args: See resolveJson
   //
   // return:
   //    An object, the result of the evaluation
     function ref(target){ // support call styles references as well
       var refObject = {};
       refObject[this.refAttribute] = target;
       return refObject;
     }
     try{
       var root = eval('(' + str + ')'); // do the eval
     }catch(e){
       throw new SyntaxError("Invalid JSON string: " + e.message + " parsing: "+ str);
     }   
     if(root){
       return this.resolveJson(root, args);
     }
     return root;
   },

   toJson: function(/*Object*/ it, /*Boolean?*/ prettyPrint, /*Object?*/ idPrefix, /*Object?*/ indexSubObjects){
     // summary:
     //    Create a JSON serialization of an object.
     //    This has support for referencing, including circular references, duplicate references, and out-of-message references
     //    id and path-based referencing is supported as well and is based on http://www.json.com/2007/10/19/json-referencing-proposal-and-library/.
     //
     // it:
     //    an object to be serialized.
     //
     // prettyPrint:
     //    if true, we indent objects and arrays to make the output prettier.
     //    The variable dojo.toJsonIndentStr is used as the indent string
     //    -- to use something other than the default (tab),
     //    change that variable before calling dojo.toJson().
     //
     // idPrefix: The prefix that has been used for the absolute ids
     //
     // return:
     //    a String representing the serialized version of the passed object.
     var useRefs = this._useRefs;
     var addProp = this._addProp;
     var refAttribute = this.refAttribute;
     idPrefix = idPrefix || ''; // the id prefix for this context
     var paths={};
     var generated = {};
     function serialize(it,path,_indentStr){
       if(typeof it == 'object' && it){
         var value;
         if(it instanceof Date){ // properly serialize dates
           return '"' + dojo.date.stamp.toISOString(it,{zulu:true}) + '"';
         }
         var id = it.__id;
         if(id){ // we found an identifiable object, we will just serialize a reference to it... unless it is the root
           if(path != '#' && ((useRefs && !id.match(/#/)) || paths[id])){
             var ref = id; 
             if(id.charAt(0)!='#'){
               if(it.__clientId == id){
                 ref = "cid:" + id;
               }else if(id.substring(0, idPrefix.length) == idPrefix){ // see if the reference is in the current context
                 // a reference with a prefix matching the current context, the prefix should be removed
                 ref = id.substring(idPrefix.length);
               }else{
                 // a reference to a different context, assume relative url based referencing
                 ref = id;
               }
             }
             var refObject = {};
             refObject[refAttribute] = ref;
             return serialize(refObject,'#');
           }
           path = id;
         }else{
           it.__id = path; // we will create path ids for other objects in case they are circular
           generated[path] = it;
         }
         paths[path] = it;// save it here so they can be deleted at the end
         _indentStr = _indentStr || "";
         var nextIndent = prettyPrint ? _indentStr + dojo.toJsonIndentStr : "";
         var newLine = prettyPrint ? "\n" : "";
         var sep = prettyPrint ? " " : "";

         if(it instanceof Array){
           var res = dojo.map(it, function(obj,i){
             var val = serialize(obj, addProp(path, i), nextIndent);
             if(typeof val != "string"){
               val = "undefined";
             }
             return newLine + nextIndent + val;
           });
           return "[" + res.join("," + sep) + newLine + _indentStr + "]";
         }

         var output = [];
         for(var i in it){
           if(it.hasOwnProperty(i)){
             var keyStr;
             if(typeof i == "number"){
               keyStr = '"' + i + '"';
             }else if(typeof i == "string" && (i.charAt(0) != '_' || i.charAt(1) != '_')){
               // we don't serialize our internal properties __id and __clientId
               keyStr = dojo._escapeString(i);
             }else{
               // skip non-string or number keys
               continue;
             }
             var val = serialize(it[i],addProp(path, i),nextIndent);
             if(typeof val != "string"){
               // skip non-serializable values
               continue;
             }
             output.push(newLine + nextIndent + keyStr + ":" + sep + val);
           }
         }
         return "{" + output.join("," + sep) + newLine + _indentStr + "}";
       }else if(typeof it == "function" && dojox.json.ref.serializeFunctions){
         return it.toString();
       }

       return dojo.toJson(it); // use the default serializer for primitives
     }
     var json = serialize(it,'#','');
     if(!indexSubObjects){
       for(var i in generated)  {// cleanup the temporary path-generated ids
         delete generated[i].__id;
       }
     }
     return json;
   },
   _addProp: function(id, prop){
     return id + (id.match(/#/) ? id.length == 1 ? '' : '.' : '#') + prop;
   },
   //  refAttribute: String
   //    This indicates what property is the reference property. This acts like the idAttribute
   //    except that this is used to indicate the current object is a reference or only partially 
   //    loaded. This defaults to "$ref". 
   refAttribute: "$ref",
   _useRefs: false,
   serializeFunctions: false
  }

  jQuery.fromJsonRef = function(str, args) { return dojox.json.ref.fromJson(str, args); };
  jQuery.toJsonRef = function(str, args) { return dojox.json.ref.toJson(str, args); };
})();
