/*
 * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
 * Digest Algorithm, as defined in RFC 1321.
 * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002.
 * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
 * Distributed under the BSD License
 * See http://pajhome.org.uk/crypt/md5 for more info.
 */

/*
 * Configurable variables. You may need to tweak these to be compatible with
 * the server-side, but the defaults work in most cases.
 */
var hexcase = 0;  /* hex output format. 0 - lowercase; 1 - uppercase        */
var b64pad  = ""; /* base-64 pad character. "=" for strict RFC compliance   */
var chrsz   = 8;  /* bits per input character. 8 - ASCII; 16 - Unicode      */

/*
 * These are the functions you'll usually want to call
 * They take string arguments and return either hex or base-64 encoded strings
 */
function hex_md5(s){ return binl2hex(core_md5(str2binl(s), s.length * chrsz));}
function b64_md5(s){ return binl2b64(core_md5(str2binl(s), s.length * chrsz));}
function str_md5(s){ return binl2str(core_md5(str2binl(s), s.length * chrsz));}
function hex_hmac_md5(key, data) { return binl2hex(core_hmac_md5(key, data)); }
function b64_hmac_md5(key, data) { return binl2b64(core_hmac_md5(key, data)); }
function str_hmac_md5(key, data) { return binl2str(core_hmac_md5(key, data)); }

/*
 * Perform a simple self-test to see if the VM is working
 */
function md5_vm_test()
{
  return hex_md5("abc") == "900150983cd24fb0d6963f7d28e17f72";
}

/*
 * Calculate the MD5 of an array of little-endian words, and a bit length
 */
function core_md5(x, len)
{
  /* append padding */
  x[len >> 5] |= 0x80 << ((len) % 32);
  x[(((len + 64) >>> 9) << 4) + 14] = len;

  var a =  1732584193;
  var b = -271733879;
  var c = -1732584194;
  var d =  271733878;

  for(var i = 0; i < x.length; i += 16)
  {
    var olda = a;
    var oldb = b;
    var oldc = c;
    var oldd = d;

    a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
    d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
    c = md5_ff(c, d, a, b, x[i+ 2], 17,  606105819);
    b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
    a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
    d = md5_ff(d, a, b, c, x[i+ 5], 12,  1200080426);
    c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
    b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
    a = md5_ff(a, b, c, d, x[i+ 8], 7 ,  1770035416);
    d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
    c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
    b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
    a = md5_ff(a, b, c, d, x[i+12], 7 ,  1804603682);
    d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
    c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
    b = md5_ff(b, c, d, a, x[i+15], 22,  1236535329);

    a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
    d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
    c = md5_gg(c, d, a, b, x[i+11], 14,  643717713);
    b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
    a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
    d = md5_gg(d, a, b, c, x[i+10], 9 ,  38016083);
    c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
    b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
    a = md5_gg(a, b, c, d, x[i+ 9], 5 ,  568446438);
    d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
    c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
    b = md5_gg(b, c, d, a, x[i+ 8], 20,  1163531501);
    a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
    d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
    c = md5_gg(c, d, a, b, x[i+ 7], 14,  1735328473);
    b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);

    a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
    d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
    c = md5_hh(c, d, a, b, x[i+11], 16,  1839030562);
    b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
    a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
    d = md5_hh(d, a, b, c, x[i+ 4], 11,  1272893353);
    c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
    b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
    a = md5_hh(a, b, c, d, x[i+13], 4 ,  681279174);
    d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
    c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
    b = md5_hh(b, c, d, a, x[i+ 6], 23,  76029189);
    a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
    d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
    c = md5_hh(c, d, a, b, x[i+15], 16,  530742520);
    b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);

    a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
    d = md5_ii(d, a, b, c, x[i+ 7], 10,  1126891415);
    c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
    b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
    a = md5_ii(a, b, c, d, x[i+12], 6 ,  1700485571);
    d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
    c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
    b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
    a = md5_ii(a, b, c, d, x[i+ 8], 6 ,  1873313359);
    d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
    c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
    b = md5_ii(b, c, d, a, x[i+13], 21,  1309151649);
    a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
    d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
    c = md5_ii(c, d, a, b, x[i+ 2], 15,  718787259);
    b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);

    a = safe_add(a, olda);
    b = safe_add(b, oldb);
    c = safe_add(c, oldc);
    d = safe_add(d, oldd);
  }
  return Array(a, b, c, d);

}

/*
 * These functions implement the four basic operations the algorithm uses.
 */
function md5_cmn(q, a, b, x, s, t)
{
  return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b);
}
function md5_ff(a, b, c, d, x, s, t)
{
  return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
}
function md5_gg(a, b, c, d, x, s, t)
{
  return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
}
function md5_hh(a, b, c, d, x, s, t)
{
  return md5_cmn(b ^ c ^ d, a, b, x, s, t);
}
function md5_ii(a, b, c, d, x, s, t)
{
  return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
}

/*
 * Calculate the HMAC-MD5, of a key and some data
 */
function core_hmac_md5(key, data)
{
  var bkey = str2binl(key);
  if(bkey.length > 16) bkey = core_md5(bkey, key.length * chrsz);

  var ipad = Array(16), opad = Array(16);
  for(var i = 0; i < 16; i++)
  {
    ipad[i] = bkey[i] ^ 0x36363636;
    opad[i] = bkey[i] ^ 0x5C5C5C5C;
  }

  var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz);
  return core_md5(opad.concat(hash), 512 + 128);
}

/*
 * Add integers, wrapping at 2^32. This uses 16-bit operations internally
 * to work around bugs in some JS interpreters.
 */
function safe_add(x, y)
{
  var lsw = (x & 0xFFFF) + (y & 0xFFFF);
  var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
  return (msw << 16) | (lsw & 0xFFFF);
}

/*
 * Bitwise rotate a 32-bit number to the left.
 */
function bit_rol(num, cnt)
{
  return (num << cnt) | (num >>> (32 - cnt));
}

/*
 * Convert a string to an array of little-endian words
 * If chrsz is ASCII, characters >255 have their hi-byte silently ignored.
 */
function str2binl(str)
{
  var bin = Array();
  var mask = (1 << chrsz) - 1;
  for(var i = 0; i < str.length * chrsz; i += chrsz)
    bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32);
  return bin;
}

/*
 * Convert an array of little-endian words to a string
 */
function binl2str(bin)
{
  var str = "";
  var mask = (1 << chrsz) - 1;
  for(var i = 0; i < bin.length * 32; i += chrsz)
    str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask);
  return str;
}

/*
 * Convert an array of little-endian words to a hex string.
 */
function binl2hex(binarray)
{
  var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
  var str = "";
  for(var i = 0; i < binarray.length * 4; i++)
  {
    str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) +
           hex_tab.charAt((binarray[i>>2] >> ((i%4)*8  )) & 0xF);
  }
  return str;
}

/*
 * Convert an array of little-endian words to a base-64 string
 */
function binl2b64(binarray)
{
  var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
  var str = "";
  for(var i = 0; i < binarray.length * 4; i += 3)
  {
    var triplet = (((binarray[i   >> 2] >> 8 * ( i   %4)) & 0xFF) << 16)
                | (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 )
                |  ((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF);
    for(var j = 0; j < 4; j++)
    {
      if(i * 8 + j * 6 > binarray.length * 32) str += b64pad;
      else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F);
    }
  }
  return str;
}

// OneCode Application Framework | Copyright (C) 2005-2011 Radek Tetik | www.onecode.cz

Prototype.Browser.IE6 = Prototype.Browser.IE && parseInt(navigator.userAgent.substring(navigator.userAgent.indexOf("MSIE")+5)) == 6;
Prototype.Browser.IE7 = Prototype.Browser.IE && parseInt(navigator.userAgent.substring(navigator.userAgent.indexOf("MSIE")+5)) == 7;
Prototype.Browser.IE8 = Prototype.Browser.IE && !Prototype.Browser.IE6 && !Prototype.Browser.IE7;

/**
* Inheritance, traits, dynamic loading etc.
*/
var Wsf =
{
    /**
    * From the book Pro Javascript Design Patterns, page 43.
    */
    extend: function (subClass, superClass)
    {
        var F = function() {};
        F.prototype = superClass.prototype;
        subClass.prototype = new F();
        subClass.prototype.constructor = subClass;
        subClass.parent = superClass.prototype;
    },

    use: function (class_, trait)
    {
        for (var o in trait) class_.prototype[o] = trait[o];
    },

    requireClass: function (className)
    {
        if (eval("typeof "+className) != "undefined") return;

        var url = className.replace(/\_/g, '/');

        Wsf.requireScript("/"+url.substr(1)+".js");
    },

    requireScript: function (file)
    {
        var code = false;

        new Ajax.Request(file, {
            method: "get",
            asynchronous: false,
            onSuccess: function(transport) { code = transport.responseText; }
        });

        if (code === false) {
            alert('Error loading file '+file+'.\n\nThe application will restart.');
            window.location.reload();
            return;
        }

        var script = document.createElement('script');
        script.type = 'text/javascript';
        script.text = code;
        document.getElementsByTagName("head")[0].appendChild(script);
    },

    requireStylesheet: function (file)
    {
        var code = false;

        var r = new Ajax.Request(file, {
            method: "get",
            asynchronous: false,
            onSuccess: function(transport) { code = transport.responseText; }
        });

        if (code === false) {
            alert('Error loading stylesheet '+file+'.\n\nThe application will restart.');
            window.location.reload();
            return;
        }

        var e = document.createElement('style');
        e.setAttribute('type', 'text/css');

        if (e.styleSheet)
            e.styleSheet.cssText = code;
        else
            e.appendChild(document.createTextNode(code));

        document.getElementsByTagName("head")[0].appendChild(e);
    },

    isObjectEmpty: function (o)
    {
        for(var p in o) {
            if (o[p] != o.constructor.prototype[p]) return false;
        }
        return true;
    },

    urldecode : function (s)
    {
        return unescape(s.replace(/\+/g, " "));
    }
}

/**
 * @todo: move to file
 */
Wsf.Number = {
    exp10: [1,10,100,1000,1000],

    /**
     * @param value
     * @param precision
     */
    roundHalfUp: function(value, precision)
    {
        var f = Wsf.Number.exp10[precision];
        return Math.round(value * f) / f;
    },

    addPercentage: function (value, percentage, precision)
    {
        return Wsf.Number.roundHalfUp(value * (1+(percentage/100)), precision);
    },

    subPercentage: function (value, percentage, precision)
    {
        return Wsf.Number.roundHalfUp(value * (1-(percentage/100)), precision);
    },

    /**
     * Formats a number using locale conventions
     * @todo: localization
     * @param value
     * @param precision
     * @return string
     */
    format: function (value, precision)
    {
        if (value===null) return '';
        var s = value.toFixed(precision);
        var p = s.split('.');
        var a = p[0];
        var r = '';
        for (var i=0,len=a.length; i < len; ++i) {
            if (i > 0 && (len-i) % 3==0) r += ' ';
            r += a.charAt(i);
        }
        if (p.length > 1) r += ',' + p[1];
        return r;
    }
}


/**
* DOM element extensions
*/
var WsfElement =
{
    /**
    * Sets the position and optionally sets the content size.
    */
    move: function(element,x,y,w,h) {
        element.style.left = x + "px";
        element.style.top = y + "px";
        if (w !== null) element.style.width = w + "px";
        if (h !== null) element.style.height = h + "px";
    },

    /**
    * Sets the content size (=not including padding and border)
    */
    setContentSize: function(element,w,h) {
        if (w !== null) element.style.width = w + "px";
        if (h !== null) element.style.height = h + "px";
    },

    getTextContent: function (element) {
        return Prototype.Browser.IE ? element.innerText : element.textContent;
    },

    setTextContent: function (element, text) {
        if (Prototype.Browser.IE)
            element.innerText = text;
        else
            element.textContent = text;
    }
}

Element.addMethods(WsfElement);

/**
* Events trait
*/
var Wsf_EventsTrait = {
    attachEventHandler: function (eventName, fnCallback)
    {
        if (this._events[eventName] === undefined) { this._events[eventName] = []; }
        this._events[eventName].push(fnCallback);
    },

    detachEventHandler: function (eventName, fnCallback)
    {
        var a = this._events[eventName];
        for (var i=0; i < a.length; ++i) {
            if (a[i] == fnCallback) {
                a.splice(i,1);
                break;
            }
        }
    },

    /**
    * @param eventData    an object { sender } plus custom properties
    */
    raiseEvent: function (eventName, eventData)
    {
        if (this._eventsDisabled || this._events[eventName] === undefined) return;
        if (!eventData) eventData = {};
        eventData.sender = this;

        for (var i=this._events[eventName].length; i--;) {
            this._events[eventName][i](eventData);
        }
    },

    enableEvents: function()
    {
        --this._eventsDisabled;
        return this;
    },

    disableEvents: function()
    {
        ++this._eventsDisabled;
        return this;
    },

    /**
     * Checks if there is any listener for the given event
     * @param string eventName
     * @return bool
     */
    isEventListener: function(eventName)
    {
        return this._events[eventName] !== undefined;
    }
};

/**
* Formatting helpers
*/
Wsf.Format = {
    createEnumRenderer : function (data) {
        return function (value) {
            return data[value];
        }
    },

    createBoolRenderer : function (trueText, falseText) {
        return function (value) {
            return value ? trueText : falseText;
        }
    },

    /**
     * @param precision
     * @return function
     */
    createNumberRenderer : function (precision) {
        return function (value) {
            return Wsf.Number.format(value, precision);
        }
    }
}

// Return a formatted string
//
// version: 909.322
// discuss at: http://phpjs.org/functions/sprintf
// +   original by: Ash Searle (http://hexmen.com/blog/)
// + namespaced by: Michael White (http://getsprink.com)
// +    tweaked by: Jack
// +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
// +      input by: Paulo Ricardo F. Santos
// +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
// +      input by: Brett Zamir (http://brett-zamir.me)
// +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
// *     example 1: sprintf("%01.2f", 123.1);
// *     returns 1: 123.10
// *     example 2: sprintf("[%10s]", 'monkey');
// *     returns 2: '[    monkey]'
// *     example 3: sprintf("[%'#10s]", 'monkey');
// *     returns 3: '[####monkey]'
Wsf.sprintf = function ( )
{
    var regex = /%%|%(\d+\$)?([-+\'#0 ]*)(\*\d+\$|\*|\d+)?(\.(\*\d+\$|\*|\d+))?([scboxXuidfegEG])/g;
    var a = arguments, i = 0, format = a[i++];

    // pad()
    var pad = function (str, len, chr, leftJustify) {
        if (!chr) {chr = ' ';}
        var padding = (str.length >= len) ? '' : Array(1 + len - str.length >>> 0).join(chr);
        return leftJustify ? str + padding : padding + str;
    };

    // justify()
    var justify = function (value, prefix, leftJustify, minWidth, zeroPad, customPadChar) {
        var diff = minWidth - value.length;
        if (diff > 0) {
            if (leftJustify || !zeroPad) {
                value = pad(value, minWidth, customPadChar, leftJustify);
            } else {
                value = value.slice(0, prefix.length) + pad('', diff, '0', true) + value.slice(prefix.length);
            }
        }
        return value;
    };

    // formatBaseX()
    var formatBaseX = function (value, base, prefix, leftJustify, minWidth, precision, zeroPad) {
        // Note: casts negative numbers to positive ones
        var number = value >>> 0;
        prefix = prefix && number && {'2': '0b', '8': '0', '16': '0x'}[base] || '';
        value = prefix + pad(number.toString(base), precision || 0, '0', false);
        return justify(value, prefix, leftJustify, minWidth, zeroPad);
    };

    // formatString()
    var formatString = function (value, leftJustify, minWidth, precision, zeroPad, customPadChar) {
        if (precision != null) {
            value = value.slice(0, precision);
        }
        return justify(value, '', leftJustify, minWidth, zeroPad, customPadChar);
    };

    // doFormat()
    var doFormat = function (substring, valueIndex, flags, minWidth, _, precision, type) {
        var number;
        var prefix;
        var method;
        var textTransform;
        var value;

        if (substring == '%%') {return '%';}

        // parse flags
        var leftJustify = false, positivePrefix = '', zeroPad = false, prefixBaseX = false, customPadChar = ' ';
        var flagsl = flags.length;
        for (var j = 0; flags && j < flagsl; j++) {
            switch (flags.charAt(j)) {
                case ' ': positivePrefix = ' '; break;
                case '+': positivePrefix = '+'; break;
                case '-': leftJustify = true; break;
                case "'": customPadChar = flags.charAt(j+1); break;
                case '0': zeroPad = true; break;
                case '#': prefixBaseX = true; break;
            }
        }

        // parameters may be null, undefined, empty-string or real valued
        // we want to ignore null, undefined and empty-string values
        if (!minWidth) {
            minWidth = 0;
        } else if (minWidth == '*') {
            minWidth = +a[i++];
        } else if (minWidth.charAt(0) == '*') {
            minWidth = +a[minWidth.slice(1, -1)];
        } else {
            minWidth = +minWidth;
        }

        // Note: undocumented perl feature:
        if (minWidth < 0) {
            minWidth = -minWidth;
            leftJustify = true;
        }

        if (!isFinite(minWidth)) {
            throw new Error('sprintf: (minimum-)width must be finite');
        }

        if (!precision) {
            precision = 'fFeE'.indexOf(type) > -1 ? 6 : (type == 'd') ? 0 : undefined;
        } else if (precision == '*') {
            precision = +a[i++];
        } else if (precision.charAt(0) == '*') {
            precision = +a[precision.slice(1, -1)];
        } else {
            precision = +precision;
        }

        // grab value using valueIndex if required?
        value = valueIndex ? a[valueIndex.slice(0, -1)] : a[i++];

        switch (type) {
            case 's': return formatString(String(value), leftJustify, minWidth, precision, zeroPad, customPadChar);
            case 'c': return formatString(String.fromCharCode(+value), leftJustify, minWidth, precision, zeroPad);
            case 'b': return formatBaseX(value, 2, prefixBaseX, leftJustify, minWidth, precision, zeroPad);
            case 'o': return formatBaseX(value, 8, prefixBaseX, leftJustify, minWidth, precision, zeroPad);
            case 'x': return formatBaseX(value, 16, prefixBaseX, leftJustify, minWidth, precision, zeroPad);
            case 'X': return formatBaseX(value, 16, prefixBaseX, leftJustify, minWidth, precision, zeroPad).toUpperCase();
            case 'u': return formatBaseX(value, 10, prefixBaseX, leftJustify, minWidth, precision, zeroPad);
            case 'i':
            case 'd':
                number = parseInt(+value, 10);
                prefix = number < 0 ? '-' : positivePrefix;
                value = prefix + pad(String(Math.abs(number)), precision, '0', false);
                return justify(value, prefix, leftJustify, minWidth, zeroPad);
            case 'e':
            case 'E':
            case 'f':
            case 'F':
            case 'g':
            case 'G':
                number = +value;
                prefix = number < 0 ? '-' : positivePrefix;
                method = ['toExponential', 'toFixed', 'toPrecision']['efg'.indexOf(type.toLowerCase())];
                textTransform = ['toString', 'toUpperCase']['eEfFgG'.indexOf(type) % 2];
                value = prefix + Math.abs(number)[method](precision);
                return justify(value, prefix, leftJustify, minWidth, zeroPad)[textTransform]();
            default: return substring;
        }
    };

    return format.replace(regex, doFormat);
}


/**
* Drag and drop helper methods
* Chrome does not allow custom data types. This fixes it.
*/
Wsf.DDHelper = {
    // Data type of the current drag and drop operation
    // Used when the browser does not support custom data types in setData()
    type : null,

    containsType : function (dataTransfer, type)
    {
        if (Prototype.Browser.Gecko) {
            return dataTransfer.types.contains(type);
        }

        return type==Wsf.DDHelper.type;
    },

    getData : function (dataTransfer, type)
    {
        if (Prototype.Browser.Gecko) {
            var s = dataTransfer.getData(type);
            return s ? s.evalJSON() : null;
        }

        if (Wsf.DDHelper.type != type) return null;
        var o = dataTransfer.getData("text");
        if (!o || !o.isJSON()) return null;

        return o && o.isJSON() ? o.evalJSON() : null;
    },

    setData : function (dataTransfer, type, data)
    {
        if (Prototype.Browser.Gecko) {
            dataTransfer.setData(type, Object.toJSON(data));
        }
        else {
            Wsf.DDHelper.type = type;
            dataTransfer.setData("text", Object.toJSON(data));
        }
    }
}

/**
 * HTML5 drop target
 * @param Element element
 * @param function onDragEnter     function(dataTransfer) returning possible drop effects like "all"
 * @param function onDrop          function(dataTransfer)
 */
Wsf.DropTarget = function (element, onDragEnter, onDrop)
{
    this.element = element;
    this.ref = 0;
    this.dropEffect = 0;
    this.onDragEnter = onDragEnter;
    this.onDrop = onDrop;
    element.observe('dragenter', this._onDragEnter.bindAsEventListener(this));
    element.observe('dragleave', this._onDragLeave.bindAsEventListener(this));
    element.observe('dragover', this._onDragOver.bindAsEventListener(this));
    element.observe('drop', this._onDrop.bindAsEventListener(this));
}

/*******************************************************************************/
Wsf.DropTarget.prototype._onDragEnter = function (event)
{
    if (++this.ref == 1) {
        this.dropEffect = this.onDragEnter(event.dataTransfer, event);
        event.dataTransfer.effectAllowed = this.dropEffect;
        if (this.dropEffect != "none") this.element.addClassName('wsfDropTarget');
    }
    event.stop();
}

/*******************************************************************************/
Wsf.DropTarget.prototype._onDragOver = function (event)
{
    event.dataTransfer.effectAllowed = this.dropEffect;
    event.stop();
}

/*******************************************************************************/
Wsf.DropTarget.prototype._onDragLeave = function (event)
{
    if (--this.ref == 0) {
        this.element.removeClassName('wsfDropTarget');
    }
    event.stop();
}

/*******************************************************************************/
Wsf.DropTarget.prototype._onDrop = function (event)
{
    event.stop();
    this.onDrop(event.dataTransfer, event);
    this.element.removeClassName('wsfDropTarget');
    this.ref = 0;
}


/**
* Main application object
*/
function Wsf_App(state)
{
    this._ctrls = {};
    this._rootCtrl = null;
    this.lang = state.lang;
    this._deletedServerCtrls = [];
    this._nextCtrlId = state._nextCtrlId;
    this._nextServerCtrlId = this._nextCtrlId;
    this._ignoreHashChangeEvent = null;

    // ...

    document.observe('dom:loaded', this.onDomReady.bind(this));
}

/*****************************************************************************/
Wsf_App.prototype.onDomReady = function()
{
    //if (Prototype.Browser.IE6) $(document.body).addClassName('ie6');
    //if (Prototype.Browser.Opera) $(document.body).addClassName('opera');
    //if (Prototype.Browser.Gecko) $(document.body).addClassName('ff');

    // Init controls

    var ctrls = this._createCtrlTree();

    for (var id in ctrls) {
        this._ctrls[id] = ctrls[id][0];
    }

    for (var id in ctrls) {
        this._ctrls[id].createFromServerState(ctrls[id][1]);
    }

    this._rootCtrl._makeStateSnapshot();
    this._rootCtrl.initFromMarkup();
    this._rootCtrl.onCreate();

    // Resize

    Event.observe(window, 'resize', this._onResize.bindAsEventListener(this));
    this._onResize();

    // In chrome and IE CSS is not ready in the domready event, so update the the layout in the load event

    Event.observe(window, 'load', this._onResize.bind(this));

    // Start automatic UI update

    this._startUpdateUI();

    window.onunload = function () { this._rootCtrl.walk('onSaveLayout'); }.bind(this);

    this._implementMouseCapture();

    // AJAX history (FF,Chrome,IE8+)

    Event.observe(window, 'hashchange', this._onHashChanged.bind(this));
    this._onHashChanged();
}

/*****************************************************************************/
Wsf_App.prototype._startUpdateUI = function()
{
    if (!this._updateUIExecuter) {
        this._rootCtrl.updateUI();  // update immediatelly to avoid enable/disable flashing
        this._updateUIExecuter = new PeriodicalExecuter(this._rootCtrl.updateUI.bind(this._rootCtrl), .25);
    }
}

/*****************************************************************************/
Wsf_App.prototype._stopUpdateUI = function()
{
    if (this._updateUIExecuter) {
        this._updateUIExecuter.stop();
        this._updateUIExecuter = null;
    }
}

/**
* Sets a cookie.
* The valus is encoded using encodeURIComponent(). You must use urldecode() in PHP.
* @param expireDate       Date object or null for session duration
*/
Wsf_App.prototype.setCookie = function(name, value, expireDate, isGlobal)
{
    var s = name + "=" + encodeURIComponent(value);
    if (expireDate !== null) s += "; expires=" + expireDate.toGMTString();
    if (isGlobal) s += "; path" + "=/";      // to make HTTRACK dizzy ;-)
    document.cookie = s;
}

/*****************************************************************************/
Wsf_App.prototype.getCookie = function (name, defValue)
{
    // cookies are separated by semicolons
    // IE bug: cookies in total can have up-to 4096KB
    var cookies = document.cookie.split("; ");

    for (var i=0; i < cookies.length; i++) {
        // a name/value pair (a crumb) is separated by an equal sign
        var aCrumb = cookies[i].split("=");
        if (name == aCrumb[0]) return unescape(aCrumb[1]);
    }

    // a cookie with the requested name does not exist
    return defValue;
}

/*****************************************************************************/
Wsf_App.prototype.getCookieInt = function (name, defValue)
{
    return parseInt(this.getCookie(name, defValue));
}

/*****************************************************************************/
Wsf_App.prototype.refresh = function ()
{
    window.location.reload();
}

/*****************************************************************************/
Wsf_App.prototype.navigate = function (url)
{
    window.location.href = url;
}

/*****************************************************************************/
Wsf_App.prototype._implementMouseCapture = function()
{
    // TODO: FF4 = Gecko 2.0 has setCapture();
    if (Prototype.Browser.IE) return;

    var capture = ["click", "mousedown", "mouseup", "mousemove", "mouseover", "mouseout" ];

    HTMLElement.prototype.setCapture = function()
    {
        if (this._capture != null) return;

        var self = this;
        var flag = false;

        this._capture = function(e)
        {
            if (flag) return;
            flag = true;

            var event = document.createEvent("MouseEvents");

            event.initMouseEvent(e.type,
                e.bubbles, e.cancelable, e.view, e.detail,
                e.screenX, e.screenY, e.clientX, e.clientY,
                e.ctrlKey, e.altKey, e.shiftKey, e.metaKey,
                e.button, e.relatedTarget);

            self.dispatchEvent(event);
            flag = false;
        };

        for (var i=0; i<capture.length; i++)
        {
            window.addEventListener(capture[i], this._capture, true);
        }
    }

    HTMLElement.prototype.releaseCapture = function()
    {
        if (this._capture == null) return;

        for (var i=0; i<capture.length; i++)
        {
            window.removeEventListener(capture[i], this._capture, true);
        }

        this._capture = null;
    }
}

/*******************************************************************************/
/*
/* CONTROLS
/*
/*******************************************************************************/

/*****************************************************************************/
Wsf_App.prototype.getRootCtrl = function()
{
    return this._rootCtrl;
}

/*****************************************************************************/
Wsf_App.prototype._registerCtrl = function (id, ctrl)
{
    this._ctrls[id] = ctrl;
}

/*****************************************************************************/
Wsf_App.prototype._deregisterCtrl = function (ctrl)
{
    var id = ctrl.getId();
    delete this._ctrls[id];

    if (id.substr(1) < this._nextServerCtrlId) {
        this._deletedServerCtrls.push(id);
    }
}

/*****************************************************************************/
Wsf_App.prototype.getCtrlById = function (id)
{
    return id ? this._ctrls[id] : null;
}

/*******************************************************************************/
/*
/* CMDS
/*
/*******************************************************************************/

/*******************************************************************************/
Wsf_App.prototype.updateUI = function ()
{
    this._rootCtrl.updateUI();
}

/*******************************************************************************/
Wsf_App.prototype.invokeUpdateCmd = function (sourceCtrl, cmdId, cmdUI, params)
{
    var method = "onUpdate" + cmdId;

    var a = sourceCtrl.getCmdTarget();

    for (; a; a=a.getParent()) {
        if (a[method] != undefined) {
            a[method](cmdUI, params);
            break;
        }
        else if (a.onUpdateCmdUI(cmdId, cmdUI, params))
            break;
    }
}

/*******************************************************************************/
Wsf_App.prototype.invokeCmd = function (sourceCtrl, cmdId, params)
{
    params.sender = sourceCtrl;
    params.cmdId = cmdId;

    // Ensure cmd is enabled

    var cmdUI = {
        isEnabled: true,
        enable: function (isEnabled) { this.isEnabled = isEnabled; },
        check: function (isChecked) { }
    };

    this.invokeUpdateCmd(sourceCtrl, cmdId, cmdUI, params);
    if (!cmdUI.isEnabled) return;

    // Handle on client
    // Try concrete OnXYZ() handler and then generic OnCommand() handler

    var method = "on" + cmdId;
    var cmdTarget = sourceCtrl.getCmdTarget();

    for (var a=cmdTarget; a; a=a.getParent()) {
        if (a[method] != undefined) {
            if (a[method](params) !== false) return;
            break;
        }
        else if (a['onCommand'] != undefined) {
            if (a.onCommand(params) !== false) return;
        }
    }

    // Handle on server

    delete params.sender;
    delete params.cmdId;

    Wsf_Web_CallServerMethodHandler.invoke("Wsf_Web_MVC_CtrlManager", "_HandleCommand",
        { cmdId: cmdId, cmdParams: params, srcCtrlId: cmdTarget.getId() },
        null, null,
        false);
}

/*******************************************************************************/
/*
/* EVENTS
/*
/*******************************************************************************/

Wsf_App.prototype._onResize = function (ev)
{
    if (this._inResize) return;   // IE calls resize multiple times
    this._inResize = true;
    this._rootCtrl.onSize();
    this._inResize = false;
}

/*******************************************************************************/
/*
/* RPC
/*
/*******************************************************************************/

/*******************************************************************************/
Wsf_App._handleRpcFailure = function (fnOnFailure, transport)
{
    if (transport.status==400 && transport.responseText.startsWith("[wsf-1]")) {
        alert(W('Wsf_Web.AJAX_NO_SESSION'));
        g_app.refresh();
    }
    else {
        alert(W('Wsf_Web.AJAX_SERVER_ERROR') + (DEBUG ? "\n\n"+transport.responseText : ''));
        if (fnOnFailure) fnOnFailure();
    }
}

/*******************************************************************************/
Wsf_App.prototype.rpc = function(method, params, onSuccess, onFailure)
{
   var self = this;

   var url = '/?m='+encodeURIComponent(method);

   if (params) {
      url += "&p="+encodeURIComponent(Object.toJSON(params));
      url += "&l="+this.lang;
   }

   new Ajax.Request(url,
      {
         method: 'get',

         onSuccess: function (transport) {
            try {
                // If PHP failed, transport.responseText is invalid and usually contains a PHP error message (in debug mode).
                var response = transport.responseText.evalJSON();
                if (onSuccess) onSuccess(response);
            }
            catch (e) {
                Wsf_App._handleRpcFailure(onFailure, transport);
                return;
            }

            },

         onFailure: Wsf_App._handleRpcFailure.bind(onFailure)
      });
}

/*******************************************************************************/
/*
/* HISTORY
/*
/*******************************************************************************/

/*****************************************************************************/
Wsf_App.getHash = function (hash)
{
    return Wsf_App._evalHashJson(window.location.hash.substr(1));
}

/*****************************************************************************/
Wsf_App._evalHashJson = function (hash)
{
    try {
        hash = hash ? hash.evalJSON() : {};
    }
    catch (e) {
        hash = {};
    }
    return hash;
}

/**
* Called when the URL's hash part has changed.
*/
Wsf_App.prototype._onHashChanged = function()
{
    var hash = window.location.hash.substr(1);

    if (this._ignoreHashChangeEvent === hash) {
        this._ignoreHashChangeEvent = false;
        //console.log('onHashChanged IGNORED', hash);
    }
    else {
        //console.log('onHashChanged', hash);
        var hash = Wsf_App._evalHashJson(hash);
        this._rootCtrl.onHashChanged(hash);
    }
}

/**
* Sets the URL's hash which raises the onHashEvent()
* Use for navigation in AJAX application
* If a param is NULL, it is removed from the hash
*/
Wsf_App.prototype.navigateHash = function (params, append, ignoreChangeEvent)
{
    if (append) {
        var newHash = Wsf_App._evalHashJson(window.location.hash.substr(1));
        for (x in params) {
            var v = params[x];
            if (v !== null)
                newHash[x] = params[x];
            else
                delete newHash[x];
        }
    }
    else {
        var newHash = params;
    }

    newHash = Wsf.isObjectEmpty(newHash) ? '' : Object.toJSON(newHash);
    window.location.hash = '#'+newHash;

    this._ignoreHashChangeEvent = ignoreChangeEvent ? newHash : null;
}

/*******************************************************************************/
Wsf_App.decorateVPageLinks = function (event)
{
    $$('a:not(.wsfSeen)').each(
        function (e) {
            var a = window.location.href.split('#');
            var b = e.href.split('#');
            a = a[0].replace(/\/$/,'');
            b = b[0].replace(/\/$/,'');
            if (a == b) e.observe('click', Wsf_App.onVPageLinkClicked);
            e.addClassName('wsfSeen');
        });
}

/*******************************************************************************/
Wsf_App.onVPageLinkClicked = function (event)
{
    event.stop();
    var a = event.findElement('a');
    var href = a.href;

    var p = href.substr(href.indexOf('#')+1);
    p = Wsf.urldecode(p).evalJSON();

    if (p.r) {
        var r = p.r;
        delete p.r;

        if (event.ctrlKey)
            g_tabBar.openTab(r, p, true, true).select();
        else if (a.target=='_blank' || !g_tabBar.selTab.closeable)
            g_tabBar.openPage(r, p, true);
        else
            g_tabBar.selTab.openPage(r, p);
    }
    else {
        g_tabBar.selTab.setParams(p);
    }
}


var Wsf_Web_MVC_ControlStateCoder = {

    decodeStateVariable : function (value)
    {
        if (value === null) {
            return value;
        }
        else if (typeof(value) == "string") {
            return value.charAt(0) == '*' ? value.substr(1) : g_app.getCtrlById(value);
        }
        else if (typeof(value) == "object") {
            if (Object.isArray(value)) {
                for (var i=0; i < value.length; ++i) {
                    value[i] = this.decodeStateVariable(value[i]);
                }
                return value;
            }
            else {
                for (var name in value) {
                    value[name] = this.decodeStateVariable(value[name]);
                }
                return value;
            }
        }
        else {
            return value;
        }
    },

    encodeVariable : function (value)
    {
        if (value === null) {
            return value;
        }
        else if (typeof(value) == "object") {
            if (value.isA) {
                return value.getId();
            }
            else if (Object.isArray(value)) {
                var newArray = [];
                for (var i=0; i < value.length; ++i) {
                    newArray[i] = this.encodeVariable(value[i]);
                }
                return newArray;
            }
            else {
                var newObj = {};
                for (var name in value) {
                    newObj[name] = this.encodeVariable(value[name]);
                }
                return newObj;
            }
        }
        else if (typeof(value) == "string") {
            return "*"+value;
        }
        else {
            return value;
        }
    }
};


/**
* Command handling:
* - define concrete handler OnXYZ(params)
*   Return TRUE or nothing if handled or FALSE to handle on the server
* - define generic handler OnCommand(eventData), where eventData = { cmdId, sender, params1, param2, etc. }
*   Return TRUE if handled or FALSE to continue routing.
*/

/*******************************************************************************/
function Wsf_Ctrls_Control()
{
    this._classes = ['Wsf_Ctrls_Control'];
    this._offsetWidth = null;
    this._offsetHeight = null;
    this._clientWidth = null;
    this._clientHeight = null;
    this._events = {};
    this._children = [];
    this._serverState = {};
    this._positionFixed = false;
    this._positionSet = false;
    this._styles = [];
    this._focusable = false;
    this._eventsDisabled = 0;
    //this._hotkeys = {};
}

Wsf_Ctrls_Control.EVENT_AFTER_DELETE =  100001;
Wsf_Ctrls_Control.EVENT_VISIBILITY_CHANGED =  100002;
Wsf_Ctrls_Control.EVENT_TITLE_CHANGED =  100003;
Wsf_Ctrls_Control.EVENT_ICON_CHANGED =  100004;

Wsf.use(Wsf_Ctrls_Control, Wsf_EventsTrait);

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.createFromServerState = function (serverState)
{
    for (var name in serverState) this._serverState[name] = null;
    this._loadState(Wsf_Web_MVC_ControlStateCoder.decodeStateVariable(serverState));

    if (this._parent)
        this._parent._children.push(this);
    else
        g_app._rootCtrl = this;
}

/**
* Init control from markup.
* May be called multiple times during the life of the control (e.g. when the markup has been updated in RPC)
*/
Wsf_Ctrls_Control.prototype.initFromMarkup = function ()
{
    this.elem = $(this._id);
    if (!this.elem) {
        console.log(this._id, " not found:", this);
        throw "Control "+this._id+" not found. Forgot to render it?";
    }
    this.elem.WsfCtrl = this;     // Got error? The control has not been rendered

    this._layout = this._childrenLayout ? eval('new '+this._childrenLayout+'(this);') : null;
    if (!this._enabled) this._enableInternal(false);

    if (this._layout) this.elem.style.overflow = "hidden";

    // reset flag because inline styles were reseted
    this._positionSet = false;

    for (var i=0,n=this._children.length ; i < n; ++i) this._children[i].initFromMarkup();
}

/*******************************************************************************
*
* EVENTS
*
*******************************************************************************/

/**
* Called after all controls have been properly inited.
* Called only once (not after the conrol has been updated in RPC).
* Overload to init the control, you may do RPC here.
*/
Wsf_Ctrls_Control.prototype.onCreate = function ()
{
    if (this._parent) this._parent.onChildAdded(this);
    for (var i=0,n=this._children.length ; i < n; ++i) this._children[i].onCreate();
}

/**
* Called in ajax, when state from server is updated.
* state is a map { property: newValue, ... }
*/
Wsf_Ctrls_Control.prototype.onStateChanged = function (state)
{
    if (state['_visible'] !== undefined) this.show(this._visible);
    if (state['_title'] !== undefined) this.raiseEvent(Wsf_Ctrls_Control.EVENT_TITLE_CHANGED, { sender: this });
}

Wsf_Ctrls_Control.prototype.onChildAdded = function (child)
{
}

Wsf_Ctrls_Control.prototype.onChildDeleted = function (child)
{
}

/**
* Called before the control is deleted or before the page is unloaded.
* Save layout to the local storage.
*/
Wsf_Ctrls_Control.prototype.onSaveLayout = function ()
{
}

/**
* Called before the control is deleted.
*/
Wsf_Ctrls_Control.prototype.onPreDelete = function ()
{
    this.onSaveLayout();
}

/**
* Called when the hash part of the URL has changed.
* Modify UI accordingly.
* @param hash object
*/
Wsf_Ctrls_Control.prototype.onHashChanged = function (hash)
{
    for (var i=0,n=this._children.length ; i < n; ++i) this._children[i].onHashChanged(hash);
}

/*******************************************************************************
*
* STATE
*
*******************************************************************************/

/**
* Load state from the server.
*/
Wsf_Ctrls_Control.prototype._loadState = function (state)
{
    for (var name in state) {
        this[name] = state[name];
    }
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype._saveState = function (ctrlsState)
{
    var state = this.getControlState();
    // save only if non-empty
    for (var name in state) { ctrlsState[this.getId()] = state; break; }

    for (var i=0; i < this._children.length; ++i) {
        this._children[i]._saveState(ctrlsState);
    }
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype._makeStateSnapshot = function ()
{
    for (var name in this._serverState) {
        this._serverState[name] = Object.toJSON(Wsf_Web_MVC_ControlStateCoder.encodeVariable(this[name]));
    }

    for (var i=0; i < this._children.length; ++i) this._children[i]._makeStateSnapshot();
}

/**
* Returns a state object to send to the server.
* You can overload it to add custom state to be passed to the server.
* Or modify state just before it's collected and send.
*/
Wsf_Ctrls_Control.prototype.getControlState = function ()
{
    if (!this._serverState) return {};

    var state = { };
    this._visible = this.elem.visible();

    for (var name in this._serverState) {
        var newValue = Wsf_Web_MVC_ControlStateCoder.encodeVariable(this[name]);
        if (Object.toJSON(newValue) !== this._serverState[name]) state[name] = newValue;
    }

    return state;
}

/*******************************************************************************/
/*
/* ...
/*
/*******************************************************************************/

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.deleteCtrl = function ()
{
    if (!this.elem) return;

    this.onPreDelete();

    this._children.each(function (ctrl) { ctrl.deleteCtrl(); });

    Element.remove(this.elem);  // this.elem.remove() does not work for the <select> element
    this.elem = null;
    g_app._deregisterCtrl(this);

    if (this._parent) {
        this._parent._children = this._parent._children.without(this);
        this._parent.onChildDeleted(this);
        this._parent = null;
    }

    this.raiseEvent(Wsf_Ctrls_Control.EVENT_AFTER_DELETE, { sender: this });
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.walk = function (fn)
{
    for (var i=0; i < this._children.length; ++i) {
        this._children[i].walk(fn);
    }
    this[fn]();
}

/**
 * Get the control's main DOM element
 * @return DOMElement
 */
Wsf_Ctrls_Control.prototype.getElem = function ()
{
    return this.elem;
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype._getChildrenContainerElem = function ()
{
    return this.elem;
}

/**
 * Register a class name to be able to RTTI (e.g. isA())
 * Call in the constructor
 */
Wsf_Ctrls_Control.prototype._addClass = function (className)
{
    this._classes.push(className);
}

/**
 * @param className
 * @return bool
 */
Wsf_Ctrls_Control.prototype.isA = function (className)
{
    return this._classes.indexOf(className) != -1;
}

/**
 * @param string className
 * @return Wsf_Ctrls_Control|null
 */
Wsf_Ctrls_Control.prototype.getAncestorByClass = function (className)
{
    for (var o=this; o; o=o._parent) {
        if (o.isA(className)) return o;
    }
    return null;
}

/**
 * @param string className
 * @return Wsf_Ctrls_Control|null
 */
Wsf_Ctrls_Control.prototype.getChildByClass = function (className)
{
    for (var i=0; i < this._children.length; ++i) {
        if (this._children[i].isA(className)) return this._children[i];
    }
    for (var i=0; i < this._children.length; ++i) {
        var o = this._children[i].getChildByClass(className);
        if (o) return o;
    }
    return null;
}

/**
 * Returns TRUE if ctrl is an ancestor of this control or the control itself.
 * @return bool
 */
Wsf_Ctrls_Control.prototype.isAncestor = function (ctrl)
{
    for (var o=this; o; o=o._parent) {
        if (o === ctrl) return true;
    }
    return false;
}

/**
 * @return Wsf_Ctrls_ViewHtml
 */
Wsf_Ctrls_Control.prototype.getView = function ()
{
    return Wsf_Ctrls_ViewHtml.prototype._instance;
}

/**
 * Returns an ancestor control including self whose class contains Frame string.
 * @return Wsf_Ctrls_Control
 */
Wsf_Ctrls_Control.prototype.getFrame = function ()
{
    for (var o=this; o; o=o._parent) {
        for (var i=0; i < o._classes.length; ++i) {
            if (o._classes[i].search('Frame') != -1) return o;
        }
    }
    return null;
}

/**
 * Control's HTML title.
 * @return string
 */
Wsf_Ctrls_Control.prototype.getTitle = function ()
{
    return this._title;
}

/**
 * @return string
 */
Wsf_Ctrls_Control.prototype.getId = function ()
{
    return this._id;
}

/**
 * @return Wsf_Ctrls_Control|null
 */
Wsf_Ctrls_Control.prototype.getParent = function ()
{
    return this._parent;
}

/**
 * @param int idx  Zero based child index
 * @return Wsf_Ctrls_Control|null
 */
Wsf_Ctrls_Control.prototype.getChildByIdx = function (idx)
{
    return idx >= this._children.length ? null : this._children[idx];
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.getChildrenCount = function ()
{
    return this._children.length;
}

/**
* @return Wsf_Ctrls_Control
*/
Wsf_Ctrls_Control.prototype.getPrevSibling = function ()
{
    if (!this._parent) return null;

    for (var i=0; i < this._parent._children.length; ++i) {
        if (this._parent._children[i] == this) {
            return i ? this._parent._children[i-1] : null;
        }
    }
    return null;
}

/**
* @return Wsf_Ctrls_Control
*/
Wsf_Ctrls_Control.prototype.getNextSibling = function ()
{
    if (!this._parent) return null;

    for (var i=0,n=this._parent._children.length; i < n; ++i) {
        if (this._parent._children[i] == this) {
            return i<n-1 ? this._parent._children[i+1] : null;
        }
    }
    return null;
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.getChildren = function ()
{
    return this._children;
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.updateUI = function ()
{
    if (!this.isVisible()) return;
    for (var i=0; i < this._children.length; ++i) {
        this._children[i].updateUI();
    }
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.getForm = function ()
{
    for (var p = this; p && !p.isA("Wsf_Forms_CtrlForm"); p=p._parent) { }
    return p;
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.getWindow = function ()
{
    for (var p = this; p && !p._window; p=p._parent) { }
    return p;
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.focus = function ()
{
    if (!this.isVisible() || !this.isEnabled()) return false;

    if (this._focusable) {
        this.elem.focus();
        this.getView().onFocusChanged(this);
        return true;
    }

    for (var i=0; i < this._children.length; ++i) {
        if (this._children[i].focus()) return true;
    }

    return false;
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.hasFocus = function ()
{
    return this.getView().getFocus() === this;
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.enable = function (enable)
{
    if (enable == this._enabled) return this;
    this._enableInternal(enable);
    return this;
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype._enableInternal = function (enable)
{
    if (enable)
        this.elem.removeClassName('disabled');
    else
        this.elem.addClassName('disabled');

    this.elem.disabled = !enable;
    this._enabled = enable;
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.isEnabled = function ()
{
    return this._enabled;
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.setReadOnly = function (readOnly)
{
    if (this._readOnly == readOnly) return this;
    this._readOnly = readOnly;

    if (readOnly)
        this.elem.addClassName('readOnly');
    else
        this.elem.removeClassName('readOnly');

    this.elem.readOnly = readOnly;
    return this;
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.isReadOnly = function ()
{
    return this._readOnly;
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.getStyle = function (style, defValue)
{
    return this._styles[style] !== undefined ? this._styles[style] : defValue;
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.setStyle = function (style, value)
{
    if (this[style] !== undefined)
        this[style] = value;
    else
        this._styles[style] = value;
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.setTitle = function (titleHtml)
{
    this._title = titleHtml;
    this.raiseEvent(Wsf_Ctrls_Control.EVENT_TITLE_CHANGED, { sender: this });
    return this;
}

/**
* Binds the title with a text control content.
* @return Wsf_Ctrls_Control
*/
Wsf_Ctrls_Control.prototype.bindTitle = function (textCtrl, callback)
{
    var f = function () {
            var s = textCtrl.getValue().escapeHTML();
            if (!s) s = W('Wsf_Ctrls.noName').escapeHTML();
            if (callback) s = callback(s);
            this.setTitle(s);
        }.bind(this);

    textCtrl.attachEventHandler('delayedChange', f);
    f();
    return this;
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.callServerMethod = function (method, params, fnOnSuccess, fnOnFailure)
{
    return Wsf_Web_CallServerMethodHandler.invoke(this, method, params, fnOnSuccess, fnOnFailure);
}

/**
* @return Wsf_Ctrls_Control
*/
Wsf_Ctrls_Control.prototype.setIcon = function (icon)
{
    this._icon = icon;
    this.raiseEvent(Wsf_Ctrls_Control.EVENT_ICON_CHANGED, { sender: this });
    return this;
}

/**
*/
Wsf_Ctrls_Control.prototype.getIcon = function ()
{
    return this._icon;
}

/**
 * Maps a DOM event to a WSF event. When a DOM event occurs, the WSF event is raised.
 * EventData = { sender, domEvent }
 * @return Wsf_Ctrls_Control
 */
Wsf_Ctrls_Control.prototype._mapDomEventToEvent = function (element, domEventName, eventName)
{
    element.observe(domEventName,
        function (domEvent) {
            this.raiseEvent(eventName, { sender: this, domEvent: domEvent });
        }.bindAsEventListener(this));
    return this;
}


/*******************************************************************************/
/*
/* VISIBILITY
/*
/*******************************************************************************/

/**
* True if the control itself is visible. Ignores if the parent is hidden.
*/
Wsf_Ctrls_Control.prototype.isVisible = function ()
{
    return this.elem.visible();
}

/**
* True if the control and all its parents are visible.
*/
Wsf_Ctrls_Control.prototype.isPathVisible = function ()
{
    for (var p=this; p; p = p.getParent()) {
        if (!p.isVisible()) return false;
    }
    return true;
}

/**
* @return Wsf_Ctrls_Control
*/
Wsf_Ctrls_Control.prototype.show = function (show)
{
    if (show || show == undefined)
        this.elem.show();
    else
        this.elem.hide();

    if (this._parent) this._parent.onSize();

    this.raiseEvent(Wsf_Ctrls_Control.EVENT_VISIBILITY_CHANGED, { sender: this });
    return this;
}

/**
* @return Wsf_Ctrls_Control
*/
Wsf_Ctrls_Control.prototype.hide = function ()
{
    this.show(false);
    return this;
}

/*******************************************************************************/
/*
/* POSITION & SIZE - this is slow
/*
/*******************************************************************************/

/*******************************************************************************/
Wsf_Ctrls_Control.prototype._cacheComputedStyles = function ()
{
    //   this._margin = [0,0,0,0];
    //   this._border = [0,0,0,0];
    //   this._padding = [0,0,0,0];
    //   return;

    var s = window.getComputedStyle ? window.getComputedStyle(this.elem, null) : this.elem.currentStyle;

    var a = [parseInt(s.paddingTop), parseInt(s.paddingRight), parseInt(s.paddingBottom), parseInt(s.paddingLeft) ];
    if (isNaN(a[0])) a[0] = 0;
    if (isNaN(a[1])) a[1] = 0;
    if (isNaN(a[2])) a[2] = 0;
    if (isNaN(a[3])) a[3] = 0;
    this._padding = a;

    a = [parseInt(s.marginTop), parseInt(s.marginRight), parseInt(s.marginBottom), parseInt(s.marginLeft)];
    if (isNaN(a[0])) a[0] = 0;
    if (isNaN(a[1])) a[1] = 0;
    if (isNaN(a[2])) a[2] = 0;
    if (isNaN(a[3])) a[3] = 0;
    this._margin = a;

    a = [parseInt(s.borderTopWidth), parseInt(s.borderRightWidth), parseInt(s.borderBottomWidth), parseInt(s.borderLeftWidth)];
    if (isNaN(a[0])) a[0] = 0;
    if (isNaN(a[1])) a[1] = 0;
    if (isNaN(a[2])) a[2] = 0;
    if (isNaN(a[3])) a[3] = 0;
    this._border = a;
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype._clearSizeCache = function ()
{
    this._offsetWidth = null;
    this._offsetHeight = null;
    this._clientWidth = null;
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype._cacheOffsetWidth = function ()
{
    this._offsetWidth = this.elem.offsetWidth;
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype._cacheOffsetHeight = function ()
{
    this._offsetHeight = this.elem.offsetHeight;
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype._cacheClientSize = function ()
{
    this._clientWidth = this.elem.clientWidth;
    this._clientHeight = this.elem.clientHeight;
}

/**
* content+padding+border
* @return { width, height }
*/
Wsf_Ctrls_Control.prototype.getOffsetSize = function ()
{
    if (this._offsetWidth === null) this._cacheOffsetWidth();
    if (this._offsetHeight === null) this._cacheOffsetHeight();
    return { width: this._offsetWidth, height: this._offsetHeight };
}

/**
* content+padding+border
*/
Wsf_Ctrls_Control.prototype.getOffsetWidth = function ()
{
    if (this._offsetWidth === null) this._cacheOffsetWidth();
    return this._offsetWidth;
}

/**
* content+padding+border
*/
Wsf_Ctrls_Control.prototype.getOffsetHeight = function ()
{
    if (this._offsetHeight === null) this._cacheOffsetHeight();
    return this._offsetHeight;
}

/**
* content+padding
* @return { width, height }
*/
Wsf_Ctrls_Control.prototype.getClientSize = function ()
{
    if (this._clientWidth === null || this._clientHeight === null) this._cacheClientSize();
    return { width: this._clientWidth, height: this._clientHeight };
}

/**
* Slow!
*/
Wsf_Ctrls_Control.prototype.getMargin = function()
{
    if (!this._margin) this._cacheComputedStyles();
    return this._margin;
}

/**
* Slow!
*/
Wsf_Ctrls_Control.prototype.getPadding = function()
{
    if (!this._padding) this._cacheComputedStyles();
    return this._padding;
}

/**
 * Slow!
 * @deprecation  Use box-sizing:border-box to avoid this
 */
/*Wsf_Ctrls_Control.prototype._getMarginBorderPadding = function()
{
    if (!this._margin) this._cacheComputedStyles();

    return [this._margin[0]+this._border[0]+this._padding[0],
        this._margin[1]+this._border[1]+this._padding[1],
        this._margin[2]+this._border[2]+this._padding[2],
        this._margin[3]+this._border[3]+this._padding[3]
        ];
}*/

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.move = function (x,y,w,h)
{
    var s = this.elem.style;

    //if (x < 0) x = 0;
    //if (y < 0) y = 0;

    if (!this._positionSet) {
        s.position = this._positionFixed ? "fixed":"absolute";
        this._positionSet = true;
    }

    s.left = x+"px";
    s.top = y+"px";

    if (w !== null || h !==null) {
        this.setSize(w, h);
    }
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.move2 = function (x1,y1,x2,y2,w,h)
{
    var s = this.elem.style;

    if (!this._positionSet) {
        s.position = this._positionFixed ? "fixed":"absolute";
        this._positionSet = true;
    }

    if (x1 !== null) s.left = x1+"px";
    if (y1 !== null) s.top = y1+"px";
    if (x2 !== null) s.right = x2+"px";
    if (y2 !== null) s.bottom = y2+"px";

    if ((x1 !== null && x2 !== null) || (y1 !== null && y2 !== null)) this._clearSizeCache();

    if (w !== null || h !== null) {
        this.setSize(w, h);
    }
    else this.onSize();
}

/**
 * Sets the width and/or height of the control.
 * W and H = border+padding+content
 * The element must have "box-sizing: border-box"
 * @return Wsf_Ctrls_Control
 */
Wsf_Ctrls_Control.prototype.setSize = function (w, h)
{
    var s = this.elem.style;
    var b = [0,0,0,0];//this._getMarginBorderPadding();

    if (w !== null) {
        this._offsetWidth = w;
        w -= b[1] + b[3];
        if (w < 0) w = 0;
        s.width = w + "px";
        this._offsetWidth = w + b[1] + b[3];
    }

    if (h !== null) {
        this._offsetHeight = h;
        h -= b[0] + b[2];
        if (h < 0) h = 0;
        s.height = h  + "px";
        this._offsetHeight = h + b[0] + b[2];
    }

    this.onSize();
    return this;
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.onSize = function ()
{
    if (!this.isVisible()) return;

    if (this._layout) {
        for (var i=0,n=this._children.length; i < n; ++i) {
            this._children[i]._clearSizeCache();
        }
        this._layout.layoutChildren(this);
    }
    else {
        // Default CSS layout of children
        for (var i=0,n=this._children.length; i < n; ++i) {
            var o = this._children[i];
            o._clearSizeCache();
            o.onSize();
        }
    }
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.setZIndex = function (zIndex)
{
    this.elem.style.zIndex = zIndex;
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.moveBelow = function (ctrl)
{
    this.elem.style.zIndex = ctrl.elem.style.zIndex - 1;
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.setOpacity = function (opacityFloat)
{
    this.elem.setOpacity(opacityFloat);
}

/*******************************************************************************/
/*
/* COMMANDS
/*
/*******************************************************************************/

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.getCmdTarget = function ()
{
    return this._cmdTarget ? this._cmdTarget : this;
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.onUpdateCmdUI = function (cmdId, cmdui, params)
{
    return false;
}

/*******************************************************************************/
/*
/* LAYOUT
/*
/*******************************************************************************/

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.saveLayout = function (name, layout)
{
    if (!window.localStorage) return;
    window.localStorage['wsf.cl.'+name] = Object.toJSON(layout);
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.loadLayout = function (name)
{
    if (!window.localStorage) return null;
    if (!window.localStorage['wsf.cl.'+name]) return null;
    return window.localStorage['wsf.cl.'+name].evalJSON();
}


/*******************************************************************************/
function Wsf_Ctrls_LayoutStack(ctrl)
{
    ctrl.elem.style.overflow = "auto";
}

/*******************************************************************************/
Wsf_Ctrls_LayoutStack.prototype.layoutChildren = function (ctrl)
{
    // Get the height of all fixed controls and # of stretch controls

    var fixedH = 0;
    var stretchCount = 0;
    var c = [];

    for (var i=ctrl._children.length; i--;)
    {
        var o = ctrl._children[i];
        if (!o.isVisible()) continue;

        if (o.getStyle('stretch',false)) {
            ++stretchCount;
            o.elem.style.overflow = "auto";
            c.push(o);
        }
        else {
            var m = o.getMargin();
            fixedH += o.getOffsetHeight() + m[0] + m[2];
        }
    }

    // Layout

    var padding = ctrl.getPadding();
    var a = ctrl.getClientSize();
    var h = a['height'] - padding[0] - padding[2];
    var y = padding[0];
    var stretchH = Math.max(h - fixedH, 0);

    if (stretchCount > 0) {
        var avgStretchH = Math.floor(stretchH / stretchCount);
        var lastStretchH = h - avgStretchH*(stretchCount-1) - fixedH;
    }

    for (var i=0; i < c.length; ++i)
    {
        var o = c[i];

        // e.g. input[text] does not stretch to 100% by default -> we must set also width

        if (o.getStyle('stretch',false)) {
            h = (--stretchCount ? avgStretchH : lastStretchH);
            o.setSize(null,h);
        }
    }
}


/*******************************************************************************/
function Wsf_Ctrls_LayoutFullsize(ctrl)
{
}

/*******************************************************************************/
Wsf_Ctrls_LayoutFullsize.prototype.layoutChildren = function (ctrl)
{
    for (var i=0,n=ctrl.getChildrenCount(); i<n; ++i)
    {
        var t = ctrl.getChildByIdx(i);

        if (t.isVisible()) {
            var p = ctrl.getPadding();
            t.move2(p[3],p[0],p[1],p[2],null,null);
            break;
        }
    }
}


/*****************************************************************************/
var Wsf_Web_Vocabulary = {

    // Vocabularies are added as new classes are loaded via AJAX
    _vocabularies : [],

    add : function (vocabulary)
    {
        this._vocabularies.push(vocabulary);
    },

    getWord : function (id)
    {
        a = id.split('.');
        var id = a[1].toLowerCase();
        for (var i=0,n=this._vocabularies.length; i < n; ++i) {
            var ns = this._vocabularies[i][a[0]];
            if (ns) {
                var w = ns[id];
                if (w) return w;
            }
        }
        return "["+id+"]";
    }
};

/*****************************************************************************/
function W(id)
{
    return Wsf_Web_Vocabulary.getWord(id);
}

/**
* UI blocking call of a server method with thr control tree synchronization
* Sending of files works in FF4 and Chrome6
*/
function Wsf_Web_CallServerMethodHandler(ctrl, method, params, fnOnSuccess, fnOnFailure, async)
{
    if (async === undefined) async = true;

    // Busy info

    var div = $(document.createElement('div'));
    div.id = 'wsfAjaxStatus';
    div.update(W('Wsf_Web.LOADING')+"...");
    document.body.appendChild(div);

    // Overlay is needed to disable clicking

    var div = $(document.createElement('div'));
    div.id = 'wsfDisableUiOverlay';
    document.body.appendChild(div);

    // DTO

    postParams = {
        wsfState: {
            async: async,
            object: Wsf_Web_MVC_ControlStateCoder.encodeVariable(ctrl),
            method: method,
            params: params === undefined ? {} : Wsf_Web_MVC_ControlStateCoder.encodeVariable(params),
            ctrlsState: {},
            deletedServerCtrls: g_app._deletedServerCtrls
            //hash: Wsf_App.getHash()
        }
    };

    g_app.getRootCtrl()._saveState(postParams.wsfState.ctrlsState);
    postParams.wsfState = Object.toJSON(postParams.wsfState);

    // Standard from POST with a page reload

    if (!async) {
        $('wsfState').value = postParams.wsfState;
        $('wsfForm').submit();
        this.done();
        return;
    }

    // Asynch

    var a = $$('input','select','textarea');
    var filePresent = false;

    for (var i=0,n=a.length; i < n; ++i) {
        var elem = a[i];
        var name = elem.name;
        if (name == 'wsfState') continue;

        if (elem.type == 'file') {
            if (!elem.files.length) continue;
            postParams[name] = elem.files[0];
            filePresent = true;
        }
        else {
            if (elem.type == 'radio' && !elem.checked) continue;
            if (elem.type == 'checkbox' && !elem.checked) continue;
            if (elem.type == 'text') continue;  // transfered via value property
            var value = elem.value;

            if (postParams[name] === undefined)
                postParams[name] = value;
            else {
                if (!Object.isArray(postParams[name])) postParams[name] = [postParams[name]];
                postParams[name].push(value);
            }
        }
    }

    if (filePresent)
    {
        var fd = new FormData();
        for (var name in postParams) {
            var param = postParams[name];
            if (typeof(param) == "object") {
                if (Object.isArray(param)) {
                    alert("Wsf_Web_CallServerMethodHandler.prototype._buildMessage: NOT IMPLEMENTED");
                }
                else fd.append(name,param);
            }
            else fd.append(name,param);
        }

        // HACK: prototype.js is modified NOT to set the content-type header if null
        new Ajax.Request(document.location.href, {
            asynchronous: true,
            method: 'post',
            contentType: null, // automatic header value will be generated by the browser
            postBody: fd,
            onSuccess: this.onSuccess.bind(this, fnOnSuccess, fnOnFailure),
            onFailure: this.onFailure.bind(this, fnOnFailure)
        });
    }
    else {
        new Ajax.Request(document.location.href, {
            asynchronous: true,
            method: 'post',
            parameters: postParams,
            onSuccess: this.onSuccess.bind(this, fnOnSuccess, fnOnFailure),
            onFailure: this.onFailure.bind(this, fnOnFailure)
        });
    }
}

/*******************************************************************************/
Wsf_Web_CallServerMethodHandler._queue = [];

/**
* From http://www.webtoolkit.info/javascript-utf8.html
* @todo Move to some global service class
*/
Wsf_Web_CallServerMethodHandler._encodeUtf8 = function (string)
{
    //    string = string.replace(/\r\n/g,"\n");
    var utftext = "";

    for (var n=0, m=string.length; n < m; n++) {
        var c = string.charCodeAt(n);
        if (c < 128) {
            utftext += String.fromCharCode(c);
        }
        else if((c > 127) && (c < 2048)) {
            utftext += String.fromCharCode((c >> 6) | 192);
            utftext += String.fromCharCode((c & 63) | 128);
        }
        else {
            utftext += String.fromCharCode((c >> 12) | 224);
            utftext += String.fromCharCode(((c >> 6) & 63) | 128);
            utftext += String.fromCharCode((c & 63) | 128);
        }
    }

    return utftext;
}

/**
* Parameters can be passed as an object of named parameters or an array of unnamed parameters.
* NOTE: You may not alter the control tree until method call has finished.
*/
Wsf_Web_CallServerMethodHandler.invoke = function (ctrl, method, params, fnOnSuccess, fnOnFailure, async)
{
    Wsf_Web_CallServerMethodHandler._queue.push([ctrl, method, params, fnOnSuccess, fnOnFailure, async]);
    if (Wsf_Web_CallServerMethodHandler._queue.length == 1) Wsf_Web_CallServerMethodHandler._runTopDefered();
}

/*******************************************************************************/
Wsf_Web_CallServerMethodHandler._runTopDefered = function ()
{
    // Defer the RPC after the JS engine is idle to avoid control tree change right after callServerMethod() has been called
    (function () {
        var p = Wsf_Web_CallServerMethodHandler._queue[0];
        new Wsf_Web_CallServerMethodHandler(p[0], p[1], p[2], p[3], p[4], p[5]);
    }).defer();
}

/*******************************************************************************/
Wsf_Web_CallServerMethodHandler.prototype.done = function ()
{
    var e = $('wsfAjaxStatus');
    e.parentNode.removeChild(e);
    e = $('wsfDisableUiOverlay');
    e.parentNode.removeChild(e);

    Wsf_Web_CallServerMethodHandler._queue.shift();

    if (Wsf_Web_CallServerMethodHandler._queue.length) Wsf_Web_CallServerMethodHandler._runTopDefered();
}

/*******************************************************************************/
Wsf_Web_CallServerMethodHandler.prototype.onFailure = function (fnOnFailure, transport)
{
    Wsf_App._handleRpcFailure(fnOnFailure, transport);
    this.done();
}

/*******************************************************************************/
Wsf_Web_CallServerMethodHandler.prototype.onSuccess = function (fnOnSuccess, fnOnFailure, transport)
{
    try {
        // If PHP failed, transport.responseText is invalid and usually contains a PHP error message (in debug mode).
        var response = transport.responseText.evalJSON();
    }
    catch (e) {
        this.onFailure(fnOnFailure, transport);
        return;
    }

    try
    {
        g_app._stopUpdateUI();

        g_app._nextCtrlId = response.nextCtrlId;
        g_app._nextServerCtrlId = response.nextCtrlId;
        g_app._deletedServerCtrls = [];

        // Load style sheets

        if (response.styleSheet) Wsf.requireStylesheet(response.styleSheet);

        // Load scripts

        for (var i=0,n=response.scripts.length; i < n; ++i) {
            var s = response.scripts[i];
            if (s.charAt(0) == '/' || s.substr(0,4) == 'http')
                Wsf.requireScript(s);
            else {
                var script = document.createElement('script');
                script.type = 'text/javascript';
                script.text = s;
                document.body.appendChild(script);
            }
        }

        // Deleted controls

        for (var i=0,n=response.deletedCtrls.length; i < n; ++i) {
            var c = g_app.getCtrlById(response.deletedCtrls[i]);
            if (c) c.deleteCtrl();
        }

        // 1. Add/update markup to the DOM

        if (response.renderedSubtrees !== undefined) {
            for (var id in response.renderedSubtrees) {
                var s = response.renderedSubtrees[id];
                var o = g_app.getCtrlById(id);

                if (o)
                    o.getElem().replace(s.html);
                else {
                    // TODO: insert before the right child. now its appended to the parent
                    var p = g_app.getCtrlById(s.pid);
                    p._getChildrenContainerElem().insert(s.html);
                }
            }
        }

        // 2) Create new subtrees

        if (response.newSubtrees !== undefined)
        {
            var newSubtreeRoots = [];
            eval('var subtrees = ' + response.newSubtrees);
            /*var script = document.createElement('script');
            script.type = 'text/javascript';
            script.text = 'var subtrees = ' + response.newSubtrees;
            document.body.appendChild(script);*/

            for (var i=0; i < subtrees.length; ++i) {
                var ctrls = subtrees[i];
                for (var id in ctrls) {
                    g_app._registerCtrl(id, ctrls[id][0]);
                }
            }

            for (var i=0; i < subtrees.length; ++i) {
                var ctrls = subtrees[i];
                for (var id in ctrls) {
                    g_app.getCtrlById(id).createFromServerState(ctrls[id][1]);
                }
            }

            var newSubtreeRoots = [];
            for (var i=0; i < subtrees.length; ++i) {
                var ctrls = subtrees[i];
                for (var id in ctrls) {
                    newSubtreeRoots.push(id);
                    break;
                }
            }
        }

        // 3) Load state of updated controls
        // Must be called after all new controls have been created

        if (response.ctrlsState !== undefined) {
            for (var id in response.ctrlsState) {
                var o = g_app.getCtrlById(id);
                Wsf_Web_MVC_ControlStateCoder.decodeStateVariable(response.ctrlsState[id]);
                o._loadState(response.ctrlsState[id]);
            }
        }

        // 4) MakeStateSnapshot() for all controls

        g_app.getRootCtrl()._makeStateSnapshot();

        // 5) .initFromMarkup() all rendered controls

        if (response.renderedSubtrees !== undefined) {
            for (var id in response.renderedSubtrees) {
                var c = g_app.getCtrlById(id);
                c.initFromMarkup();
            }
        }

        //  6) onCreate() for new subtrees

        if (response.newSubtrees !== undefined) {
            for (var i=0; i < newSubtreeRoots.length; ++i) {
                g_app.getCtrlById(newSubtreeRoots[i]).onCreate();
            }
        }

        // 7) onStateChanged() for updated controls

        if (response.ctrlsState !== undefined) {
            for (var id in response.ctrlsState) {
                var o = g_app.getCtrlById(id);
                // Control may have been deleted above in some event handler e.g. onChildAdded()
                if (o) o.onStateChanged(response.ctrlsState[id]);
            }
        }

        //  8) onHashChanged() for new subtrees (since 9.9.0)

        if (response.newSubtrees !== undefined) {
            var hash = Wsf_App.getHash();
            for (var i=0; i < newSubtreeRoots.length; ++i) {
                g_app.getCtrlById(newSubtreeRoots[i]).onHashChanged(hash);
            }
        }

        // Decorate virtual page links

        Wsf_App.decorateVPageLinks();

        // Done

        g_app.getRootCtrl().onSize();  // force reflow
        if (fnOnSuccess) fnOnSuccess(Wsf_Web_MVC_ControlStateCoder.decodeStateVariable(response.returnValue));
        g_app._startUpdateUI();
        this.done();
    }

    catch (e) {
        if (DEBUG) {
            console.log(e);
            throw e;
        }
        else {
            alert(W('Wsf_Web.AJAX_FATAL_ERROR') + (DEBUG ? "\r\n\r\n"+e+"\r\n\r\n"+e.stack : ""));
            g_app.refresh();
        }

    }
}

function Wsf_Ctrls_View() { Wsf_Ctrls_Control.call(this); } Wsf.extend(Wsf_Ctrls_View, Wsf_Ctrls_Control);


/*******************************************************************************/
function Wsf_Ctrls_ViewHtml(serverState)
{
    Wsf_Ctrls_View.call(this, serverState);

    // TODO: BUGGY
    Event.observe(document.body, 'activate', this._onFocus.bindAsEventListener(this));

    Wsf_Ctrls_ViewHtml.prototype._instance = this;
}

Wsf.extend(Wsf_Ctrls_ViewHtml, Wsf_Ctrls_View);

/*******************************************************************************/
Wsf_Ctrls_ViewHtml.prototype.onCreate = function ()
{
    Wsf_Ctrls_View.prototype.onCreate.call(this);

    if (this._focusedCtrl) {
        if (!this._focusedCtrl.focus()) this._focusedCtrl = null;
    }
}

/*******************************************************************************/
Wsf_Ctrls_ViewHtml.prototype.onStateChanged = function (state)
{
    Wsf_Ctrls_View.prototype.onStateChanged.call(this, state);

    if (state['_focusedCtrl'] !== undefined) {
        this._focusedCtrl = state['_focusedCtrl'];
        // Delay the focus() to make it work on newly created controls
        if (this._focusedCtrl) this._focusedCtrl.focus.bind(this._focusedCtrl).delay();
    }
}

/*******************************************************************************/
Wsf_Ctrls_ViewHtml.prototype.onFocusChanged = function (ctrlWithFocus)
{
    this._focusedCtrl = ctrlWithFocus;
}

/*******************************************************************************/
Wsf_Ctrls_ViewHtml.prototype._getChildrenContainerElem = function ()
{
    return this.elem.down('form');
}

/*******************************************************************************/
Wsf_Ctrls_ViewHtml.prototype._onFocus = function (ev)
{
    // BUGGY

    // NOTE: IE: Do not pass newly focused controls back to improve focus-stealing resistance :-)
    // Optimize when we know how
    if (Prototype.Browser.IE) this._focusedCtrl = g_app.getCtrlById(ev.element().id);
}

/*******************************************************************************/
Wsf_Ctrls_ViewHtml.prototype.getFocus = function()
{
    return this._focusedCtrl;
}

/*******************************************************************************/
Wsf_Ctrls_ViewHtml.prototype.getSkinPath = function()
{
    return this.skin.skinPath;
}

/*******************************************************************************/
function Wsf_Ctrls_Menu()
{
    Wsf_Ctrls_Control.call(this);
}

Wsf.extend(Wsf_Ctrls_Menu, Wsf_Ctrls_Control);

/**
* Called when an item is clicked: { id, params:{checked[,custom]} }
*/
Wsf_Ctrls_Menu.EVENT_ITEM_CLICKED = 1;
/**
* Called when no item is selected from the context menu.
*/
Wsf_Ctrls_Menu.EVENT_CANCEL = 2;

// Expand mode
Wsf_Ctrls_Menu.EM_NONE = 0;
Wsf_Ctrls_Menu.EM_HOVER = 1;
Wsf_Ctrls_Menu.EM_CLICK = 2;

/*******************************************************************************/
Wsf_Ctrls_Menu.prototype.initFromMarkup = function ()
{
    Wsf_Ctrls_Control.prototype.initFromMarkup.call(this);
    this.itemElems = [];
    this._isAnyItemEnabled = false;
    this.elem.descendants().each(this._decorateElem.bind(this));
}

/*******************************************************************************/
Wsf_Ctrls_Menu.prototype._decorateElem = function (e)
{
    if (e.tagName != "LI") return;

    this.itemElems.push(e);

    var a = e.id.split('.');
    e.cmdId = a.length == 2 ? a[1] : null;

    if (this.expandMode == Wsf_Ctrls_Menu.EM_HOVER) {
        if (e.cmdId != '' && e.hasClassName('clickable')) {
            e.observe('click', this.onClickItem.bindAsEventListener(this, e));
        }
        else if (e.hasClassName('expandable')) {
            e.observe('click', this.onExpandItem.bindAsEventListener(this, e));
        }
        e.observe('mouseover', this.LI_OnMouseOver.bindAsEventListener(this, e));
        e.observe('mouseout', this.LI_OnMouseOut.bindAsEventListener(this, e));
    }
    else if (this.expandMode == Wsf_Ctrls_Menu.EM_CLICK) {
        if (e.cmdId != '' && e.hasClassName('clickable')) {
            e.observe('click', this.onClickItem.bindAsEventListener(this, e));
        }
        else if (e.hasClassName('expandable')) {
            e.observe('click', this.onExpandItem.bindAsEventListener(this, e));
        }
    }
}

/*******************************************************************************/
Wsf_Ctrls_Menu.prototype.getControlState = function ()
{
    var a = Wsf_Ctrls_Control.prototype.getControlState.apply(this);

    a.selIDs = [];

    for (var i=this.itemElems.length; i--;) {
        var e = this.itemElems[i];
        if (e.hasClassName('sel')) a.selIDs.push(e.id.split('.')[1]);
    }

    return a;
}

/*******************************************************************************/
Wsf_Ctrls_Menu.prototype.updateUI = function ()
{
    if (!this._isCmdMode) return;

    var cmdui = {
        isEnabled : true,
        isChecked : false,
        enable: function (isEnabled) { this.isEnabled = isEnabled; },
        check: function (isChecked) { this.isChecked = isChecked; }
    };

    this._isAnyItemEnabled = false;

    for (var i=this.itemElems.length; i--;)
    {
        var e = this.itemElems[i];
        var cmdId = e.id.split('.')[1];
        if (!cmdId) continue;
        cmdui.isEnabled = true;
        cmdui.isChecked = false;
        // TODO: optimize, avoid parsing JSON all the time
        var params = e.getAttribute('data-meta');
        params = params ? params.evalJSON() : {};

        g_app.invokeUpdateCmd(this.getCmdTarget(), cmdId, cmdui, params);

        if (cmdui.isEnabled) {
            this._isAnyItemEnabled = true;
            e.removeClassName('disabled');
        }
        else
            e.addClassName('disabled');

        if (cmdui.isChecked)
            e.addClassName('checked');
        else
            e.removeClassName('checked');
    }
}

/*******************************************************************************/
Wsf_Ctrls_Menu.prototype.isAnyItemEnabled = function()
{
    return this._isAnyItemEnabled;
}

/*****************************************************************************/
/*
/* SELECTION
/*
/*****************************************************************************/

/*****************************************************************************/
Wsf_Ctrls_Menu.prototype.selectItem = function (id, isSelect)
{
    for (var i=this.itemElems.length; i--;)
    {
        var e = this.itemElems[i];

        if (id == e.id.split('.')[1])
        {
            if (isSelect) {
                e.addClassName('sel');
                e.down().addClassName('selTitle');
            }
            else {
                e.removeClassName('sel');
                e.down().removeClassName('selTitle');
            }
            break;
        }
    }
}

/*****************************************************************************/
Wsf_Ctrls_Menu.prototype.getSelId = function ()
{
    for (var i=this.itemElems.length; i--;)
    {
        var e = this.itemElems[i];

        if (e.hasClassName('sel')) return e.id.split('.')[1];
    }

    return null;
}

/*****************************************************************************/
/*
/* EVENTS
/*
/*****************************************************************************/

/*****************************************************************************/
Wsf_Ctrls_Menu.prototype.onClickItem = function (ev, LI)
{
    Event.stop(ev);

    if ($(LI).hasClassName("disabled")) return;
    if ($(LI).hasClassName("expandable")) return;

    if (this.isContextMenu) this.hideContextMenu();

    // Notify

    var params = LI.getAttribute('data-meta');
    params = params ? params.evalJSON() : {};
    params.checked = !LI.hasClassName('checked');

    this.raiseEvent(Wsf_Ctrls_Menu.EVENT_ITEM_CLICKED,
        {
            sender: this,
            id: LI.cmdId,
            params: params
        });
}

/*****************************************************************************/
/*
/* CONTEXT MENU
/*
/*****************************************************************************/

/*******************************************************************************/
Wsf_Ctrls_Menu.prototype.trackContextMenu = function(x, y, callerH)
{
    this.isContextMenu = true;
    this.isWithin = false;

    this.elem.style.left = x + "px";
    this.elem.style.top = y + "px";
    this.show(true);
    this.focus();

    // Avoid overflow bellow window and to the right 
    var screen = document.viewport.getDimensions();
    var h = this.elem.down('ul').offsetHeight;
    if (y + h - screen.height > 0) this.elem.style.top = y-h-callerH + "px";
    var w = this.elem.down('ul').offsetWidth;
    if (x + w - screen.width > 0) this.elem.style.left = x-w + "px";

    this.fnTcmOnClickDoc = this._tcmOnClickDoc.bindAsEventListener(this);
    this.fnTcmOnClickMenu = this._tcmOnClickMenu.bindAsEventListener(this);
    this.fnTcmOnKeyDown = this._tcmOnKeyDown.bindAsEventListener(this);

    Event.observe(document, "mousedown", this.fnTcmOnClickDoc);
    Event.observe(this.elem, "mousedown", this.fnTcmOnClickMenu);
    Event.observe(document, "keydown", this.fnTcmOnKeyDown);

    this.updateUI();
}

/*******************************************************************************/
Wsf_Ctrls_Menu.prototype.hideContextMenu = function()
{
    Event.stopObserving(document, "mousedown", this.fnTcmOnClickDoc);
    Event.stopObserving(this.elem, "mousedown", this.fnTcmOnClickMenu);
    Event.stopObserving(document, "keydown", this.fnTcmOnKeyDown);
    this.fnTcmOnClickDoc = null;
    this.fnTcmOnClickMenu = null;
    this.fnTcmOnKeyDown = null;
    this.show(false);
    this.isContextMenu = false;
}

/*****************************************************************************/
Wsf_Ctrls_Menu.prototype._tcmOnClickDoc = function (event)
{
    if (!this.isWithin) {
        this.hideContextMenu();
        this.raiseEvent(Wsf_Ctrls_Menu.EVENT_CANCEL, {sender:this});
    }
    this.isWithin = false;
}

/*****************************************************************************/
Wsf_Ctrls_Menu.prototype._tcmOnClickMenu = function (event)
{
    this.isWithin = true;
}

/*******************************************************************************/
Wsf_Ctrls_Menu.prototype._tcmOnKeyDown = function (ev)
{
    if (ev.keyCode == Event.KEY_ESC) {
        this.hideContextMenu();
        this.raiseEvent(Wsf_Ctrls_Menu.EVENT_CANCEL, {sender:this});
    }
}

/*******************************************************************************/
/*
/* Expanding subitems
/*
/*******************************************************************************/

/*****************************************************************************/
function WsfCtrlMenu_DelayedHide(LI)
{
    var subItemsUl = $(LI).down().next('ul');
    var TitleDIV = $(LI).down();
    TitleDIV.removeClassName("exp");
    subItemsUl.hide();

    window.clearTimeout(subItemsUl.nTimeID);
    subItemsUl.nTimeID = null;
}

/*****************************************************************************/
function WsfCtrlMenu_DelayedShow(LI)
{
    var subItemsUl = $(LI).down().next('ul');
    var TitleDIV = $(LI).down();
    TitleDIV.addClassName("exp");
    subItemsUl.show();

    window.clearTimeout(subItemsUl.nTimeID);
    subItemsUl.nTimeID = null;
}

/*****************************************************************************/
Wsf_Ctrls_Menu.prototype.LI_OnMouseOver = function (ev, LI)
{
    if ((!LI.hasClassName("clickable") && !LI.hasClassName("expandable")) || LI.hasClassName("disabled")) return;

    var subItemsUl = LI.down().next('ul');
    if (!subItemsUl) return;

    if (subItemsUl.nTimeID) {
        window.clearTimeout(subItemsUl.nTimeID);
        subItemsUl.nTimeID = null;
    }
    else {
        subItemsUl.nTimeID = window.setTimeout(function() { WsfCtrlMenu_DelayedShow(LI); }, 150);
    }
}

/*****************************************************************************/
Wsf_Ctrls_Menu.prototype.onExpandItem = function (ev, li)
{
    var subItemsUl = li.down().next();
    if (subItemsUl.nTimeID) {
        window.clearTimeout(subItemsUl.nTimeID);
        subItemsUl.nTimeID = null;
    }

    WsfCtrlMenu_DelayedShow(li);
}

/*****************************************************************************/
Wsf_Ctrls_Menu.prototype.LI_OnMouseOut = function (ev, LI)
{
    if (LI.hasClassName("disabled")) return;

    var subItemsUl = LI.down().next('ul');
    if (!subItemsUl) return;

    if (subItemsUl.nTimeID) {
        window.clearTimeout(subItemsUl.nTimeID);
        subItemsUl.nTimeID = null;
    }
    else {
        subItemsUl.nTimeID = window.setTimeout(function() { WsfCtrlMenu_DelayedHide(LI); }, 500);
    }
}


function Wsf_UIMods_Users_CtrlLoginLogout(state)
{
    Wsf_Ctrls_Control.call(this, state);
}

Wsf.extend(Wsf_UIMods_Users_CtrlLoginLogout, Wsf_Ctrls_Control);

Wsf_UIMods_Users_CtrlLoginLogout.prototype.onCreate = function ()
{
    Wsf_Ctrls_Control.prototype.onCreate.call(this);

    var f = function (event)
    {
        event.stop();
        this.callServerMethod('onLogin');
    }

    var e = $(this.getId()+'.login');
    if (e) e.observe('click', f.bindAsEventListener(this));
}
Wsf_Web_Vocabulary.add({"Web" : {},"Wsf_Ctrls" : {"noname" : "<no name>"},"Wsf" : {},"Wsf_UIMods_Users" : {},"Wsf_Web" : {"ajax_server_error" : "Error while communicating with the server. Try to repeat the action or refresh the page (the F5 key).","ajax_fatal_error" : "Web application error. The application will be reloaded.","ajax_no_session" : "Your session expired. You will be redirected to the login page.","loading" : "Working"}});


