/**
 * jdata
 */
(function () {
  var window = this, undefined,
  jdata = window.jdata = function (/* dataSelector, createIfNotExists */) {
    var dataSelector = (arguments.length >= 1 ? arguments[0] : '');
    var createIfNotExists = (arguments.length >= 2 ? arguments[1] : false);
    return new jdata.fn.init (dataSelector, createIfNotExists);
  };

  jdata.fn = jdata.prototype = {
    version:  "0.1", // version of this module
    length:   null,  // initially no elements
    selector: null,  // data selector being used
    data:     null,  // used only as a cache for data selected

    init: function (dataSelector /*, createIfNotExists? = false */)
    {
      var createIfNotExists = (arguments.length >= 2 ? arguments[1] : false);

      // wrong selector?
      if (dataSelector == null || !(typeof dataSelector === 'string')) {
        this.length   = 0;
        this.selector = null;
        this.data     = null;
        return this;
      }

      this.selector = jdata.getSelector (dataSelector, createIfNotExists);
      this.data     = jdata.get (this.selector);
      this.length   = this._getDataLength();

      return this;
    },

    // returns the number of elements of the current selection
  	size: function() {
  		return this.length;
  	},

    // returns a subselect
    filter: function (subselect /*, createIfNotExists */) {
      var createIfNotExists = (arguments.length >= 2 ? arguments[1] : false);
      var fpath = this.fullpath();
      return jdata (fpath + ((fpath != '' && subselect != '') ? "." : "") + subselect, createIfNotExists);
    },

    // if no arguments specified, returns the parent jdata selector
    // if the argument is a number, means the number of parents to go back
    // if the argument is a string stops when no more parents are found or the parent is equal
    parent : function (/*rewind*/) {
      var rewind = (arguments.length >= 1 ? arguments[0] : 1);
      var sel = this.selector;
      if (typeof rewind == 'number') {
        for (var i = 0; i < rewind && (sel.length != 0); i++) { sel.pop(); }
      }
      else { // string?
        while (sel.length != 0 && sel[sel.length - 1] != rewind) { name = sel.pop(); }
      }
      return jdata (jdata.fullpath (sel));
    },

    // returns the value of the subselect
    getval: function (subselect) {
      var fpath = this.fullpath();
      return jdata (fpath + ((fpath != '' && subselect != '') ? "." : "") + subselect).val();
    },

    // get or set data
    val: function() {
      if (arguments.length != 0) {
        // set new data, and if successful, then update this.data and this.length
        if (jdata.set (this.selector, arguments[0])) {
          this.data   = arguments[0];
          this.length = this._getDataLength();
          return this;
        }
      }
      return this.data;
    },

    // calls 'val' and parses the value as a date, returning the property requested
    date : function (/*property*/) {
      var property = (arguments.length >= 1) ? arguments[0] : null;
      var parsed = jdata.parsedate (this.val());
      if (parsed == null)   { return ''; }
      if (property == null) { return parsed; }
      return parsed[property];
    },
    
    
    // sort elements by using given attribute name
    sortBy : function (element, ascending) {
      var comparator = (arguments.length >= 3) ? arguments[2] : 'natural';
      
      if (arguments.length == 1) {
        ascending = true;
      }
      else if (typeof (ascending) == 'string') {
        if (ascending.toLowerCase() == 'asc') { ascending = true; }
        else                                  { ascending = false; }
      }

      // decide which conversion function to use
      var convert = function (x) { return x; }
      if ((comparator == 'double') || (comparator == 'float')) {
        convert = function (x) { return parseFloat (x); }
      }
      else if ((comparator == 'numeric') || (comparator == 'int')) {
        convert = function (x) { return parseInt (x); }
      }
      // else => f(x) = x  ==> 'string'/'natural'/...

      if (this.data != null)
      {
        this.data.sort (function (a,b) {
          if (convert (a[element]) > convert(b[element]))
            return (ascending ? 1 : -1); // ascending or descending?
          else if (convert (a[element]) < convert (b[element]))
            return (ascending ? -1 : 1); 
          return 0;
        });
      }
      
      return this;
    },

    merge : function (dataToMerge) {
      if (dataToMerge == null) return this;

      var merged = this.data;
      for (var item in dataToMerge) { merged[item] = dataToMerge[item]; }

      if (jdata.set (this.selector, merged)) {
        this.data   = merged;
        this.length = this._getDataLength();
      }
      return this;
    },

    // clear current data
    destroy: function () {
      if (jdata.destroy (this.selector)) {
        this.selector = null;
        this.data     = null;
        this.length   = 0;
      }
      return null;
    },

    // execute callback on each element of the current selection
    each : function (callback)
    {
      if (this.data === null) {
        // do nothing
      }
      else if (typeof this.data == 'string') {
        callback.apply (this);
      }
      else {
        var fpath = this.fullpath();
        for (var i = 0; i < this.data.length; i++)
        {
          var element = this.data[i];

          if (typeof (element) == 'object' && 'uid' in element) {
            callback.apply (jdata (fpath + '[' + element['uid'] + ']'));
          }
          else {
            // this path is executed probably because a wrong selector
          }
        }
      }
      return this;
    },

    // if any given data is equal (case insensitive by default) to current data.
    // This function accepts either arrays, strings or numbers
    is: function (dataToCompare)
    {
      var icase = (arguments.length >= 2) ? arguments[1] : true; // ignore case?
      var data1 = (icase && typeof this.data == 'string') ? this.data.toLowerCase() : this.data;
      var data2 = null;

      var eq = false;
      switch (typeof dataToCompare) {
        case 'object':
          for (var i = 0; i < dataToCompare.length && !eq; i++) {
            data2 = (icase && (typeof dataToCompare[i] == 'string')) ? dataToCompare[i].toLowerCase() : dataToCompare[i];
            eq = (data1 == data2);
          }
          break;

        case 'string':
          data2 = (icase ? dataToCompare.toLowerCase() : dataToCompare);
          eq = (data1 == data2);
          break;

        case 'number':
         eq = (data1 == data2);
         break;
      }

      return eq;
    },

    // return the full path to current object
    fullpath : function (useListInsteadOfNumbers) {
      return jdata.fullpath (this.selector, useListInsteadOfNumbers);
    },

    // convert selected data to JPath notation
    toJPath : function (/* usefullpath = true, convertSubElements : true, useListInsteadOfNumbers = false */) {
      var usefullpath = (arguments.length >= 1) ? arguments[0] : true;
      var convertSubElements  = (arguments.length >= 2) ? arguments[1] : true;
      var useListInsteadOfNumbers = (arguments.length >= 3) ? arguments[2] : false;

      // check a special case where the current selected element
      // represents a scalar value, a leaf
      // E.g: jdata ('product.attribute')
      var t = typeof (this.data)
      if ((t == 'undefined') || this.data == null) { return {}; }
      else if (t != 'object') {
        var retval = {};
        retval [this.fullpath (useListInsteadOfNumbers)] = this.data;
        return retval;
      }

      // the normal case, where a whole list is represented
      var nspace = '';
      if (usefullpath) {
        nspace = this.fullpath (useListInsteadOfNumbers);
      }

      return jdata.mkunidim (this.data, nspace, convertSubElements);
    },

    toString : function () {
      var darray = this.toJPath (true, true);
      var str = '';
      for (k in darray) {
        str += k + ": " + darray[k] + "\n";
      }
      return str;
    },

    setToHTML : function (/* selector */) {
      var selector = arguments[0] || document;
      var data     = this.toJPath (true, false, true);

      for (var attr in data)
      {
        var field = $(selector).jdataFind (attr);

        if (field.length) {
          if (field.is (':checkbox')) {
            field.each (function () { 
              this.checked = (typeof (data[attr]) == 'boolean') ? data[attr] : parseInt (data[attr]);
            });
          }
          else if (typeof (data[attr]) == 'boolean') {
            field.val (data[attr] ? 1 : 0);
          }
          else {
            field.val (data[attr] || '');
          }
        }

        // update also all classes
        $(selector).find ('.' + jdata.escape (attr)).html (data[attr]);
      }
      return this;
    },

    clearHTML : function (/* selector */) {
      var selector = arguments[0] || document;
      var data     = this.toJPath (true, false, true);

      for (var attr in data)
      {
        var field = $(selector).jdataFind (attr);

        if (field.length) {
          if (field.is (':checkbox')) {
            field.each (function () { this.checked = false; });
          }
          else {
            field.val('');
          }
        }
      }
      return this;
    },

    // gets the values in the forms and assign to each attribute
    getFromHTML : function (/* selector, createIfNotExists */) {
      var selector          = arguments[0] || document;
      var createIfNotExists = (arguments.length >= 2 ? arguments[1] : false);
      var data              = this.toJPath (true, false, false);

      for (var attr in data)
      {
        var listattr = attr.replace (/\.[0-9]+\./g, '.list.');
        var field = $(selector).jdataFind (listattr);
        if (field.length != 0)
        {
          if (field.is (':checkbox')) {
            field.each (function () {
              jdata (attr).val (this.checked);
            });
          }
          else {
            var value = field.val ();
            jdata (attr).val (value);
          }
        }
      }

      // find all input elements whose selector extends current one
      if (createIfNotExists) {
        var elements = $(selector).find ('[name^="' + jdata.escape (this.fullpath (true)) + '"]');
        var spath = this.fullpath(false);

        elements.each (function () {
          if ($(this).is (':checkbox')) {
            var name  = jdata._syncpaths (jdata.unescape ($(this).attr ('name')), spath);
            if (name != null) {
              $(this).each (function () { jdata (name, true).val (this.checked); });
            }
          }
          else if ($(this).is(':input')) {
            var name  = jdata._syncpaths (jdata.unescape ($(this).attr ('name')), spath);
            if (name != null) { jdata (name, true).val ($(this).val()); }
          }
        });
      }
      return this;
    },

    save : function (url) {
      jdata.send (url, this.data, arguments[1] || {});
      return this;
    },

    _getDataLength : function () {
      var len = 0;
      if (this.data == null)                             { len = 0; }
      else if ((typeof this.data) == 'string')           { len = 1; }
      else if ((typeof this.data.length) != 'undefined') { len = this.data.length; }
      else                                               { for (var k in this.data) { len++; } }
      return len;
    }
  };

  // give some capabilities to init function
  jdata.fn.init.prototype = jdata.fn;
})();


// add static methods to jdata
jQuery.extend(
  window.jdata,
  {
    _gcontainer : {},

    // merge data
    merge : function (newdata) {
      for (var item in newdata) { jdata._gcontainer [item] = newdata[item]; }
    },

    // erase all jdata
    clear: function () { jdata.destroy([]); },

    // transforms a dot selector in array selector
    getSelector : function (dotSelector, createIfNotExists)
    {
      if (dotSelector == null || dotSelector == '') return [];

      var selector = [];
      var ptr      = jdata._gcontainer;
      var tokens   = jdata.tokenize (dotSelector);

      var i = 0;
      while (i < tokens.length)
      {
        var s = tokens[i].text;

        // check that ptr is not an object or array
        if ((typeof ptr == 'string') || (typeof ptr == 'number')) {
          selector = null;
          break;
        }

        // check if it's possible to iterate through data, and do so
        if (s in ptr)
        {
          selector[selector.length] = s;
          oldptr                    = ptr;
          ptr                       = ptr[s];
          i++;

          // has a query? (either '[uid]' or '[name=value]'
          if (i < tokens.length && tokens[i].token == '[')
          {
            // if trying to access to an attribute as array, and the creat
            if (typeof ptr.length == 'undefined' && createIfNotExists) {
              oldptr[s] = [];
              ptr       = oldptr[s];
            }

            i++; // skip '['

            var qname = null, qvalue = null;
            if ((typeof ptr) != 'object') { selector = null; break; }

            // get by uid? (e.g "query[3]" -> get the "3]" part)
            if (i+1 < tokens.length && tokens[i+1].token == ']') {
              qname  = 'uid';
              qvalue = tokens[i].text;
              i += 2; // skip number and ']'
            }
            // name = value ]
            else if (i+3 < tokens.length && tokens[i+1].token == '=' && tokens[i+3].token == ']') {
              qname  = tokens[i].text;
              qvalue = tokens[i+2].text;
              if (tokens[i+2].token == 'dquote' || tokens[i+2].token == 'squote') {
                qvalue = tokens[i+2].text.replace( /^\s*['"]+|['"]+\s*$/g, '');
              }
              i += 4; // skip name, =, value, ]
            }

            var found = false;
            if (qname != null && qvalue != null)
            {
              // iterate through the array
              for (var k = 0; k < ptr.length && !found; k++) {
                if (qname in ptr[k]) {
                  // check case-insensitive
                  if (ptr[k][qname] == qvalue || (typeof ptr[k][qname] == 'string' && ptr[k][qname].toLowerCase() == qvalue.toLowerCase())) {
                    selector[selector.length] = k;
                    ptr                       = ptr[k];
                    found                     = true;
                  }
                }
              }

              if (!found && createIfNotExists) {
                // create an item at the end
                s = ptr.length;

                selector[selector.length] = s;
                ptr[s]                    = {}; //{ 'uid' : 'create' };
                ptr[s][qname]             = qvalue;
                ptr                       = ptr[s];
                found = true; // make as if was found
              }
            }

            if (!found) { selector = null; break; }
          }
        }
        // create given path if does not exist
        else if (createIfNotExists && (tokens[i].token != '.') && (tokens[i].token != '[')) {
          ptr[s] = {}; // create the new element but do not change 'i'
        }
        else { // invalid
          selector = null;
          break;
        }

        // skip next dot (if any)
        if (i < tokens.length && tokens[i].token == '.') { i++; }
      }

      return selector;
    },

    // iterate through data and get desired element (arraySelector is an array with the paths)
    get : function (arraySelector)
    {
      if (arraySelector == null)     return null;
      if (arraySelector.length == 0) return jdata._gcontainer;

      var data = jdata._gcontainer;
      for (var i = 0; i < arraySelector.length; i++) {
        if (typeof data[arraySelector[i]] == 'undefined') { return null; }
        data = data[arraySelector[i]];
      }

      return data;
    },

    set : function (arraySelector, newValue)
    {
      if (arraySelector == null) return false;

      var success = false;
      var data    = jdata._gcontainer;
      for (var i = 0; i < arraySelector.length; i++)
      {
        if (typeof data[arraySelector[i]] == 'undefined') { return false; }

        if ((i+1) == arraySelector.length) {
          success = true;
          data[arraySelector[i]] = newValue;
        }
        else {
          data = data[arraySelector[i]];
        }
      }

      return success;
    },

    destroy : function (arraySelector)
    {
      if (arraySelector == null) return false;
      if (arraySelector.length == 0) { // destroy the whole container
        jdata._gcontainer = {};
        return true;
      }

      var success = false;
      var data    = jdata._gcontainer;
      for (var i = 0; i < arraySelector.length; i++)
      {
        if (typeof data[arraySelector[i]] == 'undefined') { return false; }

        if ((i+1) == arraySelector.length)
        {
          if ((typeof arraySelector[i]) == 'number') {
            // delete produces wrong behaviour in arrays, but splice works great
            data.splice(arraySelector[i], 1); // this works great
          }
          else {
            delete data[arraySelector[i]];
          }
          success = true;
          break;
        }
        else {
          data = data[arraySelector[i]];
        }
      }

      return success;
    },

    load : function (url, queryArgs /*, options */)
    {
      var options = $.extend ({
        filter       : null, // parameter for filtering data
        success      : null, // takes 1 argument: success (jsonRequestData)
        error        : null
      }, arguments[2] || {});

      $.ajax ({
        url      : url,
        type     : 'POST',
        cache    : false,
        data     : queryArgs,
        dataType : 'json',

        error    : function (XMLHttpRequest, textStatus, errorThrown) {
          if (options.error) options.error(); // custom error handler
          else if (typeof (AlertDialog) != 'undefined') AlertDialog ('Error', 'Error while receiving data from server<br/>Sorry for the inconvenience :(');
          else alert ('Error while receiving data from server\n\nSorry for the inconvenience :(');
        },

        success  : function (response, textStatus)
        {
          var vdata = {};
          if (options.filter != null) {
            if (options.filter in response) { vdata[options.filter] = response[options.filter]; }
            else                            { vdata = response; }
          }
          else {
            vdata = response;
          }

          jdata.merge (vdata);

          // custom handler to be called after initializing
          if (options.success)
            options.success(response);
        }
      });
    },

    send : function (url /*data, options */)
    {
      var queryArgs = arguments.length >= 2 ? arguments[1] : jdata._gcontainer;
      var options = $.extend ({
        success      : null, // takes 1 argument: success (jsonRequestData)
        error        : null,
        async        : true
      }, arguments[2] || {});

      if (queryArgs == null)
        queryArgs = jdata._gcontainer;

      $.ajax ({
        url      : url,
        type     : 'POST',
        cache    : false,
        data     : {'json' : jdata.toJSON (queryArgs)},
        dataType : 'json',
        async    : options.async,

        error    : function (XMLHttpRequest, textStatus, errorThrown) {
          if (options.error) options.error(); // custom error handler
          else if (typeof (AlertDialog) != 'undefined') AlertDialog ('Error', 'Error while receiving data from server<br/>Sorry for the inconvenience :(');
          else alert ('Error while receiving data from server\n\nSorry for the inconvenience :(');
        },

        success  : function (response, textStatus)
        {
          // custom handler to be called after initializing
          if (options.success)
            options.success(response);
        }
      });
    },

    /** parse given jtype date format and return an array with the data */
    parsedate : function (str)
    {
      if (typeof (str) != 'string') { return null; }

      var year, month, day, hour = '00', min = '00', sec = '00';
      var regex   = /^(\d+)[\s-\/\\]+([1-9]|0[1-9]|1[012])[\s-\/\\]+([1-9]|0[1-9]|[12][0-9]|3[01])(\s+([1-9]|[01]\d|2[0-4])\s*:\s*([1-9]|[0-5]\d)(\s*:\s*([1-9]|[0-5]\d))?)?$/;
      var matches = $.trim(str).match (regex);
      if (matches == null) return null;

      // year-month-day are matched for sure
      year  = matches[1];
      month = ((matches[2].length == 1) ? '0' : '') + matches[2];
      day   = ((matches[3].length == 1) ? '0' : '') + matches[3];

      // other possible matches are hours/min/seconds with leading zeroes
      if ((matches.length >= 7) && typeof (matches[6]) != 'undefined') {
        hour = ((matches[5].length == 1) ? '0' : '') + matches[5];
        min  = ((matches[6].length == 1) ? '0' : '') + matches[6];

        // has seconds?
        if ((matches.length == 9) && typeof (matches[8]) != 'undefined') {
          sec = ((matches[8].length == 1) ? '0' : '') + matches[8];
        }
      }

      return {
        // raw parsed data
        'year' : year, 'month' : month, 'day' : day,
        'hour' : hour, 'min'   : min,   'sec' : sec,

        // full date
        full:  year + '-' + month + '-' + day + ' ' + hour + ':' + min + ':' + sec,
        // date formatted
        ymd :  year + '-' + month + '-' + day,
        dmy :   day + '-' + month + '-' + year,
        mdy : month + '-' + day   + '-' + year,

        // time formatted
        hm  :  hour + ':' + min,
        hms :  hour + ':' + min + ':' + sec
      };
    },

    // escape a jpath string to jform notation (if second parameter is true, then numbers will be converted to 'list') 
    escape : function (str /*, useListInsteadOfNumbers */) {
      var useListInsteadOfNumbers = arguments.length >= 2 ? arguments[1] : false;
      if (useListInsteadOfNumbers) { str = jdata.fullpath (str.split ('.'), true); }
      return str.replace (/->/g, '.list.').replace (/_/g, '__').replace (/-/g, '_-').replace (/\./g, '-');
    },

    // unescapes a string from jform notation to jpath
    unescape : function (str) {
      return str.replace (/__/g, '_').replace (/_-/g, '-').replace (/-/g, '.');
    },

    // selector is an array that defines the route to path (e.g ['a', 'b', 'c'])
    fullpath : function (selector, useListInsteadOfNumbers)
    {
      if (selector == null) return '';

      var fullpath = '';
      for (var i = 0; i < selector.length; i++) {
        if (fullpath != '') fullpath += '.';

        // is number? or a string representing a number?
        if (
          useListInsteadOfNumbers && 
          (
            ((typeof selector[i]) == 'number') || 
            (/^[0-9]+$/.test (selector[i]))
          )
        ) 
          fullpath += "list";
        else
          fullpath += selector[i];
      }
      return fullpath;
    },

    // String Tokenizer:
    //   Public Domain code by Christopher Diggins
    //   http://www.cdiggins.com
    //   http://www.cdiggins.com/tokenizer.html
    //
    tokenize : function (str)
    {
      var re_line_comment = /\/\/.*\r/
      var re_full_comment = /\/\*(?:.|[\n\r])*?\*\//
      var re_ident = /[a-zA-Z_][a-zA-Z0-9_]*\b/
      var re_integer = /[+-]?\d+/
      var re_float = /[+-]?\d+(([.]\d+)*([eE][+-]?\d+))?/
      var re_doublequote = /["][^"]*["]/
      var re_singlequote = /['][^']*[']/
      var re_tab = /\t/
      var re_nl = /\r/
      var re_space = /\s/
      var re_symbol = /\S/
      var re_token = /\/\/.*\r|\/\*(?:.|\n|\r)*?\*\/|\w+\b|[+-]?\d+(([.]\d+)*([eE][+-]?\d+))?|["][^"]*["]|['][^']*[']|./g
      var tokens = str.match(re_token);
      var retval = [];
      for (i = 0; i < tokens.length; i++)
      {
        var token = tokens[i];

        // skip spaces
        if (token.match(re_space) || token.match(re_nl) || token.match(re_tab)) {
          // skip
        }
        else if (token.match(re_line_comment)) {
          // skip comments: retval[retval.length] = { 'token' : 'comment', 'text' : token};
        }
        else if (token.match(re_full_comment)) {
          // skip comments: retval[retval.length] = { 'token' : 'comment', 'text' : token};
        }
        else if (token.match(re_singlequote)) {
          retval[retval.length] = { 'token' : 'squote', 'text' : token};
        }
        else if (token.match(re_doublequote)) {
          retval[retval.length] = { 'token' : 'dquote', 'text' : token};
        }
        else if (token.match(re_ident)) {
          retval[retval.length] = { 'token' : 'id', 'text' : token};
        }
        else if (token.match(re_float)) {
          retval[retval.length] = { 'token' : 'float', 'text' : token};
        }
        else if (token.match(re_integer)) {
          retval[retval.length] = { 'token' : 'int', 'text' : token};
        }
        else {
          switch (token) {
            case ">": retval[retval.length] = { 'token' : '>',   'text' : token}; break;
            case "<": retval[retval.length] = { 'token' : '<',   'text' : token}; break;
            case "=": retval[retval.length] = { 'token' : '=',   'text' : token}; break;
            case "[": retval[retval.length] = { 'token' : '[',   'text' : token}; break;
            case "]": retval[retval.length] = { 'token' : ']',   'text' : token}; break;
            case "(": retval[retval.length] = { 'token' : '(',   'text' : token}; break;
            case ")": retval[retval.length] = { 'token' : ')',   'text' : token}; break;
            case "{": retval[retval.length] = { 'token' : '{',   'text' : token}; break;
            case "}": retval[retval.length] = { 'token' : '}',   'text' : token}; break;
            case ".": retval[retval.length] = { 'token' : '.',   'text' : token}; break;
            default:  retval[retval.length] = { 'token' : 'sym', 'text' : token}; break;
          }
        }
      }
      return retval;
    },

    // synchronize paths by replacing "list" elements of the path1 using path2 elements instead
    // Example:
    //   _syncpaths ('product.i18n.list.name', 'product.i18n.3') => 'product.i18n.3.name'
    //   _syncpaths ('product.i18n.list.name', 'product.i18n')   => null
    _syncpaths: function (path1, path2) {
       var s1 = path1.split ('.'), s2 = path2.split ('.'), ret = '';
       for (var i = 0; i < s1.length; i++) {
         if (ret != '') ret += '.';
         if (s1[i] == 'list') {
           if (i < s2.length)  { ret += s2[i]; }
           else                { return null; } // could not synchronize :(
         }
         else {
           ret += s1[i];
         }
       }
       return ret;
    },

    // Code From: http://code.google.com/p/jquery-json/
    // Places quotes around a string, inteligently.
    // If the string contains no control characters, no quote characters, and no
    // backslash characters, then we can safely slap some quotes around it.
    // Otherwise we must also replace the offending characters with safe escape
    // sequences.
    quoteString : function(str)
    {
      var escapeable = /["\\\x00-\x1f\x7f-\x9f]/g;
      // table of character substitutions
      var meta = { '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"', '\\': '\\\\'};
      if (escapeable.test(str))
      {
        return '"' + str.replace(escapeable, function (a) {
          var c = meta[a];
          if (typeof c === 'string') { return c; }
          c = a.charCodeAt();
          return '\\u00' + Math.floor(c / 16).toString(16) + (c % 16).toString(16);
        }) + '"';
      }
      return '"' + str + '"';
    },

    // Code From: http://code.google.com/p/jquery-json/
    // Convert object or data to JSON string
    toJSON : function(o)
    {
      var type = typeof(o);

      if (type == "undefined")                        { return "undefined"; }
      else if (type == "number" || type == "boolean") { return o + ""; }
      else if (o === null)                            { return "null"; }

      // Is it a string?
      if (type == "string") { return jdata.quoteString(o); }

      // Does it have a .toJSON function?
      else if (type == "object" && typeof o.toJSON == "function") {
        return o.toJSON();
      }
      else if (type != "function" && typeof(o.length) == "number")  // is array?
      {
        var ret = [];
        for (var i = 0; i < o.length; i++) {
          ret.push( jdata.toJSON(o[i]) );
        }
        return "[" + ret.join(", ") + "]";
      }

      // If it's a function, we have to warn somebody!
      if (type == "function") {
        throw new TypeError("Unable to convert object of type 'function' to json.");
      }

      // It's probably an object, then.
      var ret = [];
      for (var k in o) {
        var name;
        type = typeof(k);

        if (type == "number") { name = '"' + k + '"'; }
        else if (type == "string") { name = jdata.quoteString(k); }
        else continue;  //skip non-string or number keys

        var val = jdata.toJSON(o[k]);
        if (typeof(val) != "string") {
          // skip non-serializable values
          continue;
        }

        ret.push(name + ": " + val);
      }
      return "{" + ret.join(", ") + "}";
    },

    // make unidimensional arrays by using dot notation
    mkunidim : function (data /*, nspace, recursive */)
    {
      var nspace    = arguments[1] || '';
      var recursive = arguments[2] || false;
      var ndata = {};

      if (((typeof data) == 'object') && (data != null))
      {
        if (!data) ndata = null;
        if (typeof data.length == 'number')
        {
          if (recursive) {
            for (var i = 0; i < data.length; i++) {
              if ((typeof data[i] == 'object') && (data[i] != null)) {
                var normdata =  jdata.mkunidim  (data[i], nspace + ((nspace != '') ? '.' + i : ''), recursive);
                for (var item in normdata) {
                  ndata [item] = normdata[item];
                }
              }
              else {
                ndata[nspace + ((nspace != '') ? '.' + i : '')] = data[i];
              }
            }
          }
        }
        else {
          for (var k in data) {
            // merge normdata with ndata
            if (typeof data[k] == 'object' && (data[k] != null)) {
              var normdata = jdata.mkunidim (data[k], nspace + ((nspace != '') ? '.' : '') + k, recursive);
              for (var item in normdata) {
                ndata [item] = normdata[item];
              }
            }
            else {
              ndata[nspace + ((nspace != '') ? '.' : '')  + k] = data[k];
            }
          }
        }
      }
      else {
        ndata = data;
      }

      return ndata;
    }
  }
);


/** Find an element inside a form (using jformEscape to do so) */
jQuery.fn.jdataFind = function(selector) {
  var elements = $(this).find (selector);
  if (elements.length == 0) {
    elements = $(this).find ('[name="' + selector + '"], [name="' + jdata.escape (selector) + '"], [name="' + jdata.escape (selector, true) + '"]');
  }
  return elements;
}

