/*
 * 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;
    },

    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.body.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 x in o) return false;
        return true;
    }
}


/**
* 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);
    },

    /**
    * @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;
    }
};

/**
* Formatting helpers
*/
Wsf.Format = {
    createEnumRenderer : function (data) {
        return function (value) {
            return data[value];
        }
    },

    createBoolRenderer : function (trueText, falseText) {
        return function (value) {
            return value ? trueText : falseText;
        }
    }
}
// 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));
        }
    }
}


/**
* 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;

    // RSH

    if (window.dhtmlHistory) {
        window.dhtmlHistory.create({
            toJSON: function(o) { return Object.toJSON(o); },
            fromJSON: function(s) { return s.evalJSON(); }
        });
        window.dhtmlHistory.initialize();
    }

    // ...

    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');

    // 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)
{
    var method = "onUpdate" + cmdId;

    var a = sourceCtrl.getCmdTarget();

    for (; a; a=a.getParent()) {
        if (a[method] != undefined) {
            a[method](CmdUI);
            break;
        }
        else if (a.onUpdateCmdUI(cmdId, CmdUI))
            break;
    }
}

/*******************************************************************************/
Wsf_App.prototype.invokeCmd = function (sourceCtrl, cmdId, params)
{
    // Ensure cmd is enabled

    var cmdUI = {
        isEnabled: true,
        enable: function (isEnabled) { this.isEnabled = isEnabled; },
        check: function (isChecked) { }
    };

    this.invokeUpdateCmd(sourceCtrl, cmdId, cmdUI);
    if (!cmdUI.isEnabled) return;

    // Handle on client
    // Try concrete OnXYZ() handler and then generic OnCommand() handler

    var method = "on" + cmdId;
    var cmdTarget = sourceCtrl.getCmdTarget();
    params.sender = sourceCtrl;
    params.cmdId = cmdId;

    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.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) { if (onSuccess) onSuccess(transport.responseText ? transport.responseText.evalJSON() : null); },

         onFailure:
            onFailure ?
               function (transport) { onFailure(transport.responseText ? transport.responseText.evalJSON() : null); } :
               function (transport) { alert(transport.responseText ? transport.responseText.evalJSON() : null); }
      });
}

/*******************************************************************************/
/*
/* HISTORY
/*
/*******************************************************************************/

/*****************************************************************************/
Wsf_App.prototype._evalHashJson = function()
{
    var h = window.location.hash.substr(1);
    try {
        h = h ? h.evalJSON() : {};
    }
    catch (e) {
        h = {};
    }
    return h;
}

/**
* Called when the URL's hash part has changed.
*/
Wsf_App.prototype._onHashChanged = function()
{
    this._rootCtrl.onHashChanged(this._evalHashJson());
}

/**
* Sets the URL's hash which raises the onHashEvent()
* Use for navigation in AJAX application
*/
Wsf_App.prototype.navigateHash = function (params, append)
{
    if (append) {
        var newHash = this._evalHashJson();
        for (x in params) newHash[x] = params[x];
    }
    else {
        var newHash = params;
    }
    window.location.hash = Wsf.isObjectEmpty(newHash) ? '#' : '#'+Object.toJSON(newHash);
}


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.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]();
}

/*******************************************************************************/
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())
*/
Wsf_Ctrls_Control.prototype._addClass = function (className)
{
    this._classes.push(className);
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.isA = function (className)
{
    return this._classes.indexOf(className) != -1;
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.getAncestorByClass = function (className)
{
    for (var o=this; o; o=o._parent) {
        if (o.isA(className)) 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 CtrlFrame 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('CtrlFrame') != -1) return o;
        }
    }
    return null;
}

/**
* Control's HTML title.
* @return string
*/
Wsf_Ctrls_Control.prototype.getTitle = function ()
{
    return this._title;
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.getId = function ()
{
    return this._id;
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.getParent = function ()
{
    return this._parent;
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.getChildByIdx = function (idx)
{
    return this._children[idx];
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.getChildrenCount = function ()
{
    return this._children.length;
}

/*******************************************************************************/
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._enableInternal(enable);
}

/*******************************************************************************/
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.isEnabled = function ()
{
    return this._enabled;
}

/*******************************************************************************/
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;
}

/*******************************************************************************/
Wsf_Ctrls_Control.prototype.callServerMethod = function (method, params, fnOnSuccess, fnOnFailure)
{
    return Wsf_Web_CallServerMethodHandler.invoke(this, method, params, fnOnSuccess, fnOnFailure);
}


/*******************************************************************************/
/*
/* 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!
*/
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 offset size: w/h = margin+border+padding+content
*/
Wsf_Ctrls_Control.prototype.setSize = function (w, h)
{
    var s = this.elem.style;
    var b = 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();
}

/*******************************************************************************/
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)
{
    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('.');

      for (var i=0,n=this._vocabularies.length; i < n; ++i)
      {
         var ns = this._vocabularies[i][a[0]];

         if (ns)
         {
            var w = ns[a[1]];
            if (w) return w;
         }
      }

      return "["+id+"]";
   }
};

/*****************************************************************************/
function W(id)
{
   return Wsf_Web_Vocabulary.getWord(id);
}


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

    var 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
        }
    };

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

    // GO!

    if (async) {
        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] = { filename: elem.files[0].fileName, fileObject: 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) {
            // HACK: prototype.js is modified to use sendAsBinary() FF specific function to handle binary file uploads
            var boundary = "wsfAjax--------------" + (new Date).getTime();
            new Ajax.Request(document.location.href, {
                asynchronous: true,
                method: 'post',
                encoding: null, // default is UTF-8 which as appended to contentType, but this is an error for multipart
                contentType: "multipart/form-data; boundary=" + boundary,
                postBody: this._buildMessage(postParams, boundary),
                wsfSendAsBinary: true,
                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)
            });
        }
    }
    else {
        $('wsfState').value = postParams.wsfState;
        $('wsfForm').submit();
        this.done();
    }
}

/*******************************************************************************/
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;
}

/*******************************************************************************/
Wsf_Web_CallServerMethodHandler.prototype._buildMessage = function(postParams, boundary)
{
    var parts = [];

    for (var name in postParams) {
        var part  = "";
        var param = postParams[name];

        if (typeof(param) == "object") {
            if (Object.isArray(param)) {
                alert("Wsf_Web_CallServerMethodHandler.prototype._buildMessage: NOT IMPLEMENTED");
            }
            else {
                part += 'Content-Disposition: form-data; ';
                part += 'name="' + name + '"; ';
                part += 'filename="'+ Wsf_Web_CallServerMethodHandler._encodeUtf8(param.filename) + '"' + "\r\n";
                part += "Content-Type: application/octet-stream" + "\r\n\r\n";
                // TODO: getAsBinary is FF only. Use FileReader.
                part += param.fileObject.getAsBinary() + "\r\n";
            }
        } else {
            part += 'Content-Disposition: form-data; ';
            part += 'name="' + name + '"' + "\r\n\r\n";
            //part += "Content-Type: text/plain; charset=UTF-8" + "\r\n\r\n";
            part += Wsf_Web_CallServerMethodHandler._encodeUtf8(param) + "\r\n";
        }

        parts.push(part);
    };

    var request = "--" + boundary + "\r\n";
    request += parts.join("--" + boundary + "\r\n");
    request += "--" + boundary + "--" + "\r\n";
    return request;
}

/**
* 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)
{
    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();
    }
    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);
                o.onStateChanged(response.ctrlsState[id]);
            }
        }

        // 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 Web_ViewMainFrame(state)
{
    Wsf_Ctrls_ViewHtml.call(this, state);
}

Wsf.extend(Web_ViewMainFrame, Wsf_Ctrls_ViewHtml);

/*******************************************************************************/
Web_ViewMainFrame.prototype.isUserLogged = function ()
{
    return this.userLogged;
}



/*******************************************************************************/
function Wsf_Ctrls_Menu()
{
    Wsf_Ctrls_Control.call(this);

    this.aItemElems = [];
    this._isAnyItemEnabled = false;
}

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;

/*******************************************************************************/
Wsf_Ctrls_Menu.prototype.initFromMarkup = function ()
{
    Wsf_Ctrls_Control.prototype.initFromMarkup.call(this);
    this.elem.descendants().each(this._decorateElem.bind(this));
}

/*******************************************************************************/
Wsf_Ctrls_Menu.prototype._decorateElem = function (e)
{
    if (e.tagName != "LI") return;

    this.aItemElems.push(e);

    var a = e.id.split('.');
    e.cmdId = a.length == 2 ? a[1] : null;

    if (this._isClientExpandMode) {
        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));
    }
}

/*******************************************************************************/
Wsf_Ctrls_Menu.prototype.getControlState = function ()
{
    var a = Wsf_Ctrls_Control.prototype.getControlState.apply(this);

    a.aSelIds = [];

    for (var i=this.aItemElems.length; i--;) {
        var e = this.aItemElems[i];
        if (e.hasClassName('sel')) a.aSelIds.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.aItemElems.length; i--;)
    {
        var e = this.aItemElems[i];
        var cmdId = e.id.split('.')[1];
        if (!cmdId) continue;
        CmdUI.isEnabled = true;
        CmdUI.isChecked = false;

        g_app.invokeUpdateCmd(this.getCmdTarget(), cmdId, CmdUI);

        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.aItemElems.length; i--;)
    {
        var e = this.aItemElems[i];

        if (id == e.id.split('.')[1])
        {
            if (isSelect)
                e.addClassName('sel');
            else
                e.removeClassName('sel');
            break;
        }
    }
}

/*****************************************************************************/
Wsf_Ctrls_Menu.prototype.getSelId = function ()
{
    for (var i=this.aItemElems.length; i--;)
    {
        var e = this.aItemElems[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(nX, nY)
{
    this.isContextMenu = true;
    this.isWithin = false;

    this.elem.style.left = nX + "px";
    this.elem.style.top = nY + "px";
    this.show(true);
    this.focus();

    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.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();
    }
}

/*******************************************************************************/
/*
/* 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_Forms_CtrlForm()
{
    Wsf_Ctrls_Control.call(this);
    this._addClass('Wsf_Forms_CtrlForm');
}

Wsf.extend(Wsf_Forms_CtrlForm, Wsf_Ctrls_Control);

/*******************************************************************************/
Wsf_Forms_CtrlForm.prototype.initFromMarkup = function ()
{
    Wsf_Ctrls_Control.prototype.initFromMarkup.call(this);
    this.elem.observe('keydown', this._onKeyDown.bindAsEventListener(this));
}

/*******************************************************************************/
Wsf_Forms_CtrlForm.prototype._onKeyDown= function (ev)
{
    if (ev.keyCode == Event.KEY_RETURN) {
        Event.stop(ev);
        if (this.defaultCmdId) g_app.invokeCmd(this, this.defaultCmdId, {});
    }
}

/*******************************************************************************/
Wsf_Forms_CtrlForm.prototype.onStateChanged = function (state)
{
    Wsf_Ctrls_Control.prototype.onStateChanged.call(this, state);

    if (this.ensureInfoBoxVisible) {
        this.infoBox.getElem().scrollIntoView();
        this.ensureInfoBoxVisible = false;
    }
}

/*******************************************************************************/
function Wsf_UIMods_Search_FormFulltextSearch()
{
    Wsf_Forms_CtrlForm.call(this);
}

Wsf.extend(Wsf_UIMods_Search_FormFulltextSearch, Wsf_Forms_CtrlForm);

/*******************************************************************************/
Wsf_UIMods_Search_FormFulltextSearch.prototype.getInputElement = function ()
{
    return this.getElem().down('input');
}

function Wsf_Forms_Control() { Wsf_Ctrls_Control.call(this); } Wsf.extend(Wsf_Forms_Control, Wsf_Ctrls_Control);

/*******************************************************************************/
function Wsf_Forms_CtrlText()
{
    Wsf_Forms_Control.call(this);
    this._focusable = true;
    this._lastDelayedChangeTimerId = null;
}

Wsf.extend(Wsf_Forms_CtrlText, Wsf_Forms_Control);

/*******************************************************************************/
Wsf_Forms_CtrlText.prototype.initFromMarkup = function ()
{
    Wsf_Forms_Control.prototype.initFromMarkup.call(this);

    this.inputElem = this.getElem().down('input');
    this.inputElem.observe('keyup', this.onKeyUp.bindAsEventListener(this));
    this.inputElem.observe('keydown', this.onKeyDown.bindAsEventListener(this));

    // IE passes clicks through the transparent <input> to the label <span>
    if (Prototype.Browser.IE) {
        var e = this.getElem().down('span');
        if (e) e.observe('click', function () { this.focus() }.bind(this));
    }
}

/*******************************************************************************/
Wsf_Forms_CtrlText.prototype.getControlState = function ()
{
    if (this.getStyle('showLabelInside') && !this._hasFocus && this._empty) {
        this.value = "";
    }
    else this.value = this.inputElem.value;

    return Wsf_Forms_Control.prototype.getControlState.call(this);
}

/*******************************************************************************/
Wsf_Forms_CtrlText.prototype.enable = function (enable)
{
    this.inputElem.disabled = !enable;
    Wsf_Forms_Control.prototype.enable.call(this, enable);
}

/*******************************************************************************/
Wsf_Forms_CtrlText.prototype.focus = function ()
{
    if (!this.isVisible() || !this.isEnabled()) return false;
    this.inputElem.focus();
    this.getView().onFocusChanged(this);
    return true;
}

/*******************************************************************************/
Wsf_Forms_CtrlText.prototype.setValue = function (s)
{
    this.inputElem.value = s === null ? '' : s;
}

/*******************************************************************************/
Wsf_Forms_CtrlText.prototype.getValue = function ()
{
    return this.inputElem.value;
}

/*******************************************************************************/
Wsf_Forms_CtrlText.prototype.onKeyDown = function (ev)
{
}

/*******************************************************************************/
Wsf_Forms_CtrlText.prototype.onKeyUp = function (ev)
{
    var last = this.value;
    this.value = this.inputElem.value;

    if (this.getStyle('showLabelInside')) {
        if (this.inputElem.value!='') {
            this.getElem().down('span').hide();
        }
        else {
            this.getElem().down('span').show();
        }
    }

    if (this.value != last) {
        window.clearTimeout(this._lastDelayedChangeTimerId);
        this._lastDelayedChangeTimerId = this.raiseEvent.bind(this, 'delayedChange', { sender: this }).delay(0.4);
    }
}

/*******************************************************************************/
function Wsf_Forms_CtrlButton()
{
    Wsf_Ctrls_Control.call(this);
    this._focusable = true;
}

Wsf.extend(Wsf_Forms_CtrlButton, Wsf_Ctrls_Control);

/*******************************************************************************/
Wsf_Forms_CtrlButton.prototype.initFromMarkup = function ()
{
    Wsf_Ctrls_Control.prototype.initFromMarkup.call(this);
    this.getElem().observe('click', this.onClick.bind(this));
    this.getElem().observe('keydown', this.onKeyDown.bind(this));
}

/*******************************************************************************/
Wsf_Forms_CtrlButton.prototype.check = function (isChecked)
{
    if (isChecked)
        this.elem.addClassName('checked');
    else
        this.elem.removeClassName('checked');
}

/*******************************************************************************/
Wsf_Forms_CtrlButton.prototype.isChecked = function ()
{
    return this.elem.hasClassName('checked');
}

/*******************************************************************************/
Wsf_Forms_CtrlButton.prototype.onKeyDown = function (e)
{
    if (e.keyCode == Event.KEY_RETURN || e.keyCode == 32) {
        e.stop();
        if (this.isEnabled()) this.raiseEvent('click', { sender: this });
    }
}

/*******************************************************************************/
Wsf_Forms_CtrlButton.prototype.onClick = function (e)
{
    e.stop();
    if (this.isEnabled()) this.raiseEvent('click', { sender: this });
}

/**
* A button issuing a command when clicked.
* Cmd event params
* - checked: bool - if the button is a check-button, it's the check state after the click
*/
function Wsf_Ctrls_CmdButton(state)
{
    Wsf_Forms_CtrlButton.call(this, state);
}

Wsf.extend(Wsf_Ctrls_CmdButton, Wsf_Forms_CtrlButton);

/*******************************************************************************/
Wsf_Ctrls_CmdButton.prototype.onCreate = function ()
{
    Wsf_Forms_CtrlButton.prototype.onCreate.call(this);
    this.attachEventHandler('click', this._onClick.bind(this));
}

/*******************************************************************************/
Wsf_Ctrls_CmdButton.prototype._onClick = function (ed)
{
    var p = this.cmdParams ? this.cmdParams : { };
    p.checked = !this.isChecked();
    g_app.invokeCmd(this, this.cmdId, p);
}

/*******************************************************************************/
Wsf_Ctrls_CmdButton.prototype.updateUI = function ()
{
    var cmdui = {
        ctrl : this,
        isEnabled : true,
        isChecked : false,
        enable: function (enable) { this.ctrl.enable(enable); },
        check: function (check) { this.ctrl.check(check); }
    };

    g_app.invokeUpdateCmd(this, this.cmdId, cmdui);
}


/*******************************************************************************/
function Wsf_Forms_CtrlComboBox()
{
    Wsf_Ctrls_Control.call(this);
    this._modified = false;
    this._focusable = true;
}

Wsf.extend(Wsf_Forms_CtrlComboBox, Wsf_Ctrls_Control);

Wsf_Forms_CtrlComboBox.EVENT_CHANGE = 'change';

/*******************************************************************************/
Wsf_Forms_CtrlComboBox.prototype.initFromMarkup = function ()
{
    Wsf_Ctrls_Control.prototype.initFromMarkup.call(this);
    this.getElem().observe('change', this.raiseEvent.bind(this, 'change', { sender: this }));
}

/*******************************************************************************/
Wsf_Forms_CtrlComboBox.prototype.addItem = function (title, id)
{
    var o = document.createElement('option');
    o.text = title;
    o.value = id;

    try {
        this.elem.add(o, null); // standards compliant
    }
    catch (ex) {
        this.elem.add(o); // IE
    }

    this._modified = true;
}

/*******************************************************************************/
Wsf_Forms_CtrlComboBox.prototype.removeItemById = function (id)
{
    var c = this.elem.options;

    // Start at 1 to skip the no-selection item
    for (var i=1; i < this.elem.length; ++i) {
        if (c[i].value == id) {
            this.elem.remove(i);
            this._modified = true;
            break;
        }
    }
}

/*******************************************************************************/
Wsf_Forms_CtrlComboBox.prototype.getSelId = function ()
{
    return this.elem.value;
}

/*******************************************************************************/
Wsf_Forms_CtrlComboBox.prototype.getValue = function ()
{
    return this.elem.value;
}

/*******************************************************************************/
Wsf_Forms_CtrlComboBox.prototype.setValue = function (value)
{
    this.elem.value = value === null ? '' : value;
    return this;
}

/*******************************************************************************/
Wsf_Forms_CtrlComboBox.prototype.getControlState = function ()
{
    var a = Wsf_Ctrls_Control.prototype.getControlState.call(this);
    if (!this._modified) return a;

    a.options = [];

    var c = this.elem.options;

    // Start at 1 to skip the no-selection item
    for (var i=1; i < this.elem.length; ++i) {
        a.options.push({ id: c[i].value, title: c[i].text });
    }

    return a;
}

/*******************************************************************************/
function Wsf_UIMods_Users_FormLogin(state)
{
    Wsf_Forms_CtrlForm.call(this, state);
}

Wsf.extend(Wsf_UIMods_Users_FormLogin, Wsf_Forms_CtrlForm);

/*******************************************************************************/
Wsf_UIMods_Users_FormLogin.prototype.onCreate = function ()
{
    Wsf_Forms_CtrlForm.prototype.onCreate.call(this);

    var w = this.getWindow();
    if (w) {
        w.setHeightToFitContent();
        this.email.focus();
    }
}

/*******************************************************************************/
Wsf_UIMods_Users_FormLogin.prototype.onLogin = function ()
{
    var f = function (url)
    {
        if (url !== false)
            g_app.navigate(url);
        else {
            var w = this.getWindow();
            if (w) {
                w.setHeightToFitContent();
                w.shake();
            }
        }
    }

    this.callServerMethod('onLogin', {}, f.bind(this));
}

Wsf_Web_Vocabulary.add({"Web" : {"MUST_LOGIN" : "K provedení této akce musíte být přihlášeni."},"Wsf_Ctrls" : {},"Wsf" : {},"Wsf_UIMods_Search" : {"NOT_FOUND" : "Žádné stránky nebyly nalezeny"},"Wsf_Forms" : {},"Wsf_UIMods_Users" : {},"Wsf_Web" : {"AJAX_SERVER_ERROR" : "Chyba při komunikaci se serverem. Zkuste akci opakovat nebo obnovit stránku (klávesa F5).","AJAX_FATAL_ERROR" : "Chyba webové aplikace. Aplikace bude znovu načtena.","AJAX_NO_SESSION" : "Vaše relace vypršela. Budete přesměrováni na přihlašovací stránku.","LOADING" : "Pracuji"}});


