/*  Afrous JavaScript - Core
 *  version 3.0
 *  (c) 2008 Shinichi Tomita (stomita@mashmatrix.com)
 *
 *  Afrous Core is freely distributable under the terms of an MIT-style license.
 *  For details, see the web site: http://www.afrous.com
 *
 *--------------------------------------------------------------------------*/

(function(afrous, $eval) {

afrous.Version = '3.0';

/* ---------------------------------------------------------------------- */

var JSON = {};

/**
 * Modified to accept 'undefined' value in the property
 * stomita@mashmatrix.com
 */
/*
    http://www.JSON.org/json2.js
    2008-10-31

    Public Domain.

    NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.

    See http://www.JSON.org/js.html

    This file creates a global JSON object containing two methods: stringify
    and parse.

        JSON.stringify(value, replacer, space)
            value       any JavaScript value, usually an object or array.

            replacer    an optional parameter that determines how object
                        values are stringified for objects. It can be a
                        function or an array of strings.

            space       an optional parameter that specifies the indentation
                        of nested structures. If it is omitted, the text will
                        be packed without extra whitespace. If it is a number,
                        it will specify the number of spaces to indent at each
                        level. If it is a string (such as '\t' or '&nbsp;'),
                        it contains the characters used to indent at each level.

            This method produces a JSON text from a JavaScript value.

            When an object value is found, if the object contains a toJSON
            method, its toJSON method will be called and the result will be
            stringified. A toJSON method does not serialize: it returns the
            value represented by the name/value pair that should be serialized,
            or undefined if nothing should be serialized. The toJSON method
            will be passed the key associated with the value, and this will be
            bound to the object holding the key.

            For example, this would serialize Dates as ISO strings.

                Date.prototype.toJSON = function (key) {
                    function f(n) {
                        // Format integers to have at least two digits.
                        return n < 10 ? '0' + n : n;
                    }

                    return this.getUTCFullYear()   + '-' +
                         f(this.getUTCMonth() + 1) + '-' +
                         f(this.getUTCDate())      + 'T' +
                         f(this.getUTCHours())     + ':' +
                         f(this.getUTCMinutes())   + ':' +
                         f(this.getUTCSeconds())   + 'Z';
                };

            You can provide an optional replacer method. It will be passed the
            key and value of each member, with this bound to the containing
            object. The value that is returned from your method will be
            serialized. If your method returns undefined, then the member will
            be excluded from the serialization.

            If the replacer parameter is an array of strings, then it will be
            used to select the members to be serialized. It filters the results
            such that only members with keys listed in the replacer array are
            stringified.

            Values that do not have JSON representations, such as undefined or
            functions, will not be serialized. Such values in objects will be
            dropped; in arrays they will be replaced with null. You can use
            a replacer function to replace those with JSON values.
            JSON.stringify(undefined) returns undefined.

            The optional space parameter produces a stringification of the
            value that is filled with line breaks and indentation to make it
            easier to read.

            If the space parameter is a non-empty string, then that string will
            be used for indentation. If the space parameter is a number, then
            the indentation will be that many spaces.

            Example:

            text = JSON.stringify(['e', {pluribus: 'unum'}]);
            // text is '["e",{"pluribus":"unum"}]'


            text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
            // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'

            text = JSON.stringify([new Date()], function (key, value) {
                return this[key] instanceof Date ?
                    'Date(' + this[key] + ')' : value;
            });
            // text is '["Date(---current time---)"]'


        JSON.parse(text, reviver)
            This method parses a JSON text to produce an object or array.
            It can throw a SyntaxError exception.

            The optional reviver parameter is a function that can filter and
            transform the results. It receives each of the keys and values,
            and its return value is used instead of the original value.
            If it returns what it received, then the structure is not modified.
            If it returns undefined then the member is deleted.

            Example:

            // Parse the text. Values that look like ISO date strings will
            // be converted to Date objects.

            myData = JSON.parse(text, function (key, value) {
                var a;
                if (typeof value === 'string') {
                    a =
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
                    if (a) {
                        return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
                            +a[5], +a[6]));
                    }
                }
                return value;
            });

            myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
                var d;
                if (typeof value === 'string' &&
                        value.slice(0, 5) === 'Date(' &&
                        value.slice(-1) === ')') {
                    d = new Date(value.slice(5, -1));
                    if (d) {
                        return d;
                    }
                }
                return value;
            });


    This is a reference implementation. You are free to copy, modify, or
    redistribute.

    This code should be minified before deployment.
    See http://javascript.crockford.com/jsmin.html

    USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
    NOT CONTROL.
*/

/*jslint evil: true */

/*global JSON */

/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", call,
    charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, getUTCMinutes,
    getUTCMonth, getUTCSeconds, hasOwnProperty, join, lastIndex, length,
    parse, propertyIsEnumerable, prototype, push, replace, slice, stringify,
    test, toJSON, toString, valueOf
*/

// Create a JSON object only if one does not already exist. We create the
// methods in a closure to avoid creating global variables.

if (!this.JSON) {
    JSON = {};
}
(function () {

    function f(n) {
        // Format integers to have at least two digits.
        return n < 10 ? '0' + n : n;
    }

    if (typeof Date.prototype.toJSON !== 'function') {

        Date.prototype.toJSON = function (key) {

            return this.getUTCFullYear()   + '-' +
                 f(this.getUTCMonth() + 1) + '-' +
                 f(this.getUTCDate())      + 'T' +
                 f(this.getUTCHours())     + ':' +
                 f(this.getUTCMinutes())   + ':' +
                 f(this.getUTCSeconds())   + 'Z';
        };

        String.prototype.toJSON =
        Number.prototype.toJSON =
        Boolean.prototype.toJSON = function (key) {
            return this.valueOf();
        };
    }

    var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
        escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
        gap,
        indent,
        meta = {    // table of character substitutions
            '\b': '\\b',
            '\t': '\\t',
            '\n': '\\n',
            '\f': '\\f',
            '\r': '\\r',
            '"' : '\\"',
            '\\': '\\\\'
        },
        rep;


    function quote(string) {

// 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.

        escapable.lastIndex = 0;
        return escapable.test(string) ?
            '"' + string.replace(escapable, function (a) {
                var c = meta[a];
                if (typeof c === 'string') {
                    return c;
                }
                return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
            }) + '"' :
            '"' + string + '"';
    }


    function str(key, holder) {

// Produce a string from holder[key].

        var i,          // The loop counter.
            k,          // The member key.
            v,          // The member value.
            length,
            mind = gap,
            partial,
            value = holder[key];

// If the value has a toJSON method, call it to obtain a replacement value.

        if (value && typeof value === 'object' &&
                typeof value.toJSON === 'function') {
            value = value.toJSON(key);
        }

// If we were called with a replacer function, then call the replacer to
// obtain a replacement value.

        if (typeof rep === 'function') {
            value = rep.call(holder, key, value);
        }

// What happens next depends on the value's type.

        switch (typeof value) {
        case 'string':
            return quote(value);

        case 'number':

// JSON numbers must be finite. Encode non-finite numbers as null.

            return isFinite(value) ? String(value) : 'null';

        case 'boolean':
        case 'null':

// If the value is a boolean or null, convert it to a string. Note:
// typeof null does not produce 'null'. The case is included here in
// the remote chance that this gets fixed someday.

            return String(value);

// If the type is 'object', we might be dealing with an object or an array or
// null.

        case 'object':

// Due to a specification blunder in ECMAScript, typeof null is 'object',
// so watch out for that case.

            if (!value) {
                return 'null';
            }

// Make an array to hold the partial results of stringifying this object value.

            gap += indent;
            partial = [];

// If the object has a dontEnum length property, we'll treat it as an array.

            if (typeof value.length === 'number' &&
                    !value.propertyIsEnumerable('length')) {

// The object is an array. Stringify every element. Use null as a placeholder
// for non-JSON values.

                length = value.length;
                for (i = 0; i < length; i += 1) {
                    partial[i] = str(i, value) || 'null';
                }

// Join all of the elements together, separated with commas, and wrap them in
// brackets.

                v = partial.length === 0 ? '[]' :
                    gap ? '[\n' + gap +
                            partial.join(',\n' + gap) + '\n' +
                                mind + ']' :
                          '[' + partial.join(',') + ']';
                gap = mind;
                return v;
            }

// If the replacer is an array, use it to select the members to be stringified.

            if (rep && typeof rep === 'object') {
                length = rep.length;
                for (i = 0; i < length; i += 1) {
                    k = rep[i];
                    if (typeof k === 'string') {
                        v = str(k, value);
                        if (v) {
                            partial.push(quote(k) + (gap ? ': ' : ':') + v);
                        }
                    }
                }
            } else {

// Otherwise, iterate through all of the keys in the object.

                for (k in value) {
                    if (Object.hasOwnProperty.call(value, k)) {
                        v = str(k, value);
                        if (v) {
                            partial.push(quote(k) + (gap ? ': ' : ':') + v);
                        }
                    }
                }
            }

// Join all of the member texts together, separated with commas,
// and wrap them in braces.

            v = partial.length === 0 ? '{}' :
                gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' +
                        mind + '}' : '{' + partial.join(',') + '}';
            gap = mind;
            return v;
        }
    }

// If the JSON object does not yet have a stringify method, give it one.

    if (typeof JSON.stringify !== 'function') {
        JSON.stringify = function (value, replacer, space) {

// The stringify method takes a value and an optional replacer, and an optional
// space parameter, and returns a JSON text. The replacer can be a function
// that can replace values, or an array of strings that will select the keys.
// A default replacer method can be provided. Use of the space parameter can
// produce text that is more easily readable.

            var i;
            gap = '';
            indent = '';

// If the space parameter is a number, make an indent string containing that
// many spaces.

            if (typeof space === 'number') {
                for (i = 0; i < space; i += 1) {
                    indent += ' ';
                }

// If the space parameter is a string, it will be used as the indent string.

            } else if (typeof space === 'string') {
                indent = space;
            }

// If there is a replacer, it must be a function or an array.
// Otherwise, throw an error.

            rep = replacer;
            if (replacer && typeof replacer !== 'function' &&
                    (typeof replacer !== 'object' ||
                     typeof replacer.length !== 'number')) {
                throw new Error('JSON.stringify');
            }

// Make a fake root object containing our value under the key of ''.
// Return the result of stringifying the value.

            return str('', {'': value});
        };
    }


// If the JSON object does not yet have a parse method, give it one.

    if (typeof JSON.parse !== 'function') {
        JSON.parse = function (text, reviver) {

// The parse method takes a text and an optional reviver function, and returns
// a JavaScript value if the text is a valid JSON text.

            var j;

            function walk(holder, key) {

// The walk method is used to recursively walk the resulting structure so
// that modifications can be made.

                var k, v, value = holder[key];
                if (value && typeof value === 'object') {
                    for (k in value) {
                        if (Object.hasOwnProperty.call(value, k)) {
                            v = walk(value, k);
                            if (v !== undefined) {
                                value[k] = v;
                            } else {
                                delete value[k];
                            }
                        }
                    }
                }
                return reviver.call(holder, key, value);
            }


// Parsing happens in four stages. In the first stage, we replace certain
// Unicode characters with escape sequences. JavaScript handles many characters
// incorrectly, either silently deleting them, or treating them as line endings.

            cx.lastIndex = 0;
            if (cx.test(text)) {
                text = text.replace(cx, function (a) {
                    return '\\u' +
                        ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
                });
            }

// In the second stage, we run the text against regular expressions that look
// for non-JSON patterns. We are especially concerned with '()' and 'new'
// because they can cause invocation, and '=' because it can cause mutation.
// But just to be safe, we want to reject all unexpected forms.

// We split the second stage into 4 regexp operations in order to work around
// crippling inefficiencies in IE's and Safari's regexp engines. First we
// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
// replace all simple value tokens with ']' characters. Third, we delete all
// open brackets that follow a colon or comma or that begin the text. Finally,
// we look to see that the remaining characters are only whitespace or ']' or
// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.

            if (/^[\],:{}\s]*$/.
test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').
replace(/"[^"\\\n\r]*"|true|false|null|undefined|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {

// In the third stage we use the eval function to compile the text into a
// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
// in JavaScript: it can begin a block or an object literal. We wrap the text
// in parens to eliminate the ambiguity.

                j = $eval('(' + text + ')');

// In the optional fourth stage, we recursively walk the new structure, passing
// each name/value pair to a reviver function for possible transformation.

                return typeof reviver === 'function' ?
                    walk({'': j}, '') : j;
            }

// If the text is not JSON parseable, then a SyntaxError is thrown.

            throw new SyntaxError('JSON.parse');
        };
    }
})();

/* ---------------------------------------------------------------------- */

/**
 * Basic utility functions widely used in the library.
 */
var lng = afrous.lang = {

  extend : function(dest, src) {
    for (var prop in src) {
      dest[prop] = src[prop];
    }
    return dest;
  }
  ,

  chain : function(obj, proto) {
    var f = function(){};
    f.prototype = proto;
    return lng.extend(new f(), obj);
  }
  ,

  defineClass : function(props, superClass) {
    var f = function() {
      this.initialize.apply(this, arguments);
    }
    if (superClass) {
      var subClass = function() {}
      subClass.prototype = superClass.prototype;
      f.prototype = new subClass;
    }
    lng.extend(f.prototype, props);
    return f;
  }
  ,

  keys : function(obj) {
    var keys = [];
    for (var prop in obj) {
      if (!lng.isFunction(obj[prop])) keys.push(prop);
    }
    return keys;
  }
  ,

  values : function(obj) {
    var values = [];
    for (var prop in obj) {
      if (!lng.isFunction(obj[prop])) values.push(obj[prop]);
    }
    return values;
  }
  ,

  forEach : function(arr, callback, thisObj) {
    for (var i=0,len=arr.length; i<len; i++) {
      callback.call(thisObj, arr[i], i, arr);
    }
  }
  ,

  map : function(arr, callback, thisObj) {
    for (var i=0,ret=[],len=arr.length; i<len; i++) {
      ret.push(callback.call(thisObj, arr[i], i, arr));
    }
    return ret;
  }
  ,

  filter : function(arr, callback, thisObj) {
    for (var i=0,ret=[],len=arr.length; i<len; i++) {
      if (callback.call(thisObj, arr[i], i, arr)) ret.push(arr[i]);
    }
    return ret;
  }
  ,

  find : function(arr, callback, thisObj) {
    for (var i=0,len=arr.length; i<len; i++) {
      if (callback.call(thisObj, arr[i], i, arr)) return arr[i];
    }
  }
  ,

  indexOf : function(arr, value) {
    for (var i=0,len=arr.length; i<len; i++) {
      if (arr[i]==value) return i;
    }
    return -1;
  }
  ,

  scan : function(obj, func, thisObj) {
    function _scan(obj) {
      var res = func.call(thisObj, obj);
      if (typeof res != 'undefined') return res;
      if (lng.isPrintable(obj)) {
        return obj;
      } else if (lng.isArray(obj)) {
        return lng.map(obj, _scan);
      } else {
        res = {};
        for (var key in obj) res[key] = _scan(obj[key]);
        return res;
      }
    }

    return _scan(obj);
  }
  ,

  bind : function(func, thisObj) {
    return function() { return func.apply(thisObj, arguments) }
  }
  ,

  delay : function(func, msec) {
    return function() {
      var _this = this;
      var arg = arguments;
      window.setTimeout(function() {
        func.apply(_this, arg);
      }, msec);
    }
  }
  ,

  poll : function(params) {
    var work = params.work;
    var scope = params.scope;
    var callback = params.callback || lng.emptyFunc;
    var errback = params.errback || callback;
    var timeout = params.timeout;
    var interval = params.interval || 1000;
    var startTime = new Date().getTime();

    var PID = window.setInterval(function() {
      if (work.call(scope)) {
        window.clearInterval(PID);
        callback.call(scope);
      } else if (timeout && startTime+timeout < new Date().getTime()) {
        cancel();
      }
    }, interval);

    function cancel() {
      if (PID) {
        window.clearInterval(PID);
        PID = null;
        errback.call(scope);
      }
    }

    return cancel;
  }
  ,

  isNullOrUndefined : function(a) {
    return (a === null || typeof a == 'undefined');
  }
  ,

  isString : function(a) {
    return (typeof a == "string" || a instanceof String);
  }
  ,

  isNumber : function(a) {
    return (typeof a == "number" || a instanceof Number);
  }
  ,

  isObject : function(a) {
    if (typeof a == 'undefined') return false;
    return (typeof a == 'object' || a === null || lng.isArray(a));
  }
  ,

  isArray : function(a) {
    return a && a instanceof Array;
  }
  ,

  isFunction : function(a) {
    return typeof a == 'function' && a instanceof Function && a != '[object NodeList]'
  }
  ,

  isArrayLike : function(a) {
    return lng.isArray(a) || a && !lng.isString(a) && isFinite(a.length)
  }
  ,

  isDelayed : function(a) {
    return a instanceof afrous.async.DelayedObject;
  }
  ,

  isIterator : function(a) {
    return a instanceof afrous.async.iterator.Iterator;
  }
  ,

  isDOMNode : function(a) {
    return a && a.childNodes && lng.isArrayLike(a.childNodes)
  }
  ,

  isPrintable : function(a) {
    var t = typeof(a);
    return (t == 'string' || t == 'number' || t == 'boolean' || t == 'undefined' || a === null ||
            a instanceof String || a instanceof Number || a instanceof Boolean ||
            a instanceof Date); // || afrous.lang.isDOMNode(a) );
  }
  ,

  isSerializable : function(a) {
    return lng.isPrintable(a) || lng.isObject(a);
  }
  ,

  isEmpty : function(a) {
    return typeof a == 'undefined' || a === null || a === '';
  }
  ,

  toString : function(a) {
    if (a===null) {
      return 'null';
    } else if (typeof(a)=='undefined') {
      return 'undefined';
    } else if (lng.isDOMNode(a)) {
      if (a.tagName) {
        return '<'+a.tagName.toLowerCase()+'>';
      } else {
        return a.nodeName;
      }
    } else {
      return (a).toString();
    }
  }
  ,

  toJSON : function(obj) {
    // using json2.js
    return JSON.stringify(obj);
  }
  ,

  parseJSON : function(json) {
    // using json2.js
    return JSON.parse(json);
  }
  ,

  cast : function(type, obj) {
    var isArray = /\[\]$/.test(type);
    type = type.replace(/\[\]$/,'');

    if (isArray && lng.isIterator(obj)) {
      return new afrous.async.iterator.MapIterator(obj, function(el, i, iter, cb) {
        cb.onSuccess(lng.cast(type, el));
      });
    }

    if (lng.isDelayed(obj)) {
      if (type.toLowerCase()=='object') {
        return obj;
      } else {
        return new afrous.async.DelayedObject({
          evaluate : function(callback) {
            obj.evaluate(callback.extend(function(v) {
              callback.onSuccess(lng.cast(type, v));
            }));
          }
        });
      }
    }

    if (isArray && !lng.isArray(obj)) {
      obj = obj===null || typeof obj == 'undefined' ? [] :
            obj && obj.split ? obj.split(/\s*,\s*/) :
            [ obj ];
    }

    var arr = isArray ? obj : [ obj ];
    arr = lng.map(arr, function(o) {
      try {
        otype = typeof(o);
        ostr = lng.toString(o);
        switch (type.toLowerCase()) {
          case 'integer' :
            return lng.isNullOrUndefined(o) ? o : /^\-?\d+$/.test(ostr) ? parseInt(ostr, 10) : NaN;
          case 'float' :
            return lng.isNullOrUndefined(o) ? o : /^\-?\d*(\.\d+)?([e|E]\d+)?$/.test(ostr) ? parseFloat(ostr, 10) : NaN;
          case 'boolean' :
            return !(!o || ostr.toLowerCase()=="false" || ostr=="0");
          case 'date' :
            return afrous.string.parseDate(ostr);
          case 'string' :
            return o == null || otype == 'undefined' ? o : ostr;
          case 'domnode' :
            return document.createTextNode(ostr);
          case 'object' :
          default :
            return o;
        }
      } catch (e) {
        return;
      }
    })
    return isArray ? arr : arr[0];
  }
  ,

  loadScript : function(scriptUrl, callback) {
    var script = document.createElement('script');
    script.type = 'text/javascript';
    script.charset = 'utf-8';
    script.src = scriptUrl;
    if (typeof callback == 'function') {
      script.onload = callback;
      if (document.all) {
        script.onreadystatechange = function() {
          if (script.readyState == 'complete' || script.readyState == 'loaded') {
            script.onreadystatechange = null;
            script.onload();
          }
        }
      }
    }
    afdom.scriptContainerElem.appendChild(script);
  }
  ,

  require : function(namespaces, callback) {
    callback = afasync.responder(callback);
    namespaces = lng.isArray(namespaces) ? namespaces : [ namespaces ];
    afasync.iterate(
      lng.filter(
        lng.map(namespaces, function(ns) { return ns.split('.') }),
        function(ns) {
          ns = ns.slice();
          return (function(ctx, ct) {
            return ns.length==0 ? false :
                   ctx && (ct = ctx[ns.shift()]) ? arguments.callee(ct) :
                   true;
          })(window);
        }
      ),
      function(ns, i, namespaces, cb) {
        var url = lng.scriptBaseUrl + '/' + ns.join('-') + '.js';
        lng.loadScript(url, cb.onSuccess);
      },
      callback
    );
  }
  ,

  scriptBaseUrl : (function() {
    var scripts = document.getElementsByTagName('script');
    // find script src of this script; must be the last blocking script
    for (var i=scripts.length-1, s; i>=0; i--) {
      s = scripts[i];
      if (s.defer) continue; // ignore defered script element
      else break;
    }
    var scriptUrl = s && s.src && s.src.split('#')[0];
    scriptUrl = scriptUrl && (
      /^https?:/.test(scriptUrl) ? scriptUrl :
      scriptUrl.indexOf('/')==0 ? location.href.split('/').slice(0, 3).join('/') + scriptUrl :
      location.href.split('#')[0].split('/').slice(0, -1).join('/') + '/' + scriptUrl);
    return scriptUrl && scriptUrl.split('/').slice(0, -1).join('/');
  })()
  ,

  emptyFunc : function() {},

  isIE : !!window.execScript,
  isFirefox : !!window.Components,
  isWebkit : !!window.defaultstatus,
  isOpera : !!window.opera
};

/* ---------------------------------------------------------------------- */

/**
 * afrous.dom
 */
var afdom = afrous.dom = {

  scriptContainerElem : (function() {
    return (document.getElementsByTagName('head')[0] || document.body);
  })()
  ,

  createElement : function(config, doc) {
    doc = doc || document;
    config = config || {}
    var el = doc.createElement(config.tagName || 'div');
    lng.forEach(
      lng.keys(config),
      function(key) {
        if (key=='tagName') return;
        if (key=='className') el.className = config[key];
        else el.setAttribute(key, config[key]);
      }
    )
    return el;
  }
  ,

  getElementsByClassName : function(el, className, tagName) {
    var regexp = new RegExp("(^|\\s)" + className + "(\\s|$)");
    return lng.filter(el.getElementsByTagName(tagName || '*'), function(cn) {
      return regexp.test(cn.className);
    })
  }
  ,

  addClass : function(el, className) {
    var regexp = new RegExp('(^|\\s)'+className+'(\\s|$)');
    if (!regexp.test(el.className||'')) {
      el.className = (el.className||'') + ' ' + className;
      el.className = el.className.replace(/^\s+|\s+$/g, '');
    }
  }
  ,

  removeClass : function(el, className) {
    var regexp = new RegExp('(^|\\s)'+className+'(\\s|$)', 'g');
    el.className = (el.className||'').replace(regexp, ' ').replace(/^\s+|\s+$/g, '');
  }
  ,

  toHTML : function(el) {
    if (el.parentNode) el = el.cloneNode(true);
    var ptag = 'div';
    if (el.tagName=='td' || el.tagName=='th') ptag = 'tr';
    else if (el.tagName=='tr') ptag = 'tbody';
    else if (el.tagName=='tbody' || el.tagName=='thead') ptag = 'table';
    else if (el.tagName=='li') ptag = 'ul';
    else if (el.tagName=='option') ptag = 'select';
    var p = afdom.createElement({ tagName : ptag });
    p.appendChild(el);
    return p.innerHTML;
    return 1;
  }
  ,

  toObject : function(el, depth, ignoreAttrs) {
    //var ignoreAttrs = /^(contentEditable|on.+)$/;
    if (el.nodeType==9) {// document
      var obj = {};
      obj[el.documentElement.tagName] = afdom.toObject(el.documentElement, depth);
      return obj;
    } else if (el.nodeType==3) { // text node
      return el.nodeValue.replace(/^\s+|\s+$/g, ''); //trim space
    } else if (el.nodeType==1) {
      var obj = {};
      obj.tagName = el.tagName;
      lng.forEach(el.attributes, function(attr) {
        if (attr.nodeValue && (!ignoreAttrs || !ignoreAttrs.test(attr.nodeName))) {
          obj['@'+attr.nodeName] = attr.nodeValue;
        }
      })
      if (typeof depth=='undefined' || depth>1) {
        lng.forEach(el.childNodes, function(cn) {
          var cobj = afdom.toObject(cn, depth && depth-1);
          if (!cobj) return; // blank text node is not needed.
          var nodeName = cn.nodeName;
          var val = obj[nodeName];
          if (val) {
            obj[nodeName] = (lng.isArray(val) ? val : [ val ]).concat(cobj);
          } else {
            obj[nodeName] = cobj;
          }
        })
      }
      return obj;
    }
  }
  ,

  parseHTML : function(html) {

    function tagRegExp(tagname) {
      return new RegExp("^\\s*<"+tagname+"[\\s>].*<\/"+tagname+">\\s*$", "i");
    }

    html = html.replace(/[\r\n]/g, ' ');
    var div = afdom.createElement();
    if (tagRegExp("th").test(html) || tagRegExp("td").test(html)) {
      div.innerHTML = '<table><tbody><tr>'+html+'</tr></tbody></table>';
      return div.getElementsByTagName('tr')[0].firstChild;
    } else if (tagRegExp("tr").test(html)) {
      div.innerHTML = '<table><tbody>'+html+'</tbody></table>';
      return div.getElementsByTagName('tbody')[0].firstChild;
    } else if (tagRegExp("tbody").test(html) || tagRegExp("thead").test(html)) {
      div.innerHTML = '<table>'+html+'</table>';
      return div.getElementsByTagName('table')[0].firstChild;
    } else if (tagRegExp("li").test(html)) {
      div.innerHTML = '<ul>'+html+'</ul>';
      return div.getElementsByTagName('ul')[0].firstChild;
    } else if (tagRegExp("option").test(html)) {
      div.innerHTML = '<select>'+html+'</select>';
      return div.getElementsByTagName('select')[0].firstChild;
    } else {
      div.innerHTML = html;
      return div.firstChild;
    }
  }
  ,

  /**
   * @obsolete
   */
  writeValue : function(/*HTMLDomElement*/el, /*Object*/value) {
    if (typeof value=='undefined' || value === null) {
      // nothing
    } else if (lng.isDOMNode(value)) {
      if (value.ownerDocument === el.ownerDocument) {
        if (value.parentNode) {
          value = value.cloneNode(true);
        }
        el.appendChild(value);
      } else {
        el.innerHTML = afdom.toHTML(value);
      }
    } else if (value instanceof Renderer) {
      value.render(el);
    } else if (lng.isArrayLike(value)) {
      lng.forEach(value, function(v) {
        afdom.writeValue(el, v);
      });
    } else {
      el.appendChild(document.createTextNode(lng.toString(value)));
    }
  }
  ,

  /**
   *
   */
  addEvent : (function() {
    return window.addEventListener ?
           function(element, type, func) {
             element.addEventListener(type, func, false);
           } :
           function(element, type, func) {
             element.attachEvent('on' + type, func);
           };
  })()
  ,

  /**
   *
   */
  removeEvent : (function() {
    return window.removeEventListener ?
    function(element, evt, func) {
      element.removeEventListener(evt, func, false);
    } :
    function(element, evt, func) {
      element.detachEvent('on' + evt, func);
    };
  })()

};

/* ---------------------------------------------------------------------- */

/**
 * afrous.string
 */
var afstr = afrous.string = {

  safeHTMLTags : (function() {
    var REGEXPS = {
      CLASS : /^[-_a-zA-Z0-9]+$/,
      ID : /^[a-zA-Z][a-zA-Z0-9_:.-]*$/,
      COLOR : /^([a-z]{3,20}|#[a-fA-F0-9]{3}|#[a-fA-F0-9]{6}|rgb\s*\(\s*[0-9]{3}\s*,\s*[0-9]{3}\s*,\s*[0-9]{3}\s*\))$/,
      SIZE : /^([0-9]+(px|em)?|([1-9]?[0-9]|100)%)$/,
      SIZE_INLINE : /^([0-9]+|([1-9]?[0-9]|100)%)$/,
      BORDER : /^(((dashed|dotted|double|groove|hidden|inset|none|outset|ridge|solid|[a-z]{3,20}|#[a-fA-F0-9]{3}|#[a-fA-F0-9]{6}|rgb\s*\(\s*[0-9]{3}\s*,\s*[0-9]{3}\s*,\s*[0-9]{3}\s*\)|[0-9]+px)\s*){1,3})$/,
      BORDER_STYLE : /^(dashed|dotted|double|groove|hidden|inset|none|outset|ridge|solid)$/,
      DISPLAY : /^(block|compact|inline|list-item|marker|none)$/,
      OVERFLOW : /^(auto|hidden|scroll|visible)$/,
      WHITE_SPACE : /^(normal|nowrap|pre)$/,
      HREF : /^((https?|ftp|callto|mailto|data):|\.?\/)/,
      ALIGN : /^(center|justy|left|right)$/,
      VALIGN : /^(top|middle|bottom)$/,
      TEXT_DECORATION : /^(line-through|underline)$/,
      MARGIN : /^(([0-9]+(px|em)|0|([1-9]?[0-9]|100)%|auto)\s*)+$/,
      PADDING : /^(([0-9]+(px|em)|0)\s*)+$/,
      TARGET : /^(_self|_blank)$/,
      ALL : /^.+$/,
      NUMERIC : /^[0-9]+$/
    };

    return {
      a : {
        "class" : REGEXPS.CLASS,
        href : REGEXPS.HREF,
        id : REGEXPS.ID,
        name : REGEXPS.CLASS,
        target : REGEXPS.TARGET,
        title : REGEXPS.ALL,
        type : REGEXPS.ALL
      },
      b : {
        "class" : REGEXPS.CLASS,
        style : {
          'background-color' : REGEXPS.COLOR,
          'color' : REGEXPS.COLOR
        }
      },
      blockquote : {
        "class" : REGEXPS.CLASS,
        style : {
          'margin' : REGEXPS.MARGIN,
          'margin-bottom' : REGEXPS.SIZE,
          'margin-left' : REGEXPS.SIZE,
          'margin-right' : REGEXPS.SIZE,
          'margin-top' : REGEXPS.SIZE
        }
      },
      br : {},
      center : {
        "class" : REGEXPS.CLASS
      },
      code : {
        "class" : REGEXPS.CLASS,
        style : {
          'border' : REGEXPS.BORDER,
          'border-style' : REGEXPS.BORDER_STYLE,
          'border-width' : REGEXPS.SIZE,
          'display' : REGEXPS.DISPLAY,
          'margin' : REGEXPS.MARGIN,
          'margin-bottom' : REGEXPS.SIZE,
          'margin-left' : REGEXPS.SIZE,
          'margin-right' : REGEXPS.SIZE,
          'margin-top' : REGEXPS.SIZE,
          'padding' : REGEXPS.PADDING,
          'padding-bottom' : REGEXPS.SIZE,
          'padding-left' : REGEXPS.SIZE,
          'padding-right' : REGEXPS.SIZE,
          'padding-top' : REGEXPS.SIZE,
          'overflow' : REGEXPS.OVERFLOW,
          'white-space' : REGEXPS.WHITE_SPACE
        }
      },
      dd : {
        "class" : REGEXPS.CLASS
      },
      div : {
        align : /^(center|justy|left|right)$/,
        "class" : REGEXPS.CLASS,
        style : {
          'background-color' : REGEXPS.COLOR,
          'border' : REGEXPS.BORDER,
          'border-style' : REGEXPS.BORDER_STYLE,
          'border-width' : REGEXPS.SIZE,
          'color' : REGEXPS.COLOR,
          'display' : REGEXPS.DISPLAY,
          'font-style' : /^italic$/,
          'font-weight' : /^bold$/,
          'margin' : REGEXPS.MARGIN,
          'margin-bottom' : REGEXPS.SIZE,
          'margin-left' : REGEXPS.SIZE,
          'margin-right' : REGEXPS.SIZE,
          'margin-top' : REGEXPS.SIZE,
          'padding' : REGEXPS.PADDING,
          'padding-bottom' : REGEXPS.SIZE,
          'padding-left' : REGEXPS.SIZE,
          'padding-right' : REGEXPS.SIZE,
          'padding-top' : REGEXPS.SIZE,
          'overflow' : REGEXPS.WHITE_SPACE,
          'text-align' : REGEXPS.ALIGN,
          'text-decoration' : REGEXPS.TEXT_DECORATION,
          'white-space' : REGEXPS.WHITE_SPACE
        }
      },
      dl : {
        "class" : REGEXPS.CLASS
      },
      dt : {
        "class" : REGEXPS.CLASS
      },
      em : {
        "class" : REGEXPS.CLASS,
        style : {
          'background-color' : REGEXPS.COLOR,
          'color' : REGEXPS.COLOR
        }
      },
      font : {
        "class" : REGEXPS.CLASS,
        color : /^((#[a-fA-F0-9]{3}|#[a-fA-F0-9]{6}))$/,
        size : /^[1-9]$/,
        style : {
          'background-color' : REGEXPS.COLOR,
          'color' : REGEXPS.COLOR
        }
      },
      h1 : {
        "class" : REGEXPS.CLASS
      },
      h2 : {
        "class" : REGEXPS.CLASS
      },
      h3 : {
        "class" : REGEXPS.CLASS
      },
      h4 : {
        "class" : REGEXPS.CLASS
      },
      h5 : {
        "class" : REGEXPS.CLASS
      },
      h6 : {
        "class" : REGEXPS.CLASS
      },
      hr : {
        align : REGEXPS.ALIGN,
        "class" : REGEXPS.CLASS,
        noshade : /^noshade$/,
        size : /^[1-5]$/,
        width : REGEXPS.SIZE_INLINE
      },
      i : {
        "class" : REGEXPS.CLASS,
        style : {
          'background-color' : REGEXPS.COLOR,
          'color' : REGEXPS.COLOR
        }
      },
      img : {
        align : /^(absbottom|absmiddle|bottom|left|middle|right|texttop|top)$/,
        alt : REGEXPS.ALL,
        border : /^[0-9]$/,
        "class" : REGEXPS.CLASS,
        height : REGEXPS.SIZE_INLINE,
        hspace : /^[0-9]$/,
        id : REGEXPS.ID,
        src : REGEXPS.HREF,
        title : REGEXPS.ALL,
        vspace : /^[0-9]$/,
        width : REGEXPS.SIZE_INLINE
      },
      li : {
        "class" : REGEXPS.CLASS,
        style : {
          'background-color' : REGEXPS.COLOR,
          'color' : REGEXPS.COLOR,
          'text-align' : REGEXPS.ALIGN
        }
      },
      ol : {
        "class" : REGEXPS.CLASS,
        start : REGEXPS.NUMERIC,
        style : {
          'text-align' : REGEXPS.ALIGN
        },
        type : /^[aA1iI]+$/
      },
      p : {
        align : REGEXPS.ALIGN,
        "class" : REGEXPS.CLASS,
        style : {
          'background-color' : REGEXPS.COLOR,
          'color' : REGEXPS.COLOR,
          'font-style' : /^italic$/,
          'font-weight' : /^bold$/,
          'text-align' : REGEXPS.ALIGN,
          'text-decoration' : REGEXPS.TEXT_DECORATION
        }
      },
      pre : {
        "class" : REGEXPS.CLASS
      },
      span : {
        "class" : REGEXPS.CLASS,
        style : {
          'background-color' : REGEXPS.COLOR,
          'color' : REGEXPS.COLOR,
          'font-style' : /^italic$/,
          'font-weight' : /^bold$/,
          'text-align' : REGEXPS.ALIGN,
          'text-decoration' : REGEXPS.TEXT_DECORATION
        }
      },
      strike : {
        "class" : REGEXPS.CLASS,
        style : {
          'background-color' : REGEXPS.COLOR,
          'color' : REGEXPS.COLOR
        }
      },
      strong : {
        "class" : REGEXPS.CLASS,
        style : {
          'background-color' : REGEXPS.COLOR,
          'color' : REGEXPS.COLOR
        }
      },
      sub : {
        "class" : REGEXPS.CLASS,
        style : {
          'background-color' : REGEXPS.COLOR,
          'color' : REGEXPS.COLOR
        }
      },
      sup : {
        "class" : REGEXPS.CLASS,
        style : {
          'background-color' : REGEXPS.COLOR,
          'color' : REGEXPS.COLOR
        }
      },
      table : {
        border : REGEXPS.NUMERIC,
        cellpadding : REGEXPS.NUMERIC,
        cellspacing : REGEXPS.NUMERIC,
        "class" : REGEXPS.CLASS,
        height : REGEXPS.SIZE_INLINE,
        style :{
          'background-color' : REGEXPS.COLOR,
          'border' : REGEXPS.BORDER,
          'border-color' : REGEXPS.COLOR,
          'border-width' : REGEXPS.SIZE,
          'height' : REGEXPS.SIZE,
          'width' : REGEXPS.SIZE,
          'margin' : REGEXPS.MARGIN,
          'margin-bottom' : REGEXPS.SIZE,
          'margin-left' : REGEXPS.SIZE,
          'margin-right' : REGEXPS.SIZE,
          'margin-top' : REGEXPS.SIZE
        },
        summary : REGEXPS.ALL,
        width : REGEXPS.SIZE_INLINE,
        align : REGEXPS.ALIGN
      },
      tbody : {
        align : REGEXPS.ALIGN,
        "class" : REGEXPS.CLASS,
        valign : REGEXPS.VALIGN
      },
      td : {
        align : REGEXPS.ALIGN,
        "class" : REGEXPS.CLASS,
        colspan : REGEXPS.NUMERIC,
        rowspan : REGEXPS.NUMERIC,
        style : {
          'background-color' : REGEXPS.COLOR,
          'border' : REGEXPS.BORDER,
          'border-color' : REGEXPS.COLOR,
          'border-width' : REGEXPS.SIZE,
          'height' : REGEXPS.SIZE,
          'width' : REGEXPS.SIZE,
          'padding' : REGEXPS.SIZE
        },
        valign : REGEXPS.VALIGN,
        width : REGEXPS.SIZE_INLINE
      },
      tfoot : {
        align : REGEXPS.ALIGN,
        "class" : REGEXPS.CLASS,
        valign : REGEXPS.VALIGN
      },
      th : {
        align  : REGEXPS.ALIGN,
        "class" : REGEXPS.CLASS,
        colspan : REGEXPS.NUMERIC,
        rowspan : REGEXPS.NUMERIC,
        style : {
          'background-color' : REGEXPS.COLOR,
          'border' : REGEXPS.BORDER,
          'border-color' : REGEXPS.COLOR,
          'border-width' : REGEXPS.SIZE
        },
      valign : REGEXPS.VALIGN,
        width : REGEXPS.SIZE_INLINE
      },
      thead : {
        align : REGEXPS.ALIGN,
        "class" : REGEXPS.CLASS,
        valign : REGEXPS.VALIGN
      },
      tr : {
        align : REGEXPS.ALIGN,
        "class" : REGEXPS.CLASS,
        style : {
          'background-color' : REGEXPS.COLOR,
          'border' : REGEXPS.BORDER,
          'border-color' : REGEXPS.COLOR,
          'border-width' : REGEXPS.SIZE
        },
        valign : REGEXPS.VALIGN
      },
      u : {
        "class" : REGEXPS.CLASS,
        style : {
          'background-color' : REGEXPS.COLOR,
          'color' : REGEXPS.COLOR
        }
      },
      ul : {
        "class" : REGEXPS.CLASS,
        style : {
          'text-align' : REGEXPS.ALIGN
        }
      },

      // Additional tags allowed

      iframe : {
        allowtransparency : /^(true|false)$/,
        "class" : REGEXPS.CLASS,
        height : REGEXPS.SIZE_INLINE,
        border : REGEXPS.NUMERIC,
        bordercolor : REGEXPS.COLOR,
        frameborder : REGEXPS.NUMERIC,
        scrolling : /^(yes|no|on|off|true|false|auto)$/,
        src : REGEXPS.HREF,
        hspace : REGEXPS.NUMERIC,
        vspace : REGEXPS.NUMERIC,
        title : REGEXPS.ALL,
        width : REGEXPS.SIZE_INLINE
      },
      input : {
        "class" : REGEXPS.CLASS,
        type : /^(text|password|file|checkbox|radio|hidden|submit|button|image)$/,
        maxlength : REGEXPS.NUMERIC,
        size : REGEXPS.NUMERIC,
        checked : /^(checked|true)?$/,
        readonly : /^(readonly|true)?$/,
        src : REGEXPS.HREF,
        height : REGEXPS.SIZE_INLINE,
        width : REGEXPS.SIZE_INLINE,
        name : REGEXPS.ALL,
        value : REGEXPS.ALL,
        style : {
          "height" : REGEXPS.SIZE,
          "width" : REGEXPS.SIZE
        }
      },
      textarea : {
        "class" : REGEXPS.CLASS,
        cols : REGEXPS.NUMERIC,
        rows : REGEXPS.NUMERIC,
        disabled : /^(disabled|true)?$/,
        readonly : /^(readonly|true)?$/,
        src : REGEXPS.HREF,
        name : REGEXPS.ALL,
        style : {
          "height" : REGEXPS.SIZE,
          "width" : REGEXPS.SIZE
        }
      },
      select : {
        multiple : /^(multiple|true)?$/,
        size : REGEXPS.NUMERIC,
        name : REGEXPS.ALL,
        value : REGEXPS.ALL
      },
      option : {
        "class" : REGEXPS.CLASS,
        disabled : /^(disabled|true)?$/,
        selected : /^(selected|true)?$/,
        name : REGEXPS.ALL,
        value : REGEXPS.ALL
      },
      button : {
        "class" : REGEXPS.CLASS,
        disabled : /^(disabled|true)?$/,
        type : /^(button|submit|reset)$/,
        name : REGEXPS.ALL,
        value : REGEXPS.ALL
      },
      form : {
        "class" : REGEXPS.CLASS,
        method : /^(post|get)$/i,
        target : REGEXPS.TARGET,
        action : REGEXPS.HREF,
        autocomplete : /^(on|off)$/
      }
    }

  })()
  ,

  filterUnsafeHTML : function(htmlstr) {
    var safeTags = this.safeHTMLTags;
    var div = window.document.createElement('div');
    div.innerHTML = htmlstr || '';
    (function(elem) {
      var safeAttrs = safeTags[elem.tagName.toLowerCase()];

      var attributes = elem.attributes;
      var deletingAttrs = [];
      for (var i=0; i<attributes.length; i++) {
        var attr = attributes[i];
        var attrname = attr.nodeName.toLowerCase();
        var condition = safeAttrs[attrname];
        if (attrname=='style' && condition) {
          var styles = [];
          var stylesSplit = (attr.nodeValue || '').split(/\s*;\s*/);
          for (var j=0, len=stylesSplit.length; j<len; j++) {
            var stylePair = stylesSplit[j].split(/\s*:\s*/);
            if (stylePair.length>1) {
              var name = stylePair[0];
              var value = stylePair[1];
              if (condition[name] && condition[name].test(value)) {
                styles.push(name +' : '+ value);
              }
            }
          }
          elem.setAttribute('style', styles.join(';'));
        } else {
          if (!condition || !condition.test(attr.nodeValue)) {
            deletingAttrs.push(attr);
          }
        }
      }
      lng.forEach(deletingAttrs, function(attr) { elem.removeAttributeNode(attr) });

      var childNodes = elem.childNodes;
      var deletingCNodes = [];
      for (var i=0; i<childNodes.length; i++) {
        var cn = childNodes[i];
        if (cn.nodeType==1) {
          if (!safeTags[cn.tagName.toLowerCase()]) {
            deletingCNodes.push(cn);
            continue;
          }
          arguments.callee(cn);
        }
      }
      lng.forEach(deletingCNodes, function(cn) { elem.removeChild(cn) });

    })(div);
    return div.innerHTML;
  }
  ,

  escapeHTML : function(str) {
    return (lng.isEmpty(str) ?  '' :
            lng.toString(str).replace(/&/g, '&amp;').replace(/</g, '&lt;')
                             .replace(/>/g, '&gt;').replace(/"/g, '&quot;'));
  }
  ,

  parseDate : function(str) {
    if (typeof str=='undefined' || str === null || str == '') return;

    var d = new Date(str);
    if (!isNaN(d.getTime())) return d;

    d = new Date(+str);
    if (!isNaN(d.getTime())) return d;

    var W3CDTF = /^([\d]{4})-([\d]{2})-([\d]{2})(T([\d]{2}):([\d]{2}):([\d]{2})(.([\d]{3}))?(Z|([\+\-])([\d]{2}):([\d]{2})))?$/;
    var m = str.match(W3CDTF);
    if (m) {
      d = new Date(0);
      if (!m[4]) {
        d.setFullYear(parseInt(m[1], 10));
        d.setDate(parseInt(m[3], 10));
        d.setMonth(parseInt(m[2], 10) - 1);
        d.setHours(0);
        d.setMinutes(0);
        d.setSeconds(0);
        d.setMilliseconds(0);
      } else {
        d.setUTCFullYear(parseInt(m[1], 10));
        d.setUTCDate(parseInt(m[3], 10));
        d.setUTCMonth(parseInt(m[2], 10) - 1);
        d.setUTCHours(parseInt(m[5], 10));
        d.setUTCMinutes(parseInt(m[6], 10));
        d.setUTCSeconds(parseInt(m[7], 10));
        d.setUTCMilliseconds(parseInt(m[9] || '0', 10));
        if (m[10] && m[10]!='Z') {
          var offset = parseInt(m[12],10) * 60 + parseInt(m[13], 10);
          d.setTime((m[11]=='+' ? -1 : 1) * offset * 60 * 1000 +d.getTime());
        }
      }
      return d;
    }

  }
  ,

  formatDate : function(date) {
    var yyyy = date.getFullYear();
    var mm = zeropad(date.getMonth()+1);
    var dd = zeropad(date.getDate());
    return yyyy+'-'+mm+'-'+dd;

    function zeropad(v) { return v<10 ? '0'+v : ''+v }
  }
  ,

  formatDateTime : function(date) {
    var yyyy = date.getUTCFullYear();
    var mm = zeropad(date.getUTCMonth()+1);
    var dd = zeropad(date.getUTCDate());
    var hh = zeropad(date.getUTCHours());
    var mi = zeropad(date.getUTCMinutes());
    var ss = zeropad(date.getUTCSeconds());
    return yyyy+'-'+mm+'-'+dd+'T'+hh+':'+mi+':'+ss+'Z';

    function zeropad(v) { return v<10 ? '0'+v : ''+v }
  }
  ,

  parseCSV : function(str) {
    var parser = new CSVParser(str);
    var data = [];
    while (parser.nextLine()) {
      var tokens = [];
      var token;
      while (!lng.isNullOrUndefined(token = parser.nextToken())) {
        tokens.push(token);
      }
      data.push(tokens);
    }
    return data;
  }

};

/**
 *
 */
var CSVParser = afstr.CSVParser = lng.defineClass({

  initialize : function(text) {
    this.text = text;
    this.cursor = 0;
  }
  ,

  nextToken : function() {
    var cell;
    var dquoted = false;
    var firstChar = this.text.charAt(this.cursor);
    if (firstChar == '' || firstChar == '\r' || firstChar == '\n') return null;
    if (firstChar == '"') dquoted = true;
    if (dquoted) {
      var dq = this.cursor;
      while(true) {
        dq++;
        dq = this.text.indexOf('"', dq);
        if (dq<0 || this.text.charAt(dq+1)!='"') break;
        else dq++;
      }
      if (dq>=0) {
        var delim = this.text.charAt(dq+1);
        cell = this.text.substring(this.cursor, dq+1);
        this.cursor = dq + (delim == ',' ? 2 : 1);
      } else {
        cell = this.text.substring(this.cursor);
        this.cursor = this.text.length;
      }
      return cell.replace(/""/g,'"').replace(/^"/,'').replace(/"$/,'');
    } else {
      var comma = this.text.indexOf(',', this.cursor);
      var cr = this.text.indexOf('\r', this.cursor);
      var lf = this.text.indexOf('\n', this.cursor);
      comma = comma<0 ? this.text.length+1 : comma;
      cr = cr<0 ? this.text.length+1 : cr;
      lf = lf<0 ? this.text.length+1 : lf;
      var pivot = Math.min(comma, cr, lf, this.text.length);
      cell = this.text.substring(this.cursor, pivot)
      this.cursor = pivot;
      if (comma==pivot) this.cursor++;
      return cell;
    }
  }
  ,

  nextLine : function() {
    for (var c = this.text.charAt(this.cursor);
        c == '\r' || c == '\n';
        c = this.text.charAt(++this.cursor))
      ;
    if (this.cursor == this.text.length) return false
    else return true;
  }

});

/* ---------------------------------------------------------------------- */

/**
 * afrous.async
 */
var afasync = afrous.async = {

  responder : function(callback, scope) {
    if (callback instanceof afasync.Responder) {
      return callback;
    } else if (typeof callback == 'function') {
      return new afasync.Responder({ onSuccess : callback, scope : scope });
    } else if (callback.onSuccess && typeof (callback.onSuccess) == 'function' &&
               callback.onFailure && typeof (callback.onFailure) == 'function') {
      return new afasync.Responder(callback);
    } else {
      throw 'Given callback object is not function or responder';
    }
  }
  ,

  /*
  map : function(iter, func, callback) {
    if (lng.isArray(iter)) {
      iter = afasync.iterator.fromArray(iter);
    }
    iter.map(func).evaluate(callback);
  }
  ,

  iterate : function(iter, func, callback) {
    if (lng.isArray(iter)) {
      iter = afasync.iterator.fromArray(iter);
    }
    afasync.iterator.serialize(iter.map(func)).evaluate(callback);
  }
  ,

  filter : function(iter, func, callback) {
    if (lng.isArray(iter)) {
      iter = afasync.iterator.fromArray(iter);
    }
    iter.filter(func).evaluate(callback);
  }
  ,

  inject : function(iter, initial, func, callback) {
    if (lng.isArray(iter)) {
      iter = afasync.iterator.fromArray(iter);
    }
    afasync.iterator.serialize(iter).inject(initial, func).evaluate(callback);
  }
  ,
  */

  map : function(arr, func, callback) {
    callback = afasync.responder(callback);
    var next = callback.onSuccess;
    var count = arr.length;
    var rarr = new Array(arr.length);
    if (count==0) callback.onSuccess(rarr);
    lng.forEach(arr, function(a, i) {
      afasync.stack(func).call(null, a, i, arr, {
        onSuccess : function(ret) {
          rarr[i] = ret;
          count--;
          if (count==0) next(rarr);
        },
        onFailure : function(error) {
          rarr[i] = error;
          count--;
          next = callback.onFailure;
          if (count==0) next(rarr);
        }
      })
    })
  }
  ,

  filter : function(arr, func, callback) {
    callback = afasync.responder(callback);
    afasync.map(arr, func, callback.extend(function(flags) {
      for (var i=0, ret=[], len=flags.length; i<len; i++) {
        if (flags[i]) ret.push(arr[i]);
      }
      callback.onSuccess(ret);
    }));
  }
  ,

  iterate : function(arr, func, callback) {
    callback = afasync.responder(callback);
    var error = false;
    var rarr = new Array(arr.length);

    _iterate(0);

    function _iterate(i) {
      if (i<arr.length) {
        afasync.stack(func).call(null, arr[i], i, arr, {
          onSuccess : function(ret) {
            rarr[i] = ret;
            _iterate(i+1);
          },
          onFailure : function(error) {
            rarr[i] = error;
            error = true;
            _iterate(i+1);
          }
        });
      } else {
        (error ? callback.onFailure : callback.onSuccess)(rarr);
      }
    }

  }
  ,

  inject : function(arr, initial, func, callback) {
    callback = afasync.responder(callback);

    var start = 0;
    if (!initial) {
      initial = arr[0];
      start = 1;
    }
    _inject(start, initial);

    function _inject(i, memo) {
      if (i<arr.length) {
        afasync.stack(func).call(null, memo, arr[i], i, arr, {
          onSuccess : function(ret) { _inject(i+1, ret) },
          onFailure : callback.onFailure
        });
      } else {
        callback.onSuccess(memo);
      }
    }
  }
  ,

  // To prevent recursive call stack size error.
  // It doesn't check real stack depth,
  // but if the counter exceeds the limit, it clears function call stack by delaying execution.
  stack : (function() {
    var cnt = 0;
    var limit = -1;

    var i = 0;
    return function(fn) {
      // Detect browser's stack depth
      // Stack overflow exception is not always catchable in all runtime environment.
      // So calcStackLimit must be called in new stack using setTimeout.
      function calcStackLimit() {
        // firebug requires much computing power for creating deep stack, so using fixed num.
        if (window.console && window.console.firebug) {
          limit = 2000;
        } else {
          try {
            limit = 0;
            (function() { if (limit++ < 5000) arguments.callee(); })();
          } catch(e) {}
        }
      }

      if (limit<0) { limit = 0; lng.delay(calcStackLimit, 0)(); }
      if (cnt * 10 < limit) {
        cnt++;
        return fn;
      } else {
        cnt = 0;
        return lng.delay(fn, 1); // clear fn call stack
      }

    }
  })()
  ,

  /**
   *
   */
  synchronize : function(func) {
    var sync = new afasync.Synchronizer();
    return sync.synchronize(func);
  }

};


/**
 *
 */
afasync.Responder = lng.defineClass({
  /**
   *
   */
  initialize : function(callback) {
    var scope = callback.scope || this;
    this.onSuccess = function() {
      afasync.stack(callback.onSuccess).apply(scope, arguments);
    };
    this.onFailure = function() {
      afasync.stack(callback.onFailure || callback.onSuccess).apply(scope, arguments);
    };
  }
  ,

  /**
   * Create and return extended responder instance
   * @param callback Object|Function callback overriding config. Accepts after/before injection.
   * @returns Responder extended responder instance
   */
  extend : function(callback, scope) {
    if (typeof callback == 'function') {
      callback = { onSuccess : callback, scope : scope };
    }
    var onSuccess = callback.onSuccess || this.onSuccess;
    var onFailure = callback.onFailure || this.onFailure;

    if (callback.beforeSuccess || callback.afterSuccess || callback.after) {
      onSuccess = (function(orig) {
        return function() {
          if (!callback.beforeSuccess ||
               callback.beforeSuccess.apply(this, arguments)!==false) {
            orig.apply(this, arguments);
            if (callback.afterSuccess) callback.afterSuccess.apply(this, arguments);
            if (callback.after) callback.after.call(this);
          }
        }
      })(onSuccess);
    }

    if (callback.beforeFailure || callback.afterFailure || callback.after) {
      onFailure = (function(orig) {
        return function() {
          if (!callback.beforeFailure ||
               callback.beforeFailure.apply(this, arguments)!==false) {
            orig.apply(this, arguments);
            if (callback.afterFailure) callback.afterFailure.apply(this, arguments);
            if (callback.after) callback.after.call(this);
          }
        }
      })(onFailure);
    }

    return new afasync.Responder({
      onSuccess : onSuccess, onFailure : onFailure, scope : callback.scope || this.scope
    });
  }

});


/**
 * Synchronize each requested asynchronous functions to serve linear execution.
 * All request are ordered, and serialized to serve.
 */
afasync.Synchronizer = lng.defineClass({
  /**
   *
   */
  initialize : function() {
    this.queue = [];
    this.active = false;
  }
  ,

  /**
   *
   */
  execute : function(func) {
    if (func) {
      this.queue.push(function _execute(){
        return func.apply(this, arguments);
      });
    }
    return this;
  }
  ,

  /**
   *
   */
  synchronize : function(func) {
    this.queue.push(this._synchronize(func));
    return this;
  }
  ,

  /**
   *
   */
  _synchronize : function(func) {
    return function _synchronize() {
      var resumed, suspended;
      function _resume(arg) {
        resumed = [ arg ];
        if (suspended) {
          this.running = true;
          afasync.stack(this._process).apply(this, resumed);
        }
      }
      function _suspend() {
        suspended = true;
        if (!resumed) {
          this.running = false;
        } else {
          return resumed[0];
        }
      }
      var args = Array.prototype.slice.apply(arguments);
      var ret = func.apply(this, [ lng.bind(_resume, this) ].concat(args));

      if (ret instanceof afasync.Synchronizer) {
        var sync = ret;
        ret = function(next) {
          sync.execute(function() { next.apply(this, arguments) }).end();
        }
      }
      if (typeof ret=='function') {
        // dynamic interruption
        this.queue.unshift(this._synchronize(ret));
        ret = undefined;
      }
      this.queue.unshift(_suspend);
      return ret;
    }
  }
  ,

  /**
   *
   */
  end : function() { this._start() },

  /**
   *
   */
  _start : function() {
    if (!this.active) {
      this.active = true;
      this._process();
    }
  }
  ,

  /**
   *
   */
  _process : function() {
    this.running = true;
    var next = this.queue.shift();
    if (next) {
      var ret = next.apply(this, arguments);
      // process continues as long as its running
      if (this.running) {
        afasync.stack(this._process).apply(this, [ ret ]);
      }
    } else {
      // no queued func right now, quit process exec.
      this.active = false;
      this.running = false;
      return;
    }
  }
  ,

  /**
   *
   */
  abort : function() {
    this.queue = [];
    this.running = false;
    this.active = false;
  }

});


/**
 * An asynchronous object, whose real value can be evaluated after its instantiation,
 * by calling "evaluate" method.
 */
var DelayedObject = afasync.DelayedObject = lng.defineClass({
  /**
   *
   */
  initialize : function(props) {
    lng.extend(this, props);
  }
  ,

  /**
   * Evaluate the value of the object
   * @abstract
   */
  evaluate : function(callback) {
    afasync.responder(callback).onSuccess(this);
  }
  ,

  /**
   * Reset the evaluated value
   * @abstract
   */
  reset : function() {}

});

/* ---------------------------------------------------------------------- */

/**
 * afrous.async.iterator
 */
var afiterator = afasync.iterator = {};


/**
 * Asyncronous iterator class.
 * Each iteration element is asynchronous, that is, returned in delayed manner.
 */
var Iterator = afiterator.Iterator = lng.defineClass({

  /**
   * Fetch next element set.
   * Fetched element set is returned to the given callback in array.
   * If there's no element to fetch, iterator should return empty array, or undefined value.
   * Befor method invocation, you should call #hasNext to check next element is available.
   *
   * @param callback Object|Responder|Function
   * @returns boolean Returns true when next fetch is ready, without waiting callback.
   * @abstract
   */
  next : function(callback) {
    afasync.responder(callback).onSuccess();
  }
  ,

  /**
   * Check if iterator supports skipping
   * @returns true when supports skipping
   */
  supportSkip : function() {
    return false;
  }
  ,

  /**
   * Proceeed cursor for given value, without fetching contents.
   * Actually skipeed count is returned via callback.
   *
   * @param count Integer fetch skip count
   * @param callback Object|Responder|Function
   * @abstract
   */
  skip : function(count, callback) {
    afasync.responder(callback).onSuccess(0);
  }
  ,

  /**
   * Check if the iterator has next element or not.
   *
   * @param callback Object|Responder|Function
   * @abstract
   */
  hasNext : function(callback) {
    afasync.responder(callback).onSuccess(false);
  }
  ,

  /**
   * Return total count in iterator when its available.
   * If cannot know total count, returns undefined;
   *
   * @returns Integer total record count in iterator
   */
  getTotalCount : function() {
    return undefined;
  },

  /**
   * Reset the fetch cursor to initial location
   * @abstract
   */
  reset : function() {},

  /**
   * Fetch all elements in iterator and return them in array.
   * @param callback
   */
  evaluate : function(callback) {
    callback = afasync.responder(callback);

    var iter = this;
    var arr = [];
    var count = 0;
    var requested = false;
    var callbacked = false;

    iter.reset();
    afasync.stack(_fetch).call(null, 0);

    function _fetch(i) {
      afasync.stack(iter.hasNext).call(iter, callback.extend(function(hasNext) {
        if (hasNext) {
          count++;
          var fetched = false;
          var immediate = afasync.stack(iter.next).call(iter, afasync.responder({
            onSuccess : function(records) {
              records = typeof records == 'undefined' ? [] : records;
              arr[i] = records;
              count--;
              afasync.stack(_wait).call(null);
              if (!immediate) {
                fetched = true;
                afasync.stack(_fetch).call(null, i+1);
              }
            },
            onFailure : _failure
          }));
          // can move cursor next without waiting the fetch callback
          if (immediate && !fetched) afasync.stack(_fetch).call(null, i+1);
        } else {
          requested = true;
          afasync.stack(_wait).call(null);
        }
      }));
    }

    function _failure() {
      if (!callbacked) {
        callbacked = true;
        callback.onFailure.apply(callback, arguments);
      }
    }

    function _wait() {
      if (!callbacked && requested && count==0) {
        callbacked = true;
        arr = Array.prototype.concat.apply([], arr);
        callback.onSuccess(arr);
      }
    }

  }
  ,


  /**
   * Create an iterator, which applies async map function to each elements of original iterator
   *
   * @param func Function asynchronous map function
   * @returns Iterator
   */
  map : function(func) {
    this.reset();
    return new MapIterator(this, func);
  }
  ,

  /**
   * Create an iterator, which selectively filters each elements of original iterator,
   * by applying asynchronous filter function.
   *
   * @param func Function asynchronous filter function
   * @returns Iterator
   */
  filter : function(func) {
    this.reset();
    return new FilterIterator(this, func);
  }
  ,

  /**
   * Accumulatively applies the function to each elements of the iterator,
   * and generates accumulated result.
   * It works like Ruby's Array#inject method, but asynchronous.
   *
   * @param initial ANY initial value for accumulation
   * @param func Function asynchronous callback function
   * @returns Object a delayed object with evaluate function to fetch value afterward.
   */
  inject : function(initial, func) {
    this.reset();
    return new InjectResult(this, initial, func);
  }
  ,

  /**
   * Create an iterator which partially returns the elements in given bound
   * from the orginal iterator
   *
   * @param start Integer boundary start
   * @param end Integer boundary end
   * @returns Iterator
   */
  slice : function(start, end) {
    this.reset();
    return new PartialIterator(this, start, end);
  }

}, DelayedObject); // end class Iterator


/**
 * Create an iterator, which applies async map function to each elements of original iterator
 */
var MapIterator = afiterator.MapIterator = lng.defineClass({
  /**
   *
   */
  initialize : function(iter, func) {
    this.iter = iter;
    this.func = func;
  }
  ,

  /**
   *
   */
  next : function(callback) {
    callback = afasync.responder(callback);
    var func = this.func;
    return afasync.stack(this.iter.next).call(this.iter, callback.extend(function(records) {
      if (typeof records == 'undefined') {
        callback.onSuccess(records);
      } else {
        afasync.map(records, func, callback.extend(function(recs) {
          callback.onSuccess(recs);
        }));
      }
    }));
  }
  ,

  /**
   *
   */
  supportSkip : function() {
    return this.iter.supportSkip();
  }
  ,

  /**
   *
   */
  skip : function(count, callback) {
    afasync.stack(this.iter.skip).call(this.iter, count, callback);
  }
  ,

  /**
   *
   */
  hasNext : function(callback) {
    afasync.stack(this.iter.hasNext).call(this.iter, callback);
  }
  ,

  /**
   *
   */
  getTotalCount : function() {
    return this.iter.getTotalCount();
  }
  ,

  /**
   *
   */
  reset : function(force) {
    this.iter.reset(force);
  }

}, Iterator); // end of class MapIterator


/**
 * Create an iterator, which selectively filters each elements of original iterator,
 * by applying asynchronous filter function.
 */
var FilterIterator = afiterator.FilterIterator = lng.defineClass({
  /**
   *
   */
  initialize : function(iter, func) {
    this.iter = iter;
    this.ready = false;
    this.func = func;
  }
  ,

  /**
   * Seek next filter-matching records.
   * Callback returns true if next records found.
   */
  _seek : function(callback) {
    var func = this.func;
    var iter = this.iter;
    afasync.stack(iter.hasNext).call(iter, callback.extend(function(hasNext) {
      if (hasNext) {
        afasync.stack(iter.next).call(iter, callback.extend(function(records) {
          if (typeof records == 'undefined') {
            afasync.stack(this._seek).call(this, callback);
          } else {
            afasync.filter(records, func, callback.extend(function(records) {
              if (records.length>0) {
                this.nextRecords = records;
                this.ready = true;
                callback.onSuccess(true);
              } else {
                afasync.stack(this._seek).call(this, callback);
              }
            }, this));
          }
        }, this));
      } else {
        this.ready = false;
        callback.onSuccess(false);
      }
    }, this));
  }
  ,

  /**
   *
   */
  next : function(callback) {
    callback = afasync.responder(callback);
    if (this.ready) {
      this.ready = false;
      callback.onSuccess(this.nextRecords);
      return true;
    } else {
      afasync.stack(this._seek).call(this, callback.extend(function() {
        this.ready = false;
        callback.onSuccess(this.nextRecords);
      }, this));
    }
  }
  ,

  /**
   *
   */
  hasNext : function(callback) {
    callback = afasync.responder(callback);
    if (this.ready) {
      callback.onSuccess(true);
    } else {
      afasync.stack(this._seek).call(this, callback);
    }
  }
  ,

  /**
   *
   */
  reset : function(force) {
    this.ready = false;
    this.nextRecords = undefined;
    this.iter.reset(force);
  }

}, Iterator); // end of class FilterIterator


/**
 * Accumulatively applies the function to each elements of the iterator,
 * and generates accumulated result.
 * It works like Ruby's Array#inject method, but asynchronous.
 */
var InjectResult = afiterator.InjectResult = lng.defineClass({
  /**
   *
   */
  initialize : function(iter, initial, func) {
    this.iter = iter;
    this.initial = initial;
    this.func = func;
    this.calculated = false;
  }
  ,

  /**
   *
   */
  _inject : function(callback) {
    var iter = this.iter;
    var func = this.func;
    function _inject(memo) {
      iter.hasNext(callback.extend(function(hasNext) {
        if (hasNext) {
          iter.next(callback.extend(function(records) {
            afasync.inject(records, memo, func, callback.extend(
              function(ret) { _inject(ret) }
            ));
          }));
        } else {
          callback.onSuccess(memo);
        }
      }));
    }
    _inject(this.initial);
  }
  ,

  /**
   *
   */
  evaluate : function(callback) {
    callback = afasync.responder(callback);
    if (this.calculated) {
      callback.onSuccess(this.result);
    } else {
      afasync.stack(this._inject).call(this, callback.extend({
        beforeSuccess : function(res) {
          this.result = res;
          this.calculated = true;
        },
        scope : this
      }));
    }
  }
  ,

  /**
   *
   */
  reset : function(force) {
    this.result = undefined;
    this.calculated = false;
    this.iter.reset(force);
  }

}, DelayedObject); // end of class InjectResult


/**
 * Create an iterator which partially returns the elements in given bound
 * from the orginal iterator
 */
var PartialIterator = afiterator.PartialIterator = lng.defineClass({
  /**
   *
   */
  initialize : function(iter, start, end) {
    this.iter = iter.supportSkip() ? iter : new BufferedIterator(iter);
    this.start = start || 0;
    this.end = end;
    this.cursor = 0;
    this.ready = false;
  }
  ,

  /**
   * seek the first element
   */
  _seek : function(callback) {
    var iter = this.iter;
    if (this.cursor < this.start) {
      var count = this.start - this.cursor;
      afasync.stack(iter.hasNext).call(iter, callback.extend(function(hasNext) {
        if (hasNext) {
          afasync.stack(iter.skip).call(iter, count, callback.extend(function() {
            this.cursor = this.start;
            this.ready = true;
            callback.onSuccess(true);
          }, this));
        } else {
          this.ready = true;
          callback.onSuccess(false);
        }
      }, this));
    } else {
      this.ready = true;
      callback.onSuccess(true);
    }
  }
  ,


  /**
   *
   */
  next : function(callback) {
    callback = afasync.responder(callback);

    if (this.ready) {
      afasync.stack(_fetch).call(this);
    } else {
      afasync.stack(this._seek).call(this, callback.extend(function() {
        afasync.stack(_fetch).call(this);
      }, this));
    }

    function _fetch() {
      if (this.cursor >= this.end) {
        callback.onSuccess(); // return undefined value
      } else {
        afasync.stack(this.iter.next).call(this.iter, callback.extend(function(records) {
          if (typeof records == 'undefined') {
            callback.onSuccess();
          } else {
            if (this.cursor + records.length >= this.end) {
              records = records.slice(0, this.end - this.cursor);
            }
            this.cursor += records.length;
            callback.onSuccess(records);
          }
        }, this));
      }
    }
  }
  ,

  /**
   *
   */
  supportSkip : function() {
    return true;
  }
  ,

  /**
   *
   */
  skip : function(count, callback) {
    callback = afasync.responder(callback);
    if (this.ready) {
      return afasync.stack(_skip).call(this);
    } else {
      afasync.stack(this._seek).call(this, callback.extend(function() {
        afasync.stack(_skip).call(this);
      }, this));
    }
    function _skip() {
      if (this.cursor >= this.end) {
        callback.onSuccess(0);
      } else {
        afasync.stack(this.iter.skip).call(this.iter, count, callback.extend(function(cnt) {
          if (this.cursor + cnt >= this.end) {
            cnt = this.end - this.cursor;
          }
          this.cursor += cnt;
          callback.onSuccess(cnt);
        }, this));
      }
    }
  }
  ,

  /**
   *
   */
  hasNext : function(callback) {
    callback = afasync.responder(callback);
    if (this.ready) {
      afasync.stack(_check).call(this);
    } else {
      afasync.stack(this._seek).call(this, callback.extend(function() {
        afasync.stack(_check).call(this);
      }, this));
    }
    function _check() {
      if (this.cursor >= this.end) {
        callback.onSuccess(false);
      } else {
        afasync.stack(this.iter.hasNext).call(this.iter, callback);
      }
    }
  }
  ,

  /**
   *
   */
  getTotalCount : function() {
    var totalCount = this.iter.getTotalCount();
    if (typeof totalCount != 'undefined') {
      var end = this.end > totalCount ? totalCount : this.end;
      totalCount = end > this.start ?  end - this.start : 0;
    }
    return totalCount;
  }
  ,

  /**
   *
   */
  reset : function(force) {
    this.cursor = 0;
    this.ready = false;
    this.iter.reset(force);
  }

}, Iterator); // end of class PartialIterator



/**
 * Caches fetched elements in local buffer.
 * To clear all cached buffers, use #reset(true)
 */
var BufferedIterator = afiterator.BufferedIterator = lng.defineClass({

  /**
   *
   */
  initialize : function(iter, options) {
    this.iter = iter;
    this.buf = [];
    this.options = options || {};
    this.cursor = 0;
    this.itercursor = 0;
    this.fetched = false;
  }
  ,

  /**
   *
   */
  next : function(callback) {
    callback = afasync.responder(callback);
    var buf = this.buf;
    if (this.cursor < buf.length) {
      var records = this.options.fetchsize ?
                    buf.slice(this.cursor, this.cursor + this.options.fetchsize) :
                    buf.slice(this.cursor);
      this.cursor += records.length;
      callback.onSuccess(records);
      return true;
    } else {
      if (this.iter.supportSkip() && buf.length > this.itercursor) {
        var cnt = buf.length - this.itercursor
        afasync.stack(this.iter.skip).call(this.iter, cnt, callback.extend(function(c) {
          this.itercursor += c;
          afasync.stack(this.next).call(this, callback);
        }, this));
      } else {
        afasync.stack(this.iter.next).call(this.iter, callback.extend(function(records) {
          if (typeof records == 'undefined') {
            callback.onSuccess();
          } else {
            this.itercursor += records.length;
            // if cursor overskipped, don't cache it.
            if (this.cursor == buf.length) {
              Array.prototype.push.apply(buf, records);
              records = this.options.fetchsize ?
                        buf.slice(this.cursor, this.cursor + this.options.fetchsize) :
                        buf.slice(this.cursor);
            }
            this.cursor += records.length;
            callback.onSuccess(records);
          }
        }, this));
      }
      return false; // can't proceed to next before buf.push()
    }
  }
  ,

  /**
   *
   */
  supportSkip : function() {
    return true;
  }
  ,

  /**
   *
   */
  skip : function(count, callback) {
    callback = afasync.responder(callback);
    var buf = this.buf;
    if (this.cursor + count <= buf.length) {
      this.cursor += count;
      callback.onSuccess(count);
    } else {
      if (this.iter.supportSkip()) {
        if (buf.length > this.itercursor) {
          var cnt = buf.length - this.itercursor
          afasync.stack(this.iter.skip).call(this.iter, cnt, callback.extend(function(c) {
            this.itercursor += c;
            afasync.stack(this.skip).call(this, count, callback);
          }, this));
        } else {
          var cnt = this.cursor + count - buf.length;
          var acnt = count - cnt;
          this.cursor = buf.length;
          afasync.stack(this.iter.skip).call(this.iter, cnt, callback.extend(function(c) {
            this.itercursor += c;
            this.cursor += c;
            callback.onSuccess(acnt + c);
          }, this));
        }
      } else {
        var fetched = 0;

        function _fetch() {
          afasync.stack(this.hasNext).call(this, callback.extend(function(hasNext) {
            if (hasNext) {
              afasync.stack(this.next).call(this, callback.extend(function(records) {
                fetched += records.length;
                if (fetched > count) {
                  this.cursor -= fetched - count;
                  callback.onSuccess(count);
                } else {
                  afasync.stack(_fetch).call(this);
                }
              }, this));
            } else {
              callback.onSuccess(fetched);
            }
          }, this));
        }

        afasync.stack(_fetch).call(this);
      }
    }
  }
  ,

  /**
   *
   */
  hasNext : function(callback) {
    callback = afasync.responder(callback);
    var buf = this.buf;
    if (this.cursor < buf.length) {
      callback.onSuccess(true);
    } else {
      if (this.supportSkip() && buf.length > this.itercursor) {
        var cnt = buf.length - this.itercursor
        afasync.stack(this.iter.skip).call(this.iter, cnt, callback.extend(function(c) {
          this.itercursor += c;
          afasync.stack(this.hasNext).call(this, callback);
        }, this));
      } else {
        afasync.stack(this.iter.hasNext).call(this.iter, callback.extend({
          beforeSuccess : function(hasNext) { if (!hasNext) { this.fetched = true } },
          scope : this
        }));
      }
    }
  }
  ,

  /**
   *
   */
  getTotalCount : function() {
    var totalCount = this.iter.getTotalCount();
    if (typeof totalCount=='undefined' && this.fetched) { totalCount = this.buf.length }
    return totalCount;
  }
  ,

  /**
   *
   */
  requestRefresh : function() {
    this.clearFlag = true;
  }
  ,

  /**
   *
   */
  reset : function(force) {
    if (force || this.clearFlag) {
      this.clearFlag = false;
      this.buf = [];
      this.fetched = false;
      this.iter.reset(force);
      this.itercursor = 0;
    } else if (this.iter.supportSkip()) {
      this.iter.reset();
      this.itercursor = 0;
    }
    this.cursor = 0;
  }

}, Iterator);


/**
 * Walk through iterator set, and act as one big iterator.
 */
var IteratorSetIterator = afiterator.IteratorSetIterator = lng.defineClass({

  /**
   *
   */
  initialize : function(itrset) {
    this.itrset = itrset;
    this.iter = null;
    this.itrset.reset();
  }
  ,

  /**
   *
   */
  _nextIterator : function(callback) {
    var itrset = this.itrset;
    afasync.stack(itrset.hasNext).call(itrset, callback.extend(function(hasNext) {
      if (hasNext) {
        afasync.stack(itrset.next).call(itrset, callback.extend({
          onSuccess : function(iter) {
            if (typeof iter == 'undefined') {
              this.iter = null;
              callback.onSuccess();
            } else {
              iter = iter[0];
              iter = lng.isIterator(iter) ? iter :
                     lng.isArray(iter) ? afasync.iterator.fromArray(iter) :
                     afasync.iterator.fromArray([iter]);
              this.iter = iter.supportSkip() ? iter : new BufferedIterator(iter);
              callback.onSuccess();
            }
          },
          scope : this
        }));
      } else {
        this.iter = null;
        callback.onSuccess();
      }
    }, this));
  }
  ,

  /**
   *
   */
  next : function(callback) {
    callback = afasync.responder(callback);
    if (this.iter) {
      return afasync.stack(this.iter.next).call(this.iter, callback);
    } else {
      afasync.stack(this._nextIterator).call(this, callback.extend(function() {
        if (this.iter) {
          afasync.stack(this.iter.next).call(this, iter, callback);
        } else {
          callback.onSuccess(); // return undefined.
        }
      }, this));
    }
  }
  ,

  /**
   *
   */
  supportSkip : function() {
    return true;
  }
  ,

  /**
   *
   */
  skip : function(count, callback) {
    callback = afasync.responder(callback);
    if (this.iter) {
      afasync.stack(_skip).call(this);
    } else {
      afasync.stack(this._nextIterator).call(this, callback.extend(function() {
        if (this.iter) {
          afasync.stack(_skip).call(this);
        } else {
          callback.onSuccess(0); // no iterator, no skip
        }
      }, this));
    }

    function _skip() {
      afasync.stack(this.iter.skip).call(this.iter, count, callback.extend(function(cnt) {
        if (cnt<count) {
          afasync.stack(this.hasNext).call(this, callback.extend(function(hasNext) {
            if (hasNext) {
              afasync.stack(this.skip).call(this, count-cnt, callback.extend(function(c) {
                callback.onSuccess(cnt + c);
              }));
            } else {
              callback.onSuccess(cnt);
            }
          }, this));
        } else {
          callback.onSuccess(cnt);
        }
      }, this));
    }
  }
  ,

  /**
   *
   */
  hasNext : function(callback) {
    if (this.iter) {
      afasync.stack(this.iter.hasNext).call(this.iter, callback.extend({
        beforeSuccess : function(hasNext) {
          if (!hasNext) {
            this.iter = null;
            afasync.stack(_nextIteratorHasNext).call(this);
            return false;
          }
        },
        scope : this
      }));
    } else {
      afasync.stack(_nextIteratorHasNext).call(this);
    }

    function _nextIteratorHasNext() {
      afasync.stack(this._nextIterator).call(this, callback.extend(function() {
        if (this.iter) {
          afasync.stack(this.hasNext).call(this, callback);
        } else {
          callback.onSuccess(false);
        }
      }, this));
    }
  }
  ,

  /**
   *
   */
  reset : function(force) {
    this.iter = null;
    this.itrset.reset(force);
  }

}, Iterator); // end of class IterSetIterator



var ArrayIterator = afiterator.ArrayIterator = lng.defineClass({
  /**
   *
   */
  initialize : function(arr, options) {
    this.arr = arr;
    this.options = options || {};
    this.cursor = 0;
  }
  ,

  /**
   *
   */
  next : function(callback) {
    callback = afasync.responder(callback);
    var records = this.options.fetchsize ?
                  this.arr.slice(this.cursor, this.cursor + this.options.fetchsize) :
                  this.arr.slice(this.cursor);
    this.cursor += records.length;
    lng.delay(callback.onSuccess, 0).call(callback, records); // force clear stack.
    return true;
  }
  ,

  /**
   *
   */
  supportSkip : function() {
    return true;
  }
  ,

  /**
   *
   */
  skip : function(count, callback) {
    this.cursor += count;
    if (this.arr.length < this.cursor) {
      count -= (this.cursor - this.arr.length);
      this.cursor = this.arr.length;
    }
    afasync.responder(callback).onSuccess(count);
  }
  ,

  /**
   *
   */
  hasNext : function(callback) {
    afasync.responder(callback).onSuccess(this.cursor < this.arr.length);
  }
  ,

  /**
   *
   */
  getTotalCount : function() {
    return this.arr.length;
  }
  ,

  /**
   *
   */
  reset : function() {
    this.cursor = 0;
  }

}, Iterator); // end of class ArrayIterator


/**
 * Iterator utility methods
 */
afasync.iterator = lng.extend(afasync.iterator, {

  /**
   *
   */
  fromArray : function(arr, options) {
    return new ArrayIterator(arr, options);
  }
  ,

  /**
   * Serialize the execution of fetch to happen in order in Iterator#evalute.
   * Each #next wait each next fetch execution has completed
   */
  serialize : function(iter) {
    var _next = iter.next;
    iter.next = function() {
      _next.apply(this, arguments);
      return false;
    }
    return iter;
  }

});

/* ---------------------------------------------------------------------- */

/**
 *
 */
var afajax = afrous.ajax = {

  /**
   *
   */
  createXMLHttpRequest : function() {
    if (window.XMLHttpRequest) {
      // safari seems not allowing XMLHttpRequest in opened window
      if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) {
        var w = (function(w){
          return w.opener ? arguments.callee(w.opener) : w;
        })(window);
        return new w.XMLHttpRequest();
      } else {
        return new XMLHttpRequest();
      }
    } else if (window.ActiveXObject) {
      try {
        return new ActiveXObject("Msxml2.XMLHTTP");
      } catch(e) {
        return new ActiveXObject("Microsoft.XMLHTTP");
      }
    }
  }
  ,

  /**
   *
   */
  request : function(req, callback, scope) {
    callback = afasync.responder(callback, scope);

    req = lng.extend({
      method : 'GET',
      path : '/',
      mimeType : 'text/plain',
      timeout : 15000,
      data : null
    }, req);

    debug.log(req.method + ' ' + req.path);

    afajax.createRequestInstance(req).invoke(function(res) {
      if (res.status>=200 && res.status < 300) {
        try {
          var result = afajax.parseResponse(req.mimeType, res);
          callback.onSuccess({ status : res.status, body : result });
        } catch (e) {
          callback.onFailure(e);
        }
      } else {
        callback.onFailure(res);
      }
    })
  }
  ,

  /**
   *
   */
  createRequestInstance : function(req) {
    var Request =
      req.xdProxyUrl || !afxd.directory.isSameDomainRequest(req.path) ?  afxd.XDomainRequest :
      req.scrapingDef ? afrous.scraping.ScrapeRequest :
      afajax.SimpleRequest;
    return new Request(req);
  }
  ,

  /**
   *
   */
  parseResponse : function(mimeType, res) {
    if (mimeType=='text/json' || mimeType=='application/json') {
      return res.json || lng.parseJSON(res.body);
    } else if (mimeType=='text/xml' || mimeType=='application/xml' ||
               mimeType=='application/atom') {
      return res.xml || new DOMParser().parseFromString(res.body, 'text/xml');
    } else if (mimeType=='text/html') {
      var bodyHTML = res.body.replace(/.*<body.*?>/i, '').replace(/<\/body>.*/i, '');
      var iframe = afdom.createElement({
        tagName : 'iframe',
        style : 'width:0px; heiht:0px; visibility:hidden; position:absolute;'
      });
      document.body.appendChild(iframe);
      var doc = iframe.contentDocument || iframe.contentWindow.document;
      doc.open();
      doc.write('<html><head></head><body></body></html>');
      doc.close();
      doc.getElementsByTagName('body')[0].innerHTML = bodyHTML;
      return doc.documentElement;
    } else {
      return res.body;
    }
  }
  ,

  /**
   *
   */
  iframeRequest : function(path, callback) {
    var iframe = afdom.createElement({
      tagName : 'iframe',
      style : 'width:0px; heiht:0px; visibility:hidden; position:absolute;'
    });
    if (document.all) { // if IE
      iframe.onreadystatechange = function () {
        if (iframe.readyState == "complete") {
          onContentReady();
          iframe.onreadystatechange = null;
        }
      }
    } else {
      iframe.onload = onContentReady;
    }
    document.body.appendChild(iframe);
    iframe.contentWindow.location.href = path;

    function onContentReady() {
      callback({
        document : iframe.contentWindow.document,
        cleanup : function() {
          iframe.parentNode.removeChild(iframe);
        }
      });
    }

  }
  ,

  /**
   *
   */
  jsonp : {
    count : 0,
    callbacks : {},

    /**
     *
     */
    invoke : function (url, callback, options) {
      callback = afasync.responder(callback);
      options = lng.extend({
        charset : 'utf-8',
        jsonpParam : 'callback',
        globalCallback : false,
        timeout : 15000
      }, options || {})

      var index = '_'+(this.count++);
      var script = document.createElement('script');
      script.type = 'text/javascript';
      script.charset = options.charset;
      var callbackContainer = options.globalCallback ? window : afajax.jsonp.callbacks;
      var callbackPrefix = options.globalCallback ? '' : 'afrous.ajax.jsonp.callbacks.';
      var callbackName = options.globalCallback ? 'afrous_ajax_jsonp_callbacks'+index : index;
      script.src = url + (url.indexOf('?')>0 ? '&' : '?') +
                   options.jsonpParam + '=' + callbackPrefix + callbackName;

      debug.log(script.src);

      callbackContainer[callbackName] = function() {
        if (callbackContainer[callbackName]) {
          callback.onSuccess(
            arguments.length==1 ?
            arguments[0] :
            Array.prototype.slice.apply(arguments)
          );
          callbackContainer[callbackName] = undefined;
          try { delete callbackContainer[callbackName] } catch(e) {}
// avoid ie crash
          window.setTimeout(function() { afdom.scriptContainerElem.removeChild(script) }, 1);
        }
      }

      afdom.scriptContainerElem.appendChild(script);

      window.setTimeout(function() {
        if (callbackContainer[callbackName]) {
          callbackContainer[callbackName] = lng.emptyFunc; // suppress error
          callback.onFailure({ error : 'timeout' });
// avoid ie crash
          window.setTimeout(function() { afdom.scriptContainerElem.removeChild(script) }, 1);
        }
      }, options.timeout)

    }
  }

};

/**
 *
 */
afajax.SimpleRequest = function(req) {
  this.req = req;
}

afajax.SimpleRequest.prototype = {

  /**
   *
   */
  invoke : function(callback) {
    var PID;

    var xhr = afajax.createXMLHttpRequest();
    var onreadystatechange = function() {
      if (xhr.readyState==4) {
        if (PID) {
          window.clearInterval(PID);
          PID = null;
        }
        var msg = {
          status : xhr.status,
          body : xhr.responseText
        }
        if (xhr.status>=200 && xhr.status<300) msg.xml = xhr.responseXML;
        xhr = null;
        callback(msg);
      }
    }

    xhr.open(this.req.method, this.req.path, true);
    xhr.setRequestHeader('If-Modified-Since', 'Wed, 15 Nov 1995 00:00:00 GMT');
    var headers = this.req.headers || {};
    for (var name in headers) {
      if (headers[name]) xhr.setRequestHeader(name, headers[name]);
    }
    xhr.send(this.req.data);

    // polling, instead of listening (for IE memory leak prevention)
    PID = window.setInterval(onreadystatechange, 13);
  }

}

/**
 *
 */
var DOMParser = window.DOMParser || new function () {
  var f = function () {};

  f.prototype.parseFromString = function (str, contentType) {
    if (typeof ActiveXObject != "undefined") {
      var d = new ActiveXObject("MSXML.DomDocument");
      d.loadXML(str);
      return d;
    } else if (typeof XMLHttpRequest != "undefined") {
      var req = new XMLHttpRequest;
      req.open("GET", "data:" + (contentType || "application/xml") +
                     ";charset=utf-8," + encodeURIComponent(str), false);
      if (req.overrideMimeType) {
        req.overrideMimeType(contentType);
      }
      req.send(null);
      return req.responseXML;
    }
  }

  return f;
}

/* ---------------------------------------------------------------------- */

var afxd = afrous.xd = {
  // you can override this prop if you are running this script on  crossdomain xhr enabled environment.
  xdNativeSupport : false
};


/**
 *
 */
var XDomainRequest = afxd.XDomainRequest = lng.defineClass({

  timeout : 10000,

  /**
   *
   */
  initialize : function(req, sameDomainImageUrl) {
    this.req = req;
    if (sameDomainImageUrl) this.sameDomainImageUrl = sameDomainImageUrl;
  }
  ,

  /**
   *
   */
  invoke : function(callback, scope) {
    var req = this.req;

    // create hidden iframe
    var ifr = window.document.createElement('iframe');
    ifr.style.width = ifr.style.height = '0px';
    ifr.frameBorder = 0;
    document.body.appendChild(ifr);

    // Serialize request object as JSON string, set to window.name property of iframe.
    ifr.contentWindow.name = lng.toJSON(req);

    // Using same domain image file in the page.
    var retUrl = this.sameDomainImageUrl || location.href;

    var proxyUrl = req.xdProxyUrl || afxd.directory.findProxy(req.path || req.url);
    if (!proxyUrl) {
      callback.call(scope, {
        status : 403,
        body : 'No cross domain proxy HTML is registered for ' + (req.path || url)
      });
      return;
    }
    // Send returning URL in Fragment Identfier (hash)
    ifr.contentWindow.location.replace(proxyUrl + '#' + encodeURIComponent(retUrl));

    this.ifr = ifr;
    this.retUrl = retUrl;
    this.callback = callback;
    this.scope = scope;
    if (req.timeout) this.timeout = req.timeout;

    // timer
    this.startTime = new Date().getTime();

    // Wait until the iframe url redirected back to same domain.
    this.PID = window.setInterval(lng.bind(this.checkResponse, this), 100);
  }
  ,

  /**
   *
   */
  checkResponse : function() {
    try {
      // timeout check
      if (new Date().getTime() - this.startTime > this.timeout) {
        this.cleanup()
        this.callback.call(this.scope, {
          status : 500,
          body : 'Request time out'
        });
        return;
      }
      // Security exception occurrs when iframe window is in different domain.
      if (this.ifr.contentWindow.location.href.split('#')[0] == this.retUrl) {
        // Parse the response string set in window.name.
        var res = lng.parseJSON(this.ifr.contentWindow.name);
        this.ifr.contentWindow.name = undefined;
        this.cleanup();
        this.callback.call(this.scope, res);
      }
    } catch (e) {}
  }
  ,

  /**
   *
   */
  cleanup : function() {
    if (this.PID) window.clearInterval(this.PID);
    this.PID = null;
    if (this.ifr) this.ifr.src = 'javascript:false';
    lng.delay(lng.bind(function() {
      if (this.ifr) {
//        this.ifr.parentNode.removeChild(this.ifr);
        this.ifr = null;
      }
    }, this), 100)();
  }
  ,

  /**
   *
   */
  sameDomainImageUrl : (function(url) {
    var baseUrl = url.split('#')[0].split('/').slice(0, -1).join('/');
    var siteUrl = url.split('/').slice(0, 3).join('/');
    var imgUrl;
    try {
      for (var w = window; ; w = w.parent) {
        var imgs = w.document.getElementsByTagName('img');
        for (var i=0, len=imgs.length; i<len; i++) {
          var img = imgs[i];
          if (img.src && (!/^https?:/i.test(img.src) || img.src.indexOf(siteUrl)==0)) {
            imgUrl = img.src;
            break;
          }
        }
        if (imgUrl || w.parent == w) break;
      }
    } catch (e) {}
    if (imgUrl && !/^https?:\/\//.test(imgUrl)) {
      imgUrl = (imgUrl.charAt(0)=='/' ? siteUrl : baseUrl) + imgUrl;
    }
    return imgUrl;

  })(location.href)

});


/**
 *
 */
var XDomainProxy = afxd.XDomainProxy = lng.defineClass({
  /**
   *
   */
  initialize : function(policy) {
    this.xdPolicy = policy || {};
  }
  ,

  /**
   *
   */
  init : function() {
    var m = location.href.match(/#(.+)$/);
    if (!m) return
    // Returning URL should be encoded in fragment identifier
    var retUrl = decodeURIComponent(m[1]);

    // Get requested site url information for policy enforce.
    var reqUrl = document.referrer || retUrl;
    m = reqUrl.match(/^https?:\/\/([^\/:]+)/);
    var domain = m && m[1];

    // Parse the encoded request
    var request = lng.parseJSON(window.name);

    // Check policy
    if (!this.isAllowed(request, domain)) {
      callbackToOriginDomain({
        status : 403,
        body : 'Access forbidden by cross domain policy'
      });
      return;
    }

    // Putting domain information to request header
    request.headers = request.headers || {};
    request.headers['X-XD-REQUEST-DOMAIN'] = domain;
    request.mimeType = 'text/plain';

    var Request = request.scrapingDef ? afrous.scraping.ScrapeRequest : afajax.SImpleRequest;
    new Request(request).invoke(callbackToOriginDomain);

    function callbackToOriginDomain(response) {
      delete response.xml;
      window.name = lng.toJSON(response);
      retUrl = retUrl.replace(/(#[^#]*)?$/, '#xd-ifr-callback');
      window.location.replace(retUrl);
    }

  }
  ,

  /**
   *
   */
  isAllowed : function(request, domain) {
    if (this.xdPolicy.allowedMethod) {
      var allowedMethod = new RegExp('^(?:'+this.xdPolicy.allowedMethod.replace(/\s*,\s*/g, '|')+')$', 'i');
      if (!allowedMethod.test(request.method)) return false;
    }
    if (this.xdPolicy.allowedDomain) {
      var allowedDomains = this.xdPolicy.allowedDomain.split(/\s*,\s*/);
      domain = domain ? domain.replace(/\.$/, '') : '';
      for (var i=0, len=allowedDomains.length; i<len; i++) {
        var allowedDomain = allowedDomains[i].replace(/(?=\.)/g, '\\').replace(/(?=\*)/g, '.');
        if (new RegExp('^'+allowedDomain+'$').test(domain)) return true;
      }
      return false;
    }
    return false;
  }

});


/**
 *
 *
 */
var XDomainProxyDirectory = afxd.XDomainProxyDirectory = lng.defineClass({

  /**
   *
   */
  initialize : function() {
    this.xdProxies = {};
  }
  ,

  /**
   *
   */
  getProtocolDomain : function(url) {
    return url.split('/').slice(0, 3).join('/');
  }
  ,

  /**
   *
   */
  registerProxy : function(proxyUrl) {
    this.xdProxies[this.getProtocolDomain(proxyUrl)] = proxyUrl;
  }
  ,

  /**
   *
   */
  findProxy : function(url) {
    return this.xdProxies[this.getProtocolDomain(url)];
  }
  ,

  /**
   *
   */
  isSameDomainRequest : function(url, baseUrl) {
    return (!baseUrl && !/^https?:\/\//.test(url)) ||
      (baseUrl || window.location.href).indexOf(url.split('/').slice(0, 3).join('/')) == 0;
  }

});

afxd.directory = new XDomainProxyDirectory();

/* ---------------------------------------------------------------------- */

/**
 *
 */
var afs = afrous.scraping = {};

var WebScraper = afs.WebScraper = lng.defineClass({
  /**
   *
   */
  initialize : function(config) {
    this.config = config;
  }
  ,

  /**
   *
   * <pre>
var scrapingDef = {
  records : {
    records : 'id("test")/div[@class="row"]'
    children : {
      title : 'a[1]/strong',
      link : 'a[1]/@href'
    }
  },
  nextLink : 'd("nav")//a[@class="next"]'
};
</pre>
   *
   */
  scrape : function(node) {
    var result = {};
    for (var key in this.config) {
      var cfg = this.config[key];
      if (lng.isString(cfg)) cfg = { xpath : cfg, target : 'text' };
      var vnodes = this.queryNodes(node, cfg.xpath, !cfg.children);
      result[key] = cfg.children ? lng.map(vnodes, function(vnode) {
        return new WebScraper(cfg.children).scrape(vnode);
      }) : this.convert(vnodes, cfg.target)
    }
    return result;
  }
  ,

  /**
   *
   */
  queryNodes : function(node, xpath, single) {
    var doc = node.nodeType == 9 ? node :
    node.nodeType == 1 ? node.ownerDocument :
    node.nodeType == 2 ? node.ownerElement.ownerDocument : null;
    try {
      var res = doc.evaluate(xpath, node, null, 7, null);
    } catch(e) {}
    if (res) {
      if (single) {
        return res.snapshotItem(0);
      } else {
        var nodes = [];
        for (var i=0, len=res.snapshotLength; i<len; i++) {
          nodes.push(res.snapshotItem(i));
        }
        return nodes;
      }
    }
    return res;
  }
  ,

  /**
   *
   */
  convert : function(n, target) {
    return n &&
      (target=='html' ? n.innerHTML :
       target.indexOf('@')==0 ? (
          target == '@style' && lng.isIE ? n.style.cssText :
          target == '@class' && lng.isIE ? n.className :
          n.getAttribute(target.substring(1))
       ) :
       (n.nodeValue || n.textContent || n.innerText));
  }

});

/**
 *
 */
var FrameDocLoader = afs.FrameDocLoader = lng.defineClass({
  /**
   *
   */
  JS_XPATH_LIB_URL : 'http://resource.afrous.net/javascript-xpath/javascript-xpath.js',

  /**
   *
   */
  initialize : function(url) {
    this.url = url;
    this.callbacks = [];
  }
  ,

  /**
   *
   */
  loadDocument : function() {
    var ifr = this.ifr = document.createElement('iframe');
    ifr.style.width = ifr.style.height = '0px';
    ifr.frameBorder = '0';
    ifr.style.position = 'absolute';
    ifr.style.top = ifr.style.left = '0px';
    ifr.src = 'javascript:false';
    window.document.body.appendChild(ifr);
    var _this = this;
    afrous.dom.addEvent(ifr, 'load', function() {
      afrous.dom.removeEvent(ifr, 'load', arguments.callee);
      _this.attachXPathLib(ifr.contentWindow.document);
    });
    ifr.contentWindow.location.href = this.url;
  }
  ,

  /**
   *
   */
  attachXPathLib : function(doc) {
    if (typeof doc.evaluate !== 'function') {
      var script = doc.createElement('script');
      script.type = 'text/javascript';
      script.src = this.JS_XPATH_LIB_URL;
      (doc.getElementsByTagName('head')[0] || doc.body).appendChild(script);
      afrous.lang.poll({
        work : function() { return typeof doc.evaluate === 'function' },
        callback : this.notifyAll,
        scope : this,
        interval : 100,
        timeout : 10000
      });
    } else {
      this.notifyAll();
    }
  }
  ,

  /**
   *
   */
  notifyAll : function() {
    for (var cb = this.callbacks.pop(); cb; cb = this.callbacks.pop()) {
      cb.onSuccess.call(this, this.ifr.contentWindow.document);
    }
    this.cleanup();
  }
  ,

  /**
   *
   */
  cleanup : function() {
    this.ifr.contentWindow.location.href = 'javascript:false';
  }
  ,

  /**
   *
   */
  addCallback : function(callback) {
    this.callbacks.push(afrous.async.responder(callback));
    return this;
  }


});

/**
 *
 */
var ScrapeRequest = afs.ScrapeRequest = lng.defineClass({
  /**
   *
   */
  initialize : function(request) {
    this.request = request;
  }
  ,

  /**
   *
   */
  invoke : function(callback) {
    callback = afrous.async.responder(callback);
    var req = this.request;
    new FrameDocLoader(req.path).addCallback(function(doc) {
      var res = new WebScraper(req.scrapingDef).scrape(doc);
      callback.onSuccess({
        status : 200,
        json : res
      });
    }).loadDocument();
  }

});

/* ---------------------------------------------------------------------- */

/**
 * afrous.debug
 */
var debug = afrous.debug = {
  log : (typeof console!='undefined' ?
         function(o) { debug.on && console.log(lng.toJSON(o)) } :
         lng.emptyFunc)
};

/* ---------------------------------------------------------------------- */

/**
 *
 */
var afi18n = afrous.i18n = {
  LANG_RESOURCE_SET : {},

  /**
   *
   */
  loadResources : function(lang, namespace, resources) {
    var resourceSet = this.LANG_RESOURCE_SET[lang] = this.LANG_RESOURCE_SET[lang] || {};
    var nsResourceSet = resourceSet[namespace] = resourceSet[namespace] || {};
    var nsRoot = (function(path, context) {
      return !context || path.length==0 ?  context :
              arguments.callee(path.slice(1), context[path[0]]);
    })(namespace.split('.'), window);

    for (var clsname in resources) {
      nsResourceSet[clsname] = lng.extend(nsResourceSet[clsname] || {}, resources[clsname]);
      if (this.language==lang) {
        var cls = nsRoot[clsname];
        if (!cls) continue;
        var resource = nsResourceSet[clsname];
        cls.prototype.RESOURCES = cls.prototype.RESOURCES || {};
        lng.extend(cls.prototype.RESOURCES, resource);
      }
    }
  },

  /**
   *
   */
  setLanguage : function(lang) {
    this.language = lang;
    var resourceSet = this.LANG_RESOURCE_SET[lang] || {};
    for (var namespace in resourceSet) {
      var nsResourceSet = resourceSet[namespace] || {};

      var nsRoot = (function(path, context) {
        return !context || path.length==0 ? context : arguments.callee(path.slice(1), context[path[0]]);
      })(namespace.split('.'), window);

      for (var classname in nsResourceSet) {
        var cls = nsRoot[classname];
        if (!cls) continue;
        var resource = nsResourceSet[classname];
        cls.prototype.RESOURCES = cls.prototype.RESOURCES || {};
        afrous.lang.extend(cls.prototype.RESOURCES, resource);
      }
    }
  }
}

/* ---------------------------------------------------------------------- */

/**
 * Broadcaster
 */
var Broadcaster = afrous.Broadcaster = lng.defineClass({
  initialize : function() {
    this.events = {};
  }
  ,

  addListener : function(event, func, scope) {
    var listener = { scope : scope, func : func };
    if (this.events[event]) {
      this.events[event].push(listener);
    } else {
      this.events[event] = [ listener ];
    }
  }
  ,

  removeListener : function(event, func) {
    if (this.events[event]) {
      this.events[event] = lng.filter(this.events[event], function(listener) {
        return listener.func != func;
      });
    }
  }
  ,

  fire : function(event) {
    var arg = Array.prototype.slice.call(arguments, 1);
    lng.forEach(this.events[event] || [], function(listener) {
      listener.func.apply(listener.scope, arg);
    });
  }

});

/* ---------------------------------------------------------------------- */

/**
 * UnitAction
 */
var UnitAction = afrous.UnitAction = lng.defineClass({
  // type : String
  // label : String
  // description : String
  // inputs : Hash(String, InputDef)
  // pack : UnitActionPackage
  //
  // InputDef : {
  //   name : String
  //   type : String
  //   label : String
  //   description : String
  //   default : Object
  // }

  initialize : function(props) {
    lng.extend(this, props);
    if (props.inputs && lng.isArray(props.inputs)) {
      this.inputs = {};
      lng.forEach(props.inputs || [], function(inputDef) {
        this.inputs[inputDef.name] = inputDef;
      }, this)
    }
    return lng.chain(props, this);
  }
  ,

  getQualifiedType : function() {
    return this.pack.namespace + '.' + this.type;
  }
  ,

  prepare : function(/*ActionRequest*/request, /*Responder*/next) {
    request.readAll(next);
  }
  ,

  execute : function(/*ActionRequest*/request, /*Responder*/callback) {
    callback.onSuccess();
  }
  ,

  prepareInnerParams : function(/*ActionRequest*/request, /*Responder*/callback) {
    callback.onSuccess();
  }

});
/**
 * UnitActionPackage
 */
var UnitActionPackage = afrous.UnitActionPackage = lng.defineClass({
  // namespace : String
  // icon : String
  // uactions : Hash(String, UnitAction)

  initialize : function(namespace, props) {
    this.namespace = namespace;
    lng.extend(this, props || {});
    this.uactions = {};
    this.loaded = false;
    var _this = this;
    this.setup(function() {
      _this.loaded = true;
    })
  }
  ,

  listUnitActions : function() {
    return lng.values(this.uactions);
  }
  ,

  findUnitAction : function(type) {
    return this.uactions[type];
  }
  ,

  register : function(uaction) {
    this.uactions[uaction.type] = uaction;
    uaction.pack = this;
  }
  ,

  setup : function(callback) {
    // Nothing default
    callback();
  }

}, Broadcaster /* super class */);
/**
 * @class UnitActionPackageRegistry
 */
var UnitActionPackageRegistry = afrous.UnitActionPackageRegistry = lng.defineClass({
  // scripts : Array(String)
  // nsmap : Hash(String, String)
  // packages : Hash(String, UnitActionPackage)

  initialize : function() {
    // superclass init
    Broadcaster.prototype.initialize.apply(this);

    this.scripts = [];
    this.nsmap = {};
    this.packages = {};
  }
  ,

  register : function(pack, scriptname) {
    this.packages[pack.namespace] = pack;
    if (scriptname) {
      var regexp = new RegExp("/"+scriptname.replace(/\./g, '\\.')+"$");
      var scriptURL = lng.find(
        this.scripts,
        function(scriptURL){ return regexp.test(scriptURL) }
      );
      if (scriptURL) this.nsmap[pack.namespace] = scriptURL;
    }
    this.fire('register', pack);
  }
  ,

  unregister : function(namespace) {
    if (this.packages[namespace]) delete this.packages[namespace];
    if (this.nsmap[namespace]) delete this.nsmap[namespace];
    this.fire('unregister', namespace);
  }
  ,

  findPackage : function(namespace) {
    return this.packages[namespace];
  }
  ,

  findUnitAction : function(nstype) {
    var namespace = nstype.split('.').slice(0,-1).join('.');
    var type = nstype.split('.').pop();
    var pack = this.packages[namespace];
    return pack && pack.findUnitAction(type);
  }
  ,

  loadScript : function(scriptUrl, overwrite) {
    var s = lng.find(this.scripts, function(s){ return s==scriptUrl });
    if (s && !overwrite) return;
    if (!s) this.scripts.push(scriptUrl);
    lng.loadScript(scriptUrl);
  }
  ,

  scriptBaseURL : (function() {
    var scripts = document.getElementsByTagName('script');
    var regexp = new RegExp('(.*)\\/'+afrous.SCRIPT_NAME+'\\.js$');
    for (var i=0; i<scripts.length; i++) {
      var m = scripts[i].src.match(regexp);
      if (m) {
        var scriptBaseURL = m[1];
        if (scriptBaseURL.indexOf('./')==0) {
          scriptBaseURL = location.href.replace(/[^\/]*$/,'') + scriptBaseURL.substring(2);
        }
        return scriptBaseURL;
      }
    }
  })()
  ,

  getDependencies : function(processDef) {
    var reg = this;
    var nsscript = {};
    function _getDependentScripts(procdef) {
      lng.forEach(lng.values(procdef.actions), function(actionDef) {
        var namespace = actionDef.uaction.pack.namespace;
        var scriptURL = reg.nsmap[namespace];
        if (scriptURL) nsscript[namespace] = scriptURL;
        if (actionDef.innerProcess) _getDependentScripts(actionDef.innerProcess);
      });
    }
    _getDependentScripts(processDef);
    return nsscript;
  }
  ,

  waitLoadComplete : function(namespaces, onLoaded, onTimeout) {
    lng.poll({
      work : function() {
        var p = afrous.packages;
        for (var i=0; i<namespaces.length; i++) {
          if (!p.findPackage(namespaces[i])) return false;
        }
        return true;
      },
      callback : onLoaded,
      errback : onTimeout,
      timeout : 10000,
      interval : 500
    });
  }
  ,

  listPackages : function() {
    return lng.values(this.packages);
  }
  ,

  toConfig : function() {
    var _this = this;
    return lng.map(
      lng.keys(this.nsmap),
      function(namespace) {
        return { namespace : namespace, url : _this.nsmap[namespace] };
      }
    );
  }

}, Broadcaster /* superclass */);

afrous.packages = new UnitActionPackageRegistry();

/* ---------------------------------------------------------------------- */

/**
 * ProcessDef
 */
var ProcessDef = afrous.ProcessDef = lng.defineClass({
  // name : String
  // schema : ProcessSchema
  // params : Hash(String, ParamDef)
  // output : String(EL)
  // actions : Hash(String, ActionDef)
  //
  // ParamDef : ({
  //   name : String
  //   type : String
  //   label : String
  //   description : String
  //   defer : Boolean
  //   default : Object
  // })

  initialize : function(config) {
    // superclass init
    Broadcaster.prototype.initialize.apply(this);

    this.params = {};
    this.actions = {};
    this.actionCount = 0;
    if (config) {
      this.loadConfig(config);
    }
  }
  ,

  loadConfig : function(config) {
    var _this = this;
    this.name = config.name;
    this.schema = afrous.schemas.findSchema(config.type || 'simple');
    if (!this.schema) {
      throw 'No schema defined : '+config.type;
    }

    lng.forEach(config.params || [], function(pconfig) {
      _this.addParamDef(pconfig);
    });
    this.output = config.output;

    lng.forEach(config.actions || [], function(aconfig) {
      _this.addActionDef(new ActionDef(aconfig));
    })

    this.onChange();
  }
  ,

  onChange : function() {
    this.fire('change');
  }
  ,

  onDestroy : function() {
    this.fire('destroy');
  }
  ,

  setName : function(name) {
    this.name = name;
    this.onChange();
  }
  ,

  setType : function(type) {
    this.schema = afrous.schemas.findSchema(type);
    if (!this.schema) {
      throw 'No schema defined : '+type;
    }
    this.onChange();
  }
  ,

  setOutput : function(output) {
    this.output = output;
    this.onChange();
  }
  ,

  setOutputSlot : function(slot, value) {
    if (!this.output || lng.isString(this.output)) {
      this.output = {};
    }
    this.output[slot] = value;
    this.onChange();
  }
  ,

  addParamDef : function(paramDef) {
    if (this.actions[paramDef.name] || this.params[paramDef.name] ||
        this.schema.params[paramDef.name]) {
      throw 'The name "'+paramDef.name+'" is already defined';
    }
    this.params[paramDef.name] = paramDef;
    this.onChange();
  }
  ,

  removeParamDef : function(name) {
    if (this.params[name]) delete this.params[name];
    this.onChange();
  }
  ,

  addActionDef : function(actionDef) {
    if (this.actions[actionDef.name] || this.params[actionDef.name] ||
        this.schema.params[actionDef.name]) {
      throw 'The name "'+actionDef.name+'" is already defined';
    }
    this.actions[actionDef.name] = actionDef;
    actionDef.order = this.actionCount++;
    actionDef.addListener('change', this.onChange, this);
    this.onChange();
  }
  ,

  removeActionDef : function(name) {
    var actionDef = this.actions[name];
    if (actionDef) {
      actionDef.removeListener('change', this.onChange);
      actionDef.destroy();
      delete this.actions[name];
      this.onChange();
    }
  }
  ,

  findActionDef : function(name) {
    return this.actions[name];
  }
  ,

  toConfig : function() {
    return {
      name : this.name || '',
      type : this.schema.type || 'simple',
      requires : afrous.packages.getDependencies(this),
      params : lng.values(this.params),
      output : this.output || '',
      actions : lng.map(
                  lng.values(this.actions).sort(function(a1, a2) {
                    return a1.order - a2.order;
                  }),
                  function(action) {
                    return action.toConfig();
                  }
                )
    };
  }
  ,

  serialize : function() {
    return lng.toJSON(this.toConfig());
  }
  ,

  destroy : function() {
    var _this = this;
    lng.forEach(lng.values(this.actions), function(actionDef) {
      actionDef.removeListener('change', _this.onChange);
      actionDef.destroy();
    });

    this.onDestroy();
  }

}, Broadcaster /* super class */)
/**
 * ActionDef
 */
var ActionDef = afrous.ActionDef = lng.defineClass({
  // name : String
  // uaction : UnitAction
  // inputs : Hash(String, String)
  // extension : Object?
  // nocaching : Boolean
  // innerProcess : ProcessDef?
  // order : Integer
  //
  // InputDef : {
  //   name : String
  //   type : String
  //   label : String
  //   description : String
  //   default : Object
  // }

  initialize : function(config) {
    // superclass init
    Broadcaster.prototype.initialize.apply(this);

    if (config) {
      this.loadConfig(config)
    }
  }
  ,

  loadConfig : function(config) {
    this.name = config.name;
    this.type = config.type;
    this.uaction = afrous.packages.findUnitAction(config.type);
    if (!this.uaction) throw "No UnitAction registered : "+config.type;
    if (config.extension) {
      this.uaction = new UnitAction(lng.chain(config.extension, this.uaction));
    }
    this.inputs = config.inputs;
    for (var name in this.uaction.inputs || {}) {
      if (lng.isNullOrUndefined(this.inputs[name]) &&
          !lng.isNullOrUndefined(this.uaction.inputs[name]['default'])) {
        this.inputs[name] = this.uaction.inputs[name]['default'];
      }
    }
    this.nocaching = config.nocaching || false;
    if (config.innerProcess) {
      this.setInnerProcess(new ProcessDef(config.innerProcess));
    }
    this.config = config;

    this.onChange();
  }
  ,

  setName : function(name) {
    this.name = name;
    this.onChange();
  }
  ,

  setInputs : function(inputs) {
    this.inputs = inputs;
    this.onChange();
  }
  ,

  setInnerProcess : function(innerProcess) {
    this.innerProcess = innerProcess;
    this.innerProcess.addListener('change', this.onChange, this);
  }
  ,

  onChange : function() {
    this.fire('change');
  }
  ,

  onDestroy : function() {
    this.fire('destroy');
  }
  ,

  toConfig : function() {
    this.config.name = this.name;
    this.config.type = this.type;
    if (this.inputs) this.config.inputs = this.inputs;
    if (this.innerProcess) {
      this.config.innerProcess = this.innerProcess.toConfig();
      delete this.config.innerProcess['params'];
    }
    return this.config;
  }
  ,

  destroy : function() {
    if (this.innerProcess) {
      this.innerProcess.removeListener('change', this.onChange);
    }

    this.onDestroy();
  }

}, Broadcaster /* super class */);
/**
 * ProcessRegistry
 */
var ProcessRegistry = afrous.ProcessRegistry = lng.defineClass({
  //configs : Hash(String, ProcessConf)
  //processes : Hash(String, ProcessDef)
  //callbackQueues : Hash(String, Responder[])

  initialize : function() {
    this.configs = {};
    this.processes = {};
    this.callbackQueues = {};
  }
  ,

  loadProcessConfig : function(key, callback) {
    // ver 2.0, metadata service
    //if (afrous.metadata) {
    var _this = this;
    afrous.metadata.getProcess(key, function(config) {
      _this.configs[key] = config;
      if (callback) callback(config);
    });
    /**
    } else { // old process registry
      var config = this.configs[key]
      if (config) {
        if (typeof callback == 'function') callback(config);
        return config;
      }
      if (typeof callback == 'function') {
        var callbackQueue = this.callbackQueues[key]
        if (!callbackQueue) {
          callbackQueue = this.callbackQueues[key] = [];
        }
        callbackQueue.push(callback);
      }

      lng.loadScript(afrous.config.PROCESS_REGISTRY_URL + '/' + key);
    }
    */
  }
  ,

  loadProcessDef : function(key, callback) {
    var procdef, config;
    if (procdef = this.processes[key]) {
      if (typeof callback == 'function') callback(procdef);
      return procdef;
    } else if (config = this.configs[key]) {
      procdef = this.processes[key] = new ProcessDef(config);
      if (typeof callback == 'function') callback(procdef);
      return procdef;
    } else {
      var _this = this;
      this.loadProcessConfig(key, function() {
        _this.loadProcessDef(key, callback);
      });
    }
  }
  ,

  register : function(key, conf) {
    this.configs[key] = conf;
    var callbackQueue = this.callbackQueues[key];
    while (callbackQueue.length>0) {
      var callback = callbackQueue.pop();
      callback(conf);
    }
  }


});

afrous.processes = new ProcessRegistry();
/**
 * ProcessSchema
 */
var ProcessSchema = afrous.ProcessSchema = lng.defineClass({
  // type : String
  // label : String
  // params : Hash(String, Object)
  // output : Object
  //

  initialize : function(config) {
    this.type = config.type;
    this.label = config.label;
    this.params = {};
    lng.forEach(config.params || [], function(pconfig) {
      this.params[pconfig.name] = pconfig;
    }, this);

    this.output = config.output.type == 'complex' ? {
      type : config.output.type,
      slots : (function() {
        var slots = {};
        lng.forEach(config.output.slots, function(slot) {
          slots[slot.name] = slot;
        });
        return slots;
      })()
    } : config.output;
  }

});

/**
 * ProcessSchemaRegistry
 */
var ProcessSchemaRegistry = afrous.ProcessSchemaRegistry = lng.defineClass({
  initialize : function() {
    this.schemas = {}
  }
  ,

  register : function(schema) {
    this.schemas[schema.type] = schema;
  }
  ,

  listSchemas : function() {
    return lng.values(this.schemas);
  }
  ,

  findSchema : function(type) {
    return this.schemas[type];
  }

})

afrous.schemas = new ProcessSchemaRegistry();

/**
 *
 *
 */
afrous.schemas.register(new ProcessSchema({
  type : 'simple',
  label : 'Simple',
  params : [],
  output : {
    type : 'object'
  }
}));

afrous.schemas.register(new ProcessSchema({
  type : 'entryList',
  label : 'Entry List',
  params : [{
    name : 'start',
    label : 'Start Index',
    type : 'integer',
    'default' : 0
  }, {
    name : 'limit',
    label : 'Entry size limit per page',
    type : 'integer',
    'default' : 10
  }],
  output : {
    type : 'complex',
    slots : [{
      name : 'entries',
      label : 'Entries',
      type : 'object[]'
    }, {
      name : 'totalCount',
      label : 'Total count of entries',
      type : 'integer'
    }]
  }
}));

afrous.schemas.register(new ProcessSchema({
  type : 'infEntryList',
  label : 'Infinite Entry List',
  params : [{
    name : 'cursor',
    label : 'Cursor',
    type : 'object'
  }],
  output : {
    type : 'complex',
    slots : [{
      name : 'entries',
      label : 'Entries',
      type : 'object[]'
    }, {
      name : 'next',
      label : 'Cursor to next entries',
      type : 'string'
    }]
  }
}));

/* ---------------------------------------------------------------------- */

/**
 * ProcessInstance
 */
var ProcessInstance = afrous.ProcessInstance = lng.defineClass({
  // definition : ProcessDef
  // parentProc : ProcessInstance?
  // actions : Hash(String, ActionInstance)
  // params : Hash(String, {
  //   value : Object
  //   timestamp : integer
  // })

  initialize : function(definition, parentProc) {
    // superclass init
    Broadcaster.prototype.initialize.apply(this);

    this.definition = definition;
    this.parentProc = parentProc;
    this.actions = {};
    this.params = {};
  }
  ,

  checkUpdated : function(expr, timestamp) {
    var vars = EL.extractVars(expr) || [];
    for (var i=0, len=vars.length; i<len; i++) {
      var ref = this.resolveRef(vars[i]);
      if (ref && ref.checkUpdated(timestamp)) return true;
    }
    return false;
  }
  ,

  start : function(slot, callback) {
    if (arguments.length==1) {
      callback = arguments[0];
      slot = null;
    }
    callback = afasync.responder(callback);
    var output = slot ? this.definition.output[slot] : this.definition.output;
    var _this = this;
    this.evaluate(output, {
      onSuccess : function(result) {
        callback.onSuccess(result.value);
      },
      onFailure : callback.onFailure
    });
  }
  ,

  setParam : function(name, value) {
    var paramDef = this.definition.params[name] || this.definition.schema.params[name];
    if (paramDef) {
      value = lng.cast(paramDef.type, value);
    }
    var param = this.params[name] = this.params[name] || new ProcessParam(this, name);
    param.setValue(value);
    this.onValueChange(name, value);
  }
  ,

  resolveRef : function(name) {
    var ref = this.params[name] || this.actions[name];
    if (!ref) {
      var paramDef = this.definition.params[name] || this.definition.schema.params[name];
      if (paramDef) {
        if (typeof paramDef['default'] != 'undefined') {
          var value = lng.cast(paramDef.type, paramDef['default']);
          ref = this.params[name] = new ProcessParam(this, name, value);
        } else if (paramDef.defer === true) {
          ref = this.params[name] = new ProcessParam(this, name);
        }
      } else {
        var actionDef = this.definition.findActionDef(name);
        if (actionDef) {
          this.actions[name] = new ActionInstance(actionDef, this);
          ref = this.actions[name];
        } else if (this.parentProc) {
          ref = this.parentProc.resolveRef(name);
        }
      }
    }
    return ref;
  }
  ,

  evaluate : function(expr, callback) {
    callback = afasync.responder(callback);
    var vars = EL.extractVars(expr) || [];
    var _this = this;
    afasync.map(
      vars,
      function(v, i, vars, cb) {
        cb = afasync.responder(cb);
        var ref = _this.resolveRef(v);
        if (ref) {
          ref.evaluate({
            onSuccess : function(result) {
              cb.onSuccess({
                name : v,
                value : result.value,
                timestamp : result.timestamp
              })
            },
            onFailure : cb.onFailure
          })
        } else {
          cb.onSuccess(null);
        }
      }
      ,
      {
        onSuccess : function(results) {
          var timestamp = 0;
          var context = {};
          lng.forEach(results, function(result) {
            if (!result) return;
            timestamp = result.timestamp>timestamp ? result.timestamp : timestamp;
            context[result.name] = result.value;
          })
          var value = EL.evaluate(context, expr);
          callback.onSuccess({ timestamp : timestamp, value : value });
        }
        ,
        onFailure : callback.onFailure
      }
    )
  }
  ,

  onValueChange : function(name, value) {
    this.fire('refreshed', name, value);
  }
  ,

  clear : function() {
    lng.forEach(lng.values(this.actions), function(action) {
      action.clear();
    })
    lng.forEach(lng.values(this.params), function(param) {
      param.clear();
    })
  }
  ,

  destroy : function() {
    lng.forEach(lng.values(this.actions), function(action) {
      action.destroy();
    })
  }

}, Broadcaster /* superclass */);


/**
 * ProcessParam
 */
var ProcessParam = afrous.ProcessParam = lng.defineClass({
  // callbackQueue : Responder[]
  // process : ProcessInstance
  // name : String
  // value : Object
  // timestamp : integer

  initialize : function(process, name, value) {
    this.process = process;
    this.name = name;
    this.callbackQueue = [];
    if (typeof value != 'undefined') this.setValue(value);
  }
  ,

  setValue : function(value) {
    var oldValue = this.value;
    // ignore when same value set
    if (value !== oldValue &&
        (lng.isDelayed(value) || lng.isDelayed(oldValue) ||
         lng.toJSON(value) !== lng.toJSON(oldValue))) {
      this.value = value;
      this.timestamp = new Date().getTime();
      this.process.fire('setparam', this.name, this.value, oldValue);
      this.dispatch();
    }
  }
  ,

  checkUpdated : function(timestamp) {
    return !timestamp || this.timestamp > timestamp;
  }
  ,

  evaluate : function(callback) {
    this.callbackQueue.push(callback);
    this.dispatch();
  }
  ,

  dispatch : function() {
    if (this.timestamp) {
      while(this.callbackQueue.length>0) {
        try {
          var callback = this.callbackQueue.pop();
          afasync.stack(callback.onSuccess)
                 .call(callback, { value : this.value, timestamp : this.timestamp });
        } catch(e) {
          debug.log(e)
        }
      }
    }
  }
  ,

  clear : function() {
    this.timestamp = 0;
  }

});
/**
 * ActionInstance
 */
var ActionInstance = afrous.ActionInstance = lng.defineClass({
  // definition : ActionDef
  // process : ProcessInstance
  // callbackQueue : Responder[]
  // status : String
  // value : Object
  // timestamp : integer

  initialize : function(definition, process) {
    // superclass init
    Broadcaster.prototype.initialize.apply(this);

    this.definition = definition;
    this.process = process;
    this.callbackQueue = [];
    this.status = ActionInstance.Status.WAIT;

    this.addListener('change', this.process.onValueChange, this.process);
    this.definition.addListener('change', this.clear, this);
    this.definition.addListener('destroy', this.destroy, this);
    if (debug.on) {
      this.addListener('status', this.statusDebug, this);
    }

  }
  ,

  checkUpdated : function(timestamp) {
    return !this.timestamp || this.timestamp > timestamp ||
           this.process.checkUpdated(this.definition.inputs, this.timestamp);
  }
  ,

  prepareInnerParams : function(callback) {
    callback = afasync.responder(callback);
    var uaction = this.definition.uaction;
    var request = new ActionRequest(this);
    uaction.prepare(request, {
      onSuccess : function() {
        uaction.prepareInnerParams(request, callback);
      },
      onFailure : callback.onFailure
    });
  }
  ,

  evaluate : function(callback) {
    this.callbackQueue.push(callback);
    if (this.status == ActionInstance.Status.WAIT) {
      this.changeStatus(ActionInstance.Status.ACTIVE);
      afasync.stack(this.prepare).call(this, new ActionRequest(this));
    }
  }
  ,

  prepare : function(request) {
    this.changeStatus(ActionInstance.Status.PREPARE)

    var _this = this;
    this.definition.uaction.prepare(request, afrous.async.responder({
      onSuccess : function() {
        if (!_this.definition.uaction.nocaching &&
             _this.timestamp && request.timestamp < _this.timestamp) {
          // using previous cache
          _this.dispatch(request);
        } else {
          _this.execute(request);
        }
      },
      onFailure : function(error) {
        _this.error(request, error)
      }
    }))
  }
  ,

  execute : function(request) {
    this.changeStatus(ActionInstance.Status.EXECUTE);

    var _this = this;
    try {
      this.definition.uaction.execute(request, afrous.async.responder({
        onSuccess : function(value) {
          _this.value = value;
          _this.timestamp = new Date().getTime();
          _this.fire('change', _this.definition.name, _this.value);
          _this.dispatch(request);
        },
        onFailure : function(error) {
          _this.error(request, error)
        }
      }));
    } catch (e) {
      this.error(request, e);
    }
  }
  ,

  dispatch : function(request) {
    this.changeStatus(ActionInstance.Status.DISPATCH);

    while(this.callbackQueue.length>0) {
      try {
        var callback = this.callbackQueue.pop();
        afasync.stack(this.definition.uaction.waittime ?
                      lng.delay(callback.onSuccess, this.definition.uaction.waittime) :
                      callback.onSuccess)
               .call(callback, { value : this.value, timestamp : this.timestamp });
      } catch(e) {
        debug.log(e)
      }
    }

    this.changeStatus(ActionInstance.Status.WAIT);
  }
  ,

  error : function(request, error) {
    this.changeStatus(ActionInstance.Status.ERROR);

    error = new ActionError(this.definition.name, error);
    while(this.callbackQueue.length>0) {
      var callback = this.callbackQueue.pop();
      afasync.stack(this.definition.uaction.waittime ?
                    lng.delay(callback.onFailure, this.definition.uaction.waittime) :
                    callback.onFailure)
             .call(callback, error);
    }

    this.changeStatus(ActionInstance.Status.WAIT);
  }
  ,

  clear : function() {
    this.timestamp = 0;
  }
  ,

  changeStatus : function(status) {
    this.status = status;
    this.fire('status', status);
  }
  ,

  statusDebug : function(status) {
    debug.log(this.definition.name + ': ' + this.status);
  }
  ,

  destroy : function() {
    this.definition.removeListener('change', this.clear);
    this.definition.removeListener('destroy', this.destroy);
    delete this.process.actions[this.definition.name];
  }

}, Broadcaster /* superclass */)


ActionInstance.Status = {
  WAIT : 'WAIT',
  ACTIVE : 'ACTIVE',
  PREPARE : 'PREPARE',
  EXECUTE : 'EXECUTE',
  DISPATCH : 'DISPATCH',
  ERROR : 'ERROR'
}
/**
 * ActionRequest
 */
var ActionRequest = afrous.ActionRequest = lng.defineClass({
  // action : ActionInstance
  // timestamp : Number
  // params : Hash(String, Object)

  initialize : function(action) {
    this.action = action;
    this.params = {};
    this.timestamp = 0;
  }
  ,

  readParam : function(name, next) {
    var obj = {};
    obj[name] = this.action.definition.inputs[name];
    this.read(obj, next);
  }
  ,

  readAll : function(next) {
    var inputs = this.action.definition.inputs;
    if (inputs) {
      this.read(inputs, next);
    } else {
      next.onSuccess();
    }
  }
  ,

  read : function(obj, next) {
    var _this = this;
    this.action.process.evaluate(obj, {
      onSuccess : function(result) {
        if (!_this.timestamp || result.timestamp > _this.timestamp) {
          _this.timestamp = result.timestamp;
        }
        for (var name in result.value) {
          var value = result.value[name];
          var inputDef = _this.action.definition.uaction.inputs[name];
          _this.params[name] = inputDef ?  lng.cast(inputDef.type, value) : value;
        }
        next.onSuccess();
      },
      onFailure : next.onFailure
    })
  }

})

/* ---------------------------------------------------------------------- */

/**
 * ActionError
 */
var ActionError = afrous.ActionError = lng.defineClass({

  initialize : function(name, error) {
    this.name = name;
    this.error = error;
  },

  getStackTrace : function() {
    return lng.isArray(this.error) ? ActionError.getStackTrace(this.error) : [ this ];
  },

  toString : function() {
    return this.error ? this.error.toString() : ''
  }

});

ActionError.getStackTrace = function(errors) {
  var stacks = [];
  lng.forEach(errors, function(e) {
    if (typeof e.getStackTrace=='function') {
      Array.prototype.push.apply(stacks, e.getStackTrace())
    }
  });
  return stacks;
}

ActionError.printStackTrace = function(errors) {
  var stacks = ActionError.getStackTrace(errors);
  return lng.map(stacks, function(stack) {
    return '<strong>' + stack.name + '</strong> : ' +
      afstr.escapeHTML(lng.toJSON(stack.error));
  }).join('<br/>');
}

/* ---------------------------------------------------------------------- */

/**
 * EL
 * Expression Language evaluater class.
 */
var EL = afrous.EL = {

  EXPR_REF_REGEXP : /\${([^}]*)}/g,
  EXPR_REF_STRICT_REGEXP : /^\${([^}]*)}$/g,
  VARIABLE_REGEXP : /(^|[^\w\"\'\.])(([a-zA-Z_]\w*)(\.[a-zA-Z_]\w*|\[(\'(\\\'|[^\'])*\'|\"(\\\"|[^\"])*\"|\d+)\])*)/g,

  /**
   * Recursively scanning all properties in the given object
   * and extract variable reference in the ${...} expression.
   */
  extractVars : function(obj, includesAccesssor) {
    var vars = {};
    lng.scan(obj || '', function(o) {
      if (lng.isString(o)) {
        _extractVars(o);
        return true;
      }
    });
    return lng.keys(vars);

    // ad-hoc program, maybe not lexically valid for all type of inputs ...
    function _extractVars(str) {
      var m = str.match(EL.EXPR_REF_REGEXP);
      if (m) {
        m.join('').replace(EL.VARIABLE_REGEXP, function(_0, _1, _2, _3) {
          vars[includesAccesssor ? _2 : _3] = true;
        });
      }
    }
  }
  ,

  /**
   * Evaluate object and it's properties in the given context;
   * ${...} expressions in the object property will be resolved and evaluated in the context.
   */
  evaluate: function(ctx, obj) {
    var fn = function(){};
    fn.prototype = EL.fn;
    context = lng.extend(new fn(), ctx);

    return lng.scan(obj, function(o) {
      if (lng.isString(o)) return _evaluate(o);
    });

    function _evaluate(str) {
      try {
        if (str.match(EL.EXPR_REF_STRICT_REGEXP)) {
          var expr = RegExp.$1;
          var v = compile(expr).call(ctx, context);
          return typeof v == 'undefined' ? null : v;
        } else {
          return str.replace(EL.EXPR_REF_REGEXP, function(s, p1) {
            var v;
            try {
              v = compile(p1).call(ctx, context);
            } catch (e) {
              v = '';
            }
            return typeof v == 'undefined' || v===null ? '' : v.toString()
          })
        }
      } catch (e) {
        return null
      }
    }

    function compile(expr) {
      return (
        EL._compiledCache[expr] ||
        (EL._compiledCache[expr] = new Function('c', 'with(c) return ('+expr+');'))
      );
    }
  }
  ,

  /**
   * Set value specified path in the context.
   */
  setValueInPath : function(ctx, path, value) {
    var obj = ctx;
    path = this.splitPath(path);
    if (path && path.length>0) {
      for (var i=0; i<path.length-1; i++) {
        var prop = path[i];
        obj = obj[prop] = obj[prop] || (typeof path[i+1] == 'number' ? [] : {});
      }
    }
    obj[path[path.length-1]] = value;
  }
  ,


  /**
   * Much safer value accessor in context, no-evaluation
   */
  getValueInPath : function(ctx, path) {
    var v = ctx;
    path = this.splitPath(path);
    if (path && path.length>0) {
      for (var i=0; i<path.length; i++) {
        var prop = path[i];
        if (!v) return;
        v = v[prop];
      }
    }
    return v;
  }
  ,


  /**
   *
   */
  splitPath : function(path) {
    var arr = [];
    var prop;
    while (path.length>0) {
      var m = path.match(/([a-zA-Z_]\w*)(\.|\[(\"([^\"]|\\\")*\")\]\.?|\[(0|[1-9]\d*)\]\.?|$)/);
      if (!m) return null;
      var prop = m[1];
      if (prop != "this") arr.push(prop);
      if (m[2] == "." || m[2] == "") {
        path = path.substring(m[0].length);
      } else {
        prop = lng.parseJSON(m[3] || m[5]);
        arr.push(prop);
        path = path.substring(m[0].length);
      }
    }
    return arr;
  }
  ,

  /**
   * Utility Functions that can be used in EL
   */
  fn : {

    TODAY : function() {
      return afstr.formatDate(new Date());
    }
    ,

    NOW : function() {
      return afstr.formatDateTime(new Date());
    }
    ,

    ADD_DATE : function(d, num) {
      d = afstr.parseDate(d);
      return afstr.formatDate(new Date(d.getTime()+86400000*num));
    }
    ,

    ESC : function(s) {
      return afstr.escapeHTML(s);
    }
    ,

    ENC : function(s) {
      return encodeURIComponent(s);
    }

  }
  ,

  /**
   *
   */
  _compiledCache : {}

}

/* ---------------------------------------------------------------------- */

/**
 *
 */
var ProcessResultSet = afrous.ProcessResultSet = function(process, options) {

  var recset;

  switch (process.definition.schema.type) {
    case 'entryList' :
      recset = new EntryListRecordSet(process, options);
      break;
    case 'infEntryList' :
      recset = new InfEntryListRecordSet(process, options);
      break;
    case 'simple' :
    default :
      recset = new SimpleRecordSet(process, options);
      break;
  }

  var rset = new afiterator.BufferedIterator(recset);
  process.definition.addListener('change', function() { rset.requestRefresh() });
  process.addListener('setparam', function() {
    if (!recset.suspendChange) { rset.requestRefresh() }
  });

  return rset;
}

/**
 *
 */
var SimpleRecordSet = lng.defineClass({
  /**
   *
   */
  initialize : function(process, options) {
    this.process = process;
    this.options = options || {};
  }
  ,

  /**
   *
   */
  _processFetch : function(callback) {
    callback = afasync.responder(callback);
    if (this.options.output) {
      afasync.stack(this.process.evaluate).call(this.process, this.options.output,
        callback.extend(function(res) { handleResponse.call(this, res.value) }, this));
    } else {
      afasync.stack(this.process.start).call(this.process, callback.extend(handleResponse, this));
    }

    function handleResponse(res) {
      this.resultIter = lng.isIterator(res) ? res :
                        lng.isArray(res) ? afiterator.fromArray(res) :
                        afiterator.fromArray([ res ]);
      this.resultIter.reset(); // this is for the case that its previously referenced and not reseted
      callback.onSuccess();
    }
  }
  ,

  /**
   *
   */
  next : function(callback) {
    callback = afasync.responder(callback);
    if (this.resultIter) {
      afasync.stack(this.resultIter.next).call(this.resultIter, callback);
    } else {
      this._processFetch(callback.extend(function() {
        afasync.stack(this.resultIter.next).call(this.resultIter, callback);
      }, this));
    }
  }
  ,

  /**
   *
   */
  hasNext : function(callback) {
    callback = afasync.responder(callback);
    if (this.resultIter) {
      afasync.stack(this.resultIter.hasNext).call(this.resultIter, callback);
    } else {
      this._processFetch(callback.extend(function(records) {
        afasync.stack(this.resultIter.hasNext).call(this.resultIter, callback);
      }, this));
    }
  }
  ,

  /**
   *
   */
  getTotalCount : function() {
    var totalCount;
    if (this.resultIter) totalCount = this.resultIter.getTotalCount();
    return totalCount;
  }
  ,

  /**
   *
   */
  reset : function(force) {
    if (this.resultIter) this.resultIter.reset();
    delete this.resultIter;
  }

}, Iterator); // end of class SimpleRecordSet

/**
 *
 */
var EntryListRecordSet = lng.defineClass({
  /**
   *
   */
  initialize : function(process, options) {
    this.process = process;
    this.options = options || {};
    this.start = 0;
    this.done = false;
  }
  ,

  /**
   *
   */
  next : function(callback) {
    callback = afasync.responder(callback);
    if (this.done) {
      callback.onSuccess(); // return undefined value
    } else {
      var process = this.process;
      this.suspendChange = true;
      process.setParam('start', this.start);
      if (this.options.fetchlimit) process.setParam('limit', this.options.fetchlimit);
      this.suspendChange = false;
      afasync.stack(process.start).call(process, 'entries', callback.extend(function(entries) {
        this.start += entries.length;
        this.done = entries.length == 0;
        // delay callback to ensure the process evaluation dispatch to complete.
        lng.delay(callback.onSuccess,0).call(callback, entries);
      }, this));
    }
  }
  ,

  /**
   *
   */
  supportSkip : function() {
    return true;
  }
  ,

  /**
   *
   */
  skip : function(count, callback) {
    callback = afasync.responder(callback);
    if (this.done) {
      callback.onSuccess(0);
    } else {
      this.start += count;
      callback.onSuccess(count);
    }
  }
  ,

  /**
   *
   */
  hasNext : function(callback) {
    callback = afasync.responder(callback);
    if (this.done) {
      callback.onSuccess(false);
    } else {
      var process = this.process;
      this.suspendChange = true;
      process.setParam('start', this.start);
      if (this.options.fetchlimit) process.setParam('limit', this.options.fetchlimit);
      this.suspendChange = false;
      afasync.stack(process.start).call(process, 'totalCount', callback.extend(
        function(totalCount) {
          this.totalCount = totalCount;
          // delay callback to ensure the process evaluation dispatch to complete.
          lng.delay(callback.onSuccess,0).call(callback, this.start < totalCount);
        }, this));
    }
  }
  ,

  /**
   *
   */
  getTotalCount : function() {
    return this.totalCount;
  }
  ,

  /**
   *
   */
  reset : function(force) {
    this.start = 0;
    this.done = false;
    delete this.totalCount;
  }


}, Iterator); // end of class EntryListRecordSet


/**
 *
 */
var InfEntryListRecordSet = lng.defineClass({

  /**
   *
   */
  initialize : function(process) {
    this.process = process;
    this.done = false;
    this.doneInNext = false;
    this.totalCount = 0;
  }
  ,

  /**
   *
   */
  next : function(callback) {
    callback = afasync.responder(callback);
    if (this.done) {
      callback.onSuccess();
    } else {
      this.suspendChange = true;
      this.process.setParam('cursor', this.cursor || null);
      this.suspendChange = false;
      afasync.stack(this.process.start).call(this.process, callback.extend(function(res) {
        this.cursor = res.next;
        this.done = this.doneInNext || lng.isNullOrUndefined(this.cursor);
        this.totalCount += res.entries ? res.entries.length : 0;
        // delay callback to ensure the process evaluation dispatch to complete.
        lng.delay(callback.onSuccess,0).call(callback, res.entries || []);
      }, this));
    }
  }
  ,

  /**
   *
   */
  hasNext : function(callback) {
    callback = afasync.responder(callback);
    if (this.done) {
      callback.onSuccess(false);
    } else {
      this.suspendChange = true;
      this.process.setParam('cursor', this.cursor || null);
      this.suspendChange = false;
      this.process.start('next', callback.extend(function(next) {
        this.doneInNext = lng.isNullOrUndefined(next);
        // delay callback to ensure the process evaluation dispatch to complete.
        lng.delay(callback.onSuccess,0).call(callback, !this.done);
      }, this));
    }
  }
  ,

  /**
   *
   */
  getTotalCount : function() {
    var totalCount;
    if (this.done) totalCount = this.totalCount;
    return totalCount;
  }
  ,

  /**
   *
   */
  reset : function(force) {
    this.cursor = undefined;
    this.done = false;
    this.doneInNext = false;
    this.totalCount = 0;
    // this.process.clear();
  }

}, Iterator);

/* ---------------------------------------------------------------------- */

/**
 * afrous.config
 */
afrous.config = lng.extend({
  AFROUS_BASE_URL : lng.scriptBaseUrl.split('/').slice(0, -1).join('/'),
  METADATA_BASE_URL : 'https://metadata.afrous.net',
  APP_BASE_URL : 'https://app.afrous.net'
}, window.afConfig || {});

/* ---------------------------------------------------------------------- */

window.afrous = afrous;

})(
window.afrous || (window.afrous = {}),
function(expr, context) {
  if (context) {
    with(context) return eval(expr);
  } else {
    return eval(expr);
  }
});
