/**
 *  Prototype Xtensions 
 *  
 *  @author    Simon Martins
 *  @copyright (c) 2008 Netatoo SARL <http://www.netatoo.fr>
 *  @license   MIT License <http://www.prototypextensions.com/#main=license>
 * 
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 *  THE SOFTWARE.
 *
 */
 
/**
 * Prototype.X requirements
 */
Prototype.X = {
    Version   : '0.1.2',
    Required  : {
        Prototype : '1.6.0.2',
        Scripty   : '1.8.1'
    },

    Require   : function() {
        if(typeof Prototype       == 'undefined' 
        || typeof Element         == 'undefined'
        || typeof Element.Methods == 'undefined'
        || this.convertVersionString(Prototype.Version  || 0) < 
           this.convertVersionString(this.Required.Prototype)
        ) {
            throw( "Prototype.Xtensions require Prototype.js >= " 
             + this.Required.Prototype );
        } 
        
        if(typeof Scriptaculous   == 'undefined' 
        || typeof Effect          == 'undefined'
        || this.convertVersionString(Scriptaculous.Version || 0) < 
           this.convertVersionString(this.Required.Scripty)
        ) {
            throw( "Prototype.Xtensions require Scriptaculous >= " 
             + this.Required.Scripty );
        }
    },
    
    convertVersionString: function (versionString) {
        var v = versionString.replace(/_.*|\./g, '');
        v = parseInt(v + '0'.times(4-v.length));
        return versionString.indexOf('_') > -1 ? v-1 : v;
    }
};

Prototype.X.Require();

/**
 * Create registry for self components
 */
Prototype.X.Registry = {};

/**
 * Prototype.X.Browser
 *
 * @desc Used to retrieve the browser version
 */
(function() {
    var nav       = navigator;
    var userAgent = ua = navigator.userAgent;
    var v         = nav.appVersion;
    var version   = parseFloat(v);

    Prototype.X.Browser = {
        IE      : (Prototype.Browser.IE)    ? parseFloat(v.split("MSIE ")[1]) || 0 : 0,
        Firefox : (Prototype.Browser.Gecko) ? parseFloat(ua.split("Firefox/")[1]) || 0 : 0,
        Camino  : (Prototype.Browser.Gecko) ? parseFloat(ua.split("Camino/")[1]) || 0 : 0,
        Flock   : (Prototype.Browser.Gecko) ? parseFloat(ua.split("Flock/")[1]) || 0 : 0,
        Opera   : (Prototype.Browser.Opera) ? version : 0,
        AIR     : (ua.indexOf("AdobeAIR") >= 0) ? 1 : 0,
        Mozilla : (Prototype.Browser.Gecko || !this.Khtml) ? version : 0,
        Khtml   : (v.indexOf("Konqueror") >= 0 && this.safari) ? version : 0,
        Safari  : (function() {
            var safari = Math.max(v.indexOf("WebKit"), v.indexOf("Safari"), 0);
            return (safari) ? (
                parseFloat(v.split("Version/")[1]) || ( ( parseFloat(v.substr(safari+7)) >= 419.3 ) ? 3 : 2 ) || 2
            ) : 0;
        })()
    };
})();

/**
 * $unixTime
 *
 * @desc Easy recovering of UNIX timestamp
 */
$unixTime = function(inSeconds) { 
    var time = new Date();
    time = time.getTime();
    if(inSeconds == true) time = time / 1000;
    return time; 
};

/**
 * String extension
 */
Object.extend(String.prototype, 
{
    /**
     * (string).ucfirst()
     *
     * @desc Make a string's first character uppercase
     */
    ucfirst: function() {
        return this.charAt(0).toUpperCase() + this.substring(1);
    }
});

/**
 * Number extension
 */
Object.extend(Number.prototype, 
{
    /**
     * (Number).hour()
     *
     * @desc Return the multiplication of 'Number' per hour
     */
    minute: function() {
        return this * 60;
    },
    
    /**
     * (Number).hour()
     *
     * @desc Return the multiplication of 'Number' per hour
     */
    hour: function() {
        return this * 3600;
    },
    
    /**
     * (Number).day()
     *
     * @desc Return the multiplication of 'Number' per day
     */
    day: function() {
        return this * 86400;
    },
    
    /**
     * (Number).week()
     *
     * @desc Return the multiplication of 'Number' per week
     */
    week: function() {
        return this * 604800;
    },
    
    /**
     * (Number).month()
     *
     * @desc Return the multiplication of 'Number' per month
     */
    month: function() {
        return this * 2629743.83;
    },
    
    /**
     * (Number).year()
     *
     * @desc Return the multiplication of 'Number' per year
     */
    year: function() {
        return this * 31556926;
    },
    
    /**
     * (Number).time()
     *
     * @desc Return the unix time + Number
     */
    time: function() {
        return (this.toMs()) + $unixTime();
    },
    
    /**
     * (Number).toSeconds()
     *
     * @desc Convert milisecond to seconds
     */
    toSeconds: function() {
        return this * 1000;
    },
    
    /**
     * (Number).toSeconds()
     *
     * @desc Convert seconds to miliseconds
     */
    toMs: function() {
        return this / 1000;
    }
});

/**
 * Class extension
 */
Object.extend(Class, 
{
    /**
     * Class.accessors
     *
     * @desc Provides a method setOptions() you can create 
     * accessors for all options 'opt'.
     * And method createAccessors
     */
    accessors: {
        
        /**
         * createAccessors( object options )
         *
         * @desc Create accessors (get/set) for each options.
         */
        createAccessors: function(options, entries) {
            var entries = (!Object.isArray(entries)) ? Object.keys(entries) : entries;
            entries.each(function(name) {
                var cname = (name.camelize()).ucfirst();
                (function(name, that){
                    that['get' + cname] = function(){ 
                        return options[name];
                    };
                    that['set' + cname] = function(value){ 
                        options[name] = value; 
                        return that; 
                    };
                })(name, this);
            }, this);
        },
        
        /**
         * setOptions( object options )
         *
         * @desc Create accessors for each options (Included mechanism Object.extend)
         */
        setOptions: function (defaultOptions, userOptions, accessorize) {
            var access = (Object.isUndefined(accessorize)) ? false : accessorize;
            if (!defaultOptions) defaultOptions = { };
            Object.extend(defaultOptions, userOptions || { });
            if(accessorize) this.createAccessors(defaultOptions, defaultOptions);
        }
    }
});






// -------------------------------------------------------------------

/**
 * EventManager
 * 
 * @extend Class.accessors
 * @desc Easly event manager for create custom event on your own class
 */
var EventManager = Class.create({
        
    /**
     * Initialize
     *
     * @desc Set scope and events hash
     */
    initialize: function(scope) {
        this.scope  = scope;
        this.events = new Hash();
    },
    
    /**
     * addListener
     *
     * @desc Add event observer
     */
    addObserver: function(name) {
        return this.events.set(name, new Hash());
    },
    
    /**
     * observe
     *
     * @desc Add an callback for listener 'name'
     */
    observe: function(name, callback) {
        var observers = this.events.get(name);
        
        if(!observers) observers = this.addObserver(name);
        
        if(!Object.isFunction(callback)) {
            throw('EventManager.observe : callback must be an js function');
        }
        
        var i = this.events.get(name).keys().length;
        observers.set(i, callback.bind(this.scope));
        
        return this;
    },
    
    /**
     * notify
     *
     * @desc Launch all callbacks for listener 'name'
     */
    notify: function(name) {
        var observers = this.events.get(name);
        
        if(observers) {
            var args = $A(arguments).slice(1);
            observers.each(function(callback) {
                if(Object.isFunction(callback[1])) {
                    callback[1].apply(this.scope, args);
                }
            });
        }
        
        return this;
    }
    
});

// -------------------------------------------------------------------

/**
 * Cookie
 * 
 * @desc Set / Get / Unset cookie 
 */
var Cookie = {
    
    /**
     * Custom events
     *
     * @desc Instance of EventManager
     */
    events: new EventManager(this),
    
    /**
     * isEnabled()
     *
     * @desc Return true if cookie is enabled, false else
     */
    isEnabled: function() {
        this.set('enabled', 'ok');
        var test = this.get('enabled');
        this.unset('enabled');
        return (test == 'ok') ? true : false;
    },
    
    /**
     * observe()
     *
     * @desc Observe custom event on Cookie
     */
    observe: function(name, callback) {
        this.events.observe(name, callback);
        return this;
    },
    
    /**
     * set()
     *
     * @desc Set a cookie
     */
    set: function(name, value, options, noevents) {
        var expire = '';
        var o      = options || {};
        var path   = (o.path)   ? '; path=' + o.path : '; path=/';
        var domain = (o.domain) ? '; domain=' + o.domain : '';
        var secure = (o.secure == true) ? '; secure' : '';
        var date = new Date();
        
        if(Object.isNumber(o)) {
            date.setTime(date.getTime() + (o * 1000));
            expire = '; expires=' + date.toGMTString();
        } else if(o.expires) {
            date.setTime(date.getTime() + (o.expires * 1000));
            expire = '; expires=' + date.toGMTString();
        }
        
        document.cookie = name + "=" + value + expire + path + domain + secure;
        
        if(!noevents) this.events.notify('set', name, value, options);
        
        return this;
    },
    
    /**
     * get()
     *
     * @desc Get a cookie
     * @attribution http://www.quirksmode.org/js/cookies.html
     */
    get: function(name) {
        this.events.notify('get', name);
        
        var nameEQ = name + "=";
        var ca = document.cookie.split(';');
        for(var i = 0; i < ca.length; i++){
            var c = ca[i];
            while(c.charAt(0) == ' ')
            c = c.substring(1,c.length);
            if(c.indexOf(nameEQ) == 0)
            return c.substring(nameEQ.length,c.length);
        }
        return null;
    },
    
    /**
     * get()
     *
     * @desc Unset a cookie
     */
    unset: function(name) {
        this.events.notify('unset', name);
        this.set(name, '', -1, true);
        return this;
    } 
};

// -------------------------------------------------------------------

/**
 * History
 *
 * @desc Provides basic methods to manage the history browsing.
 */
var History = {
    __altered: false,
    __currentHash: null,
    __previousHash: null,
    __iframe: false,
    __title: false,
    
    /**
     * init()
     * @desc Initialize the hash. Call this method in first
     */
    init: function() {
        var inst  = this;
        var hash  = location.hash.substring(1);
        this.hash = $H(hash.toQueryParams());
        if(Prototype.X.Browser.Mozilla) {
        	hash = hash.replace("/", "%2F");
        }
        this.__currentHash  = hash;
        this.__previousHash = hash;

        this.__title = document.title;
        
        if(Prototype.Browser.IE && Prototype.X.Browser.IE < 8) {
            document.observe('dom:loaded', function(e) {
                if(!$('px-historyframe')) {
                    History.__iframe = new Element('iframe', {
                        name   : 'px-historyframe',
                        id     : 'px-historyframe',
                        src    : '',
                        width  : '0',
                        height : '0',
                        style  : {
                            visibility: 'hidden'
                        }
                    });
                    
                    document.body.appendChild(History.__iframe);
                    
                    History.setHashOnIframe(inst.hash.toQueryString());
                }
            });
        }
    },
    
    /**
     * set( string $name, string $value )
     *
     * @desc Set new value $value for parameter $name
     */
    set: function($name, $value) {
        this.__previousHash = this.hash.toQueryString();
        this.hash.set($name, $value);
        this.apply();
    },
    
    /**
     * get( string $name )
     *
     * @desc Get value parameter $name
     */
    get: function($name) {
        return this.hash.get($name);
    },
    
    /**
     * unset( string $name )
     *
     * @desc Unset parameter $name
     */
    unset: function($name) {
        this.hash.unset($name);
        this.apply();
    },
    
    /**
     * update()
     *
     * @desc Updates this.hash with the current hash
     */
    update: function() {
        this.__previousHash = this.hash.toQueryString();
        var hash = window.location.hash.substring(1);
        
        if(Prototype.X.Browser.Mozilla) {
        	hash = this.replaceSlash(hash);
        }
        
        // If IE, look in the iframe if the hash is updated
        if(Prototype.Browser.IE && Prototype.X.Browser.IE < 8 && this.__iframe ) {
            var hashInFrame = this.getHashOnIframe();
            
            if(hashInFrame != hash) {
                hash = hashInFrame;
            }
        }

        this.hash = $H(hash.toQueryParams());
        this.__currentHash = hash;
    },
    
    replaceSlash: function (text)
    {            
    	text = text.replace("/", "%2F");
        if(text.indexOf("/") != -1)
            text = this.replaceSlash(text);
        
        return text;
    },
    
    /**
     * apply()
     *
     * @desc Apply this.hash to location.hash
     */
    apply: function() {
        var newHash = this.hash.toQueryString();

        // set new hash
        window.location.hash = newHash;
        
        // If IE, apply new hash to frame for history    
        if(Prototype.Browser.IE && Prototype.X.Browser.IE < 8 && this.__iframe) {
            if(this.__currentHash != newHash) 
            {
                this.setHashOnIframe(newHash);   
            }
            else if(newHash != this.getHashOnIframe()) 
            {
                this.setHashOnIframe(newHash);    
            }
        }
    },

    /**
     * isAltered()
     *
     * @desc Return true if current hash is different of previous hash.
     * this.__altered allows to force the dispatch.
     */
    isAltered: function() {
        if(this.__altered == true) {
            return true;
        }
        this.__altered = false;

        return (History.__currentHash != History.__previousHash);
    },
    
    /**
     * setHashOnIframe()
     *
     * @use  For IE compatibility
     * @desc Set hash value on iframe
     */
    setHashOnIframe: function(hash) {
        try {
            var doc = History.__iframe.contentWindow.document;
            doc.open();
            doc.write('<html><body id="history">' + hash + '</body></html>');
            doc.close();
        } catch(e) {}
    },
    
    /**
     * getHashOnIframe()
     *
     * @use  For IE compatibility
     * @desc Get hash value on iframe
     */
    getHashOnIframe: function() {
        var doc = this.__iframe.contentWindow.document;
        if (doc && doc.body.id == 'history') {
            return doc.body.innerText;
        } else {
            return this.hash.toQueryString();
        }
    },
    
    /**
     * setTitle()
     *
     * @desc Set a new title for window
     */
    setTitle: function(title) {
        if(document.title) {
            document.title = title;
        }
    },
    
    /**
     * getTitle()
     *
     * @desc Return current window title
     */
    getTitle: function() {
        return this.__title;
    }
};

History.init();
 
/**
 * History.Registry
 *
 * @desc Used to register a callback for a parameter
 */
History.Registry = 
{
    /**
     * @desc Hash
     */
    hash : new Hash(),
    
    /**
     * set( string $config )
     *
     * @desc Set new value $historyId for parameter $config
     */
    set: function($config) {
    
        if(typeof($config) != 'object') {
            throw('History.Registry.set : $config must be an javascript object');
        } 
        
        // id
        if(!$config.id || !Object.isString($config.id)) {
            throw('History.Registry.set : $config.id must be an string');
        } 
        
        // onChange
        if(!$config.onStateChange || !Object.isFunction($config.onStateChange)) {
            throw('History.Registry.set : $config.onStateChange '
                + 'must be an javascript callback function');
        }
    
        // defaultValue
        if(!$config.defaultValue || !Object.isString($config.defaultValue)) {
            $config.defaultValue = '';
        }
                
        this.hash.set($config.id, $config);
    },
    
    /**
     * get( string $id )
     *
     * @desc Get value parameter $id
     */
    get: function($id) {
        return this.hash.get($id);
    },
    
    /**
     * unset( string $id )
     *
     * @desc Unset parameter $id
     */
    unset: function($id) {
        this.hash.unset($id);
    }
}

/**
 * History.Observer
 *
 * @desc Used to perform actions defined in the registry, 
 * according to the hash of the url.
 */
History.Observer = {

    /**
     * @desc Interval delay in seconds
     */
    delay : 0.2,
    
    /**
     * @desc Interval timer instance
     */
    interval : null,
    
    /**
     * @desc If interval is started : true, else false
     */
    started : false,
    
    /**
     * start()
     *
     * @desc Start a interval timer
     */
    start: function() {
        if(this.started) return;
        this.interval = new PeriodicalExecuter(History.Observer.dispatch, this.delay);
        this.started = true;
    },
    
    /**
     * stop()
     *
     * @desc Stop the interval timer
     */
    stop: function() {
        if(!this.started) return;
        this.interval.stop();
        this.started = false;
    },
    
    /**
     * dispatch()
     *
     * @desc This method is called each time interval, 
     * the dispatch of the registry is implemented only if 
     * the hash has been amended (optimisiation)
     */
    dispatch: function() {
    
        // Update the hash
        History.update();
            
        // Dispatch only if location.hash has been altered
        if(History.isAltered()) {
        
            //if(console) console.log('pass');
            History.hash.each(function(pair)  {
                var registry = History.Registry.get(pair.key);
                if(registry) {
                   registry.onStateChange.bind(History)( pair.value );
                }
            });
        }
    }
};

/**
 * Ajax.History
 *
 * @desc Provides core methods to easily manage browsing history 
 * with Ajax.History.Request / Updater.
 */
Ajax.History = {

    /**
     * @desc Allowed Ajax.History prefix (for validation)
     */
    types : ['Request', 'Updater'],
    
    /**
     * observe( string $type, string $id, string $url, object $options )
     *
     * @desc This method helps manage the browsing history
     */
    observe: function($type, $id, $url, $options) {
    
        var getter         = Ajax.History.Registry.get($id);
        var currentVersion = 0;
        var output         = false;
         
        // Type validation   
        if(this.types.indexOf($type) == -1) {
            throw('Ajax.History.observer() -> type ' + $type + ' is invalid !');
        }      

        // Registry management
        if(getter == undefined) {
            currentVersion = ($options.history.state) ? $options.history.state : 0;  
            var hash = new Hash();
            hash.set(currentVersion, $options);
            Ajax.History.Registry.set($id, hash);
        } else {
            currentVersion = ($options.history.state) 
                ? $options.history.state : this.getCurrentVersion($id);
            getter.set(currentVersion, $options);
        }
                
        // add handler on registry
        this.addCallback($type, $id);

        return currentVersion;
    },
    
    /**
     * addCallback( string $type, string $id )
     *
     * @desc This method adds a state for request on History.Registry
     */
    addCallback: function($type, $id) {
        
        History.Observer.start();
        
        // Set history altered state to true : force dispatch
        History.__altered = true;
        
        // Return void if registry is already set
        if(!Object.isUndefined(History.Registry.get($id))) return;  

        // Add this id to history registry
        History.Registry.set({
            id: $id,
            onStateChange: function(state) {
                var options = Ajax.History.Registry.get($id).get(state.toString());
                var request = null;
                   
                if(Object.isUndefined(options)) return;

                if(options.history.cache == true && options.history.__request) {
                    new Ajax.Cache(options.history.__request);
                } else {
                    
                    if($type == 'Request') {
                        request = new Ajax.Request(options.history.__url, options);
                    } else if($type == 'Updater') {
                        request = new Ajax.Updater(options.container, options.history.__url, options);
                    }
                    options.history.__request = request; 
                }
                
                History.__altered = false;
                
                if (Object.isFunction(options.history.onStateChange)) {
//                    options.history.onStateChange(state);
                }
            } 
        }); 
    },
    
    /**
     * getCurrentVersion( string $id )
     *
     * @desc This method returns the current state in history 
     * (if the state is not defined)
     */
    getCurrentVersion: function($id) {
        var getter = Ajax.History.Registry.get($id);        
        return Object.isUndefined(getter) ? 0 : getter.keys().length;
    }
};

/**
 * Ajax.History.Registry
 *
 * @desc Simple Hash to manage request history
 */
Ajax.History.Registry = new Hash();

/**
 * Ajax.History.Features
 *
 * @desc It's default request options for Ajax.History.Request/Updater
 */
Ajax.History.Features = {
    id              : null,    // set custom history value for this instance
    state           : false,   // set custom state value for this instance
    cache           : false,   // enable/disable history cache 
    onStateChange   : null,    // this handler is called when history change
    __url           : null,
    __request       : null
};

/**
 * Ajax.History.Request
 *
 * @desc Used to execute an Ajax.Request by integrating 
 * the management of browsing history
 */
Ajax.History.Request = Class.create({
    initialize: function($url, $options) {
    
        this.options = {};
        Object.extend(this.options, $options);
            
        // Set historing only if option is actived
        if(this.options.history) {
        
            var tmpOpt = Object.clone(Ajax.History.Features);
            Object.extend(tmpOpt, this.options.history);  
            this.options.history = tmpOpt;
            this.options.history.__url = $url;
            
            // History id
            if(Object.isUndefined($options.history.id)) {
                throw('Ajax.History.Request error : you must define historyId');
            } else {
                var id = this.options.history.id;
            }
            
            // Enable history observer
            var version = Ajax.History.observe('Request', id, $url, this.options);
            
            // Set current version value for container
            History.set(id, version);
            
        } else {
            return new Ajax.Request($url, this.options);
        }
    }
});

/**
 * Ajax.History.Updater
 *
 * @desc Used to execute an Ajax.Updater by integrating 
 * the management of browsing history
 */
Ajax.History.Updater = Class.create({
    initialize: function($container, $url, $options) {
        
        this.options = {};
        Object.extend(this.options, $options);

        // Set historing only if option is actived
        if(this.options.history) {
            
            var tmpOpt = Object.clone(Ajax.History.Features);
            Object.extend(tmpOpt, this.options.history);  
            this.options.history = tmpOpt;
            this.options.history.__url = $url;
                
            // History id
            if(Object.isUndefined($options.history.id)) {
                var id = (Object.isString($container)) ? $container : $container.id;
            } else {
                var id = this.options.history.id;
            }
            
            // Set $container to this.options (take it back then)
            this.options.container = $container;
            
            // Enable history observer
            var version = Ajax.History.observe('Updater', id, $url, this.options);
            
            // Set current version value for container
            History.set(id, version);
            
        } else {
            return new Ajax.Updater($container, $url, this.options);
        }
    }
});

/**
 * Ajax.Cache
 *
 * @desc Ajax.Cache can "simulate" an Ajax request from an 
 * Ajax.Request/Updater made beforehand. 
 */
Ajax.Cache = Class.create(Ajax.Base, {
    _complete: false,
    initialize: function($super, request) {
        $super(request.options);
        request._complete = false;
        this.transport = request.transport;
        this.request(request.url);
        return this;
    },
    
    request: function(url) {
        this.url = url;
        this.method = this.options.method;
        var params = Object.clone(this.options.parameters);
        
        try {
            var response = new Ajax.Response(this);

            if (this.options.onCreate) this.options.onCreate(response);
            Ajax.Responders.dispatch('onCreate', this, response);
                        
            if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1);
            
            this.onStateChange();
        }
        catch (e) {
            this.dispatchException(e);
        }
    }
});

Object.extend(Ajax.Cache.prototype, {
    respondToReadyState : Ajax.Request.prototype.respondToReadyState,
    onStateChange       : Ajax.Request.prototype.onStateChange,
    success             : Ajax.Request.prototype.getStatus,
    getStatus           : Ajax.Request.prototype.getStatus,
    isSameOrigin        : Ajax.Request.prototype.isSameOrigin,
    getHeader           : Ajax.Request.prototype.getHeader,
    evalResponse        : Ajax.Request.prototype.evalResponse,
    dispatchException   : Ajax.Request.prototype.dispatchException
});


/**
 * Ajax.Request Extended
 *
 * @desc Just a small change: now Ajax.Request return self scope.
 * It is required by Ajax.Cache
 */
Ajax.Request = Class.create(Ajax.Request, {
    initialize: function($super, url, options) {
        $super(url, options);
        return this;
    }
});

Ajax.Request.Events =
  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];

/**
 * Ajax.Updater Extended
 *
 * @desc Just a small change: now Ajax.Updater return self scope
 * It is required by Ajax.Cache
 */
Ajax.Updater = Class.create(Ajax.Updater, {
    initialize: function($super, container, url, options) {
        $super(container, url, options);
        return this;
    } 
});

// -------------------------------------------------------------------

/**
 * Create registry namespace for tabs
 */
Prototype.X.Registry.Tabs = {};

/**
 * Tabs( string|HTMLElement element, object options)
 * 
 * @extend Class.accessors
 * @desc Tabs is a class providing a set of method to manage a tab system. 
 * The purpose Tabs acting as the manager of objects TabsItem 
 * each representing a tab.
 */
var Tabs = Class.create(Class.accessors, {
    
    /**
     * Initialize
     *
     * @desc Sets options and create tabs TabsItem.
     */
    initialize: function(element, options) {
        var inst = this;
        
        // items registry
        this.items = [ ];
        
        // set options
        this.options = {
            element         : null,         // is parent element
            elementId       : null,         // is parent element id
            tabsId          : null,         // is uniq namespace id for tabs
            tabsContainer   : null,         // is <ul> for tabs
            domTabs         : {},           // is <ul> childs elements
            requests        : {},
            selectEvent     : 'click',      // is event type for select tabs
            defaultActive   : 0,            // is index of default active tab
            currentActive   : null,         // is index of current active tab
            history         : true,         // is dynamic hash with query params (simulate back and forward)
            sortable        : false,        // is sortable option (true : active sortable, false : inactive)
            sortableOptions : {             // is sortable options
                tag         : 'li', 
                overlap     : 'horizontal', 
                constraint  : 'horizontal'
            },
            deletable       : false 
        },
        this.setOptions(this.options, options, true);

        // declare some options
        this.setElement(element);
        this.setElementId(this.getElement().id);
        this.setTabsId(this.getElementId().camelize());
        
        // declare some variable
        var tabs   = this.getElement().firstDescendant().childElements();
        this.setDomTabs(tabs);
        this.setTabsContainer(this.getElement().firstDescendant());
        
        // builds items
        this.__buildItems();
        
        // select default tab (or get active tab on history)
        this.__selectByDefault();
    },
    
    // ----------------------------------------------
    
    /**
     * __buildItems()
     * 
     * @desc This method is called on init and she's private.
     * It can create TabsItem instance for each tab.
     */
    __buildItems: function() {
        var inst   = this;
        var panels = this.getElement().childElements();
        var tabs   = this.getDomTabs();
        
        // shift first child (tabs ul)
        panels.shift();
        
        // create and save tabs item on registry
        tabs.each(function(elem, index) {
            if(!inst.items[index]) {
                var requestOptions = {};
                var seekedOptions  = inst.options.requests[panels[index].id];
                
                // get request options if exist
                if(seekedOptions) {
                    requestOptions = seekedOptions;
                }
        
                inst.items[index] = new TabsItem(inst, {
                    item    : elem,
                    panel   : panels[index],
                    request : requestOptions,
                    effect  : inst.effect
                });
            }
        });
    },
    
    /**
     * __selectByDefault()
     * 
     * @desc This method is called on init and she's private.
     * It allows you to select the tab defines default from 
     * the option 'defaultActive', and from browsing history 
     * if the option 'history' is activated.
     */
    __selectByDefault: function() {
        var inst = this;
        
        // options.history
        if(this.getHistory()) {
        
            // start history observer
            History.Observer.start();
            
            // set handler for this instance
            History.Registry.set({
                id: this.getTabsId(),
                onStateChange: function(tabName) {
                    var getter = inst.get(tabName, false);
                    
                    if(getter) {
                        // fix for opera history management
                        if(Prototype.Browser.Opera)
                            getter.__select();
                        else
                            getter.select();
                    }
                    
                    this.setTitle(this.getTitle() + ' (' + tabName.ucfirst() + ')');
                }
            });
            
            var value = History.get(this.getTabsId());
            var tabs  = this.get(value, false);
                            
            if(value && tabs) {
                tabs.select();
            } else {
                this.get(this.getDefaultActive()).select();
            }
            
        } else {
            this.get(this.getDefaultActive()).select();
        }
    },
      
    /**
     * makeSortable()
     * 
     * @desc This method can make the tabs sortable 
     * (use of the component Sortable script.aculo.us)
     */  
    makeSortable: function(options) {
        if(this.getSortable()) return;
        var opt = Object.extend(this.getSortableOptions(), options || {}); 
        Sortable.create(this.getTabsContainer().identify(), opt);
        this.setSortable(true)
    },
    
    // ----------------------------------------------
    
    /**
     * show()
     * 
     * @desc To show the tab through its index or its unique identifier. 
     * It is an alias TabsItem.show() (syntax shorter)
     */ 
    show: function(index, select) {   
        var getter = this.get(index);
        if(getter) {
            getter.show(select);
        }
    },
    
    /**
     * show()
     * 
     * @desc To show the tab through its index or its unique identifier. 
     * It is an alias TabsItem.show() (syntax shorter)
     */ 
    hide: function(index) {   
        var getter = this.get(index);
        if(getter) {
            getter.hide();
        }
    },
    
    /**
     * select()
     * 
     * @desc To select the tab through its index or its unique identifier. 
     * It is an alias TabsItem.select () (syntax shorter)
     */ 
    select: function(index) {   
        var getter = this.get(index);
        if(getter) {
            getter.select();
        }
    },
    
    /**
     * selectFirst()
     * 
     * @desc To select the first tab (displayed)
     */
    selectFirst: function() {   
        this.items.each(function(elem, index) {
            if(elem.getDisplay()) {
                elem.select();
                throw $break;
            }
        });
    },
    
    /**
     * selectLast()
     * 
     * @desc To select the last tab (displayed)
     */
    selectLast: function() {   
        this.items.last().select();
    },
    
    /**
     * unSelectAll()
     * 
     * @desc To unSelect all tabs
     */
    unSelectAll: function(without) {
        without = (Object.isUndefined(without)) ? false : without;
        this.items.each(function(tabs, index) {
            if(without) {
                if(tabs != without) tabs.unSelect();
            } else {
                tabs.unSelect();
            }
        });
    },
    
    /**
     * get()
     * 
     * @desc To get a tab (TabsItem instance) through its index or its unique identifier. 
     * @return TabsItem
     */ 
    get: function(index, throwHandler) {
        var inst  = this;
        var error = 'Error on Tabs.get() : index '+index+' is undefined ! ';
        
        if(Object.isUndefined(throwHandler)) {
            throwHandler = true;
        } 

        if(Object.isNumber(index)) {
            if(!this.items[index]) {
                if(throwHandler) throw(error);
                else return false;
            }
            return this.items[index];
        } else if(Object.isString(index)) {
            var seeked  = false;
            var itemOut = null;
            this.getDomTabs().each(function(elem, id) {
                if(inst.items[id]) {
                    var item   = inst.items[id];
                    var itemId = item.getPanel().id;
                    if(itemId == index) {
                        seeked = true;
                        itemOut = item;
                    }
                }
            });
            if(!seeked) {
                if(throwHandler) throw(error);
                else return false;
            }
            return itemOut;
        }
        if(throwHandler) throw(error);
        else return false;
    }
});

/**
 * TabsItem( object tabs, object options)
 * 
 * @extend Class.accessors
 * @desc TabsItem is a class providing a set of method to handle each tab.
 * In fact, each tab is an instance of TabsItem.
 */
var TabsItem = Class.create(Class.accessors, {
    /**
     * Initialize
     *
     * @desc Defines the options and make treatment on the tab.
     */
    initialize: function(tabs, options) {
        this.options = {
            tabs    : null,
            item    : null,
            panel   : null,
            display : true,
            request : null,
            loaded  : false,
            titleElement : null,
            selected : false,
            iframe   : null
        };
        this.setOptions(this.options, options, true);
        
        this.setTabs(tabs);
        this.__getTitleElement();
        this.__setEvents();
        this.__voidAhref();
        
        // custom events
        this.events = new EventManager(this);
    },
        
    /**
     * __setEvents()
     *
     * @desc This method is private.
     */
    __setEvents: function() {
        var inst = this;
        var selectEvent = this.getTabs().getSelectEvent();
        $(this.getItem()).observe(selectEvent, this.select.bindAsEventListener(this));
    },
    
    /**
     * __getTitleElement()
     *
     * @desc This method is private.
     */
    __getTitleElement: function() {
        this.setTitleElement(this.getItem().firstDescendant());
    },
    
    /**
     * __voidAhref()
     *
     * @desc This method is private.
     */
    __voidAhref: function() {
        var a = this.getItem().firstDescendant();
        if(a.hasAttribute('href')) {
            a.href = 'javascript:void(0)';
        } 
    },

    /**
     * observe()
     *
     * @desc Observe an custom event
     */
    observe: function(eventName, callback) {
        this.events.observe(eventName, callback);
        return this;
    },
    
    
    /**
     * getTitle()
     *
     * @desc Return the current title of tab.
     */
    getTitle: function() {
        return this.getTitleElement().innerHTML;
    },
    
    /**
     * setTitle()
     *
     * @desc Set new title on tab
     */
    setTitle: function(title) {
        this.getTitleElement().update(title);
    },
        
    /**
     * select()
     *
     * @desc Select the tab and make history if actived.
     */
    select: function() {
        // update hash if options.history as true
        if(this.getTabs().getHistory() ) {
            History.set(this.getTabs().getTabsId(), this.getPanel().id);
            History.setTitle(History.getTitle() + ' (' + this.getPanel().id.ucfirst() + ')');
        }
        
        this.__select();
    },
    
    /**
     * __select()
     *
     * @desc Select the tab without history treatments (fix for opera)
     */
    __select: function () {
        if(this.getTabs().getHistory() ) {
            History.setTitle(History.getTitle() + ' (' + this.getPanel().id.ucfirst() + ')');
        }
    
        this.getTabs().unSelectAll(this);


        this.getItem().className = 'tab-show';
        this.getPanel().className = 'panel-show';
        this.getPanel().show();
        
        this.load();
        
        this.events.notify('select', this);
        
        this.setSelected(true);
    },
    
    /**
     * unSelect()
     *
     * @desc Unselect the tab.
     */
    unSelect: function() {
        this.getItem().className = 'tab-hide';
        this.getPanel().className = 'panel-hide';
        
        //this.hide();
        if(this.getSelected()) {
            this.events.notify('unselect', this);
        }
        
        this.setSelected(false);
    },
        
    /**
     * show()
     *
     * @desc Show the tab (with effect if actived)
     */
    show: function(select) {
        if(this.getDisplay()) return;
        this.events.notify('show', this);
        this.getItem().show();
        this.getPanel().show();
        this.setDisplay(true);
        if(select === true) this.select();
    },
    
    /**
     * hide()
     *
     * @desc Hide the tab (with effect if actived)
     */
    hide: function() {
        if(!this.getDisplay()) return;
        this.events.notify('hide', this);
        this.getItem().hide();
        this.getPanel().hide();
        this.setDisplay(false);
        this.getTabs().selectFirst();
    },
    
    /**
     * toggle()
     *
     * @desc Toggle for show/hide.
     */
    toggle: function(select) {
        if(!this.getDisplay()) {
            return this.show(select);
        }
        this.hide();
    },
    
    /**
     * load()
     *
     * @desc Load an Ajax request or an iframe in the panel of the tab.
     */
    load: function(options) {
        var inst = this;
        var options = Object.extend(options || {}, this.getRequest().options || {});
        
        if(this.getRequest().url != null && !this.getLoaded()) {
            var loadType = this.getRequest().type;

            // AJAX
            if(loadType == 'ajax') {
            
                // Default options
                options = Object.clone(Object.extend({
                    onLoading: function(response, json, tabs) {
                        tabs.setTitle('Loading...');
                    }
                }, options));
                
                // onCreate
                var onCreate = options.onCreate;
                options.onCreate = (function(response) {
                    if (Object.isFunction(onCreate)) onCreate(response, inst);
                }).bind(this);
                
                // onSuccess
                var onSuccess = options.onSuccess;
                options.onSuccess = (function(response, json) {
                    inst.setLoaded(true);  
                    if (Object.isFunction(onSuccess)) onSuccess(response, json, inst);
                }).bind(this);
                
                // onFailure
                var onFailure = options.onFailure;
                options.onFailure = (function(response, json) {
                    if (Object.isFunction(onFailure)) onFailure(response, json, inst);
                }).bind(this);
                
                // onComplete
                var onComplete = options.onComplete;
                options.onComplete = (function(response, json) {
                    inst.setTitle(inst.options.tmpTitle);
                    if (Object.isFunction(onComplete)) onComplete(response, json, inst);
                }).bind(this);
                
                // onLoading
                var onLoading = options.onLoading;
                options.onLoading = (function(response, json) {
                    inst.options.tmpTitle = inst.getTitle();
                    if (Object.isFunction(onLoading)) onLoading(response, json, inst);
                }).bind(this);
                
                // onLoaded
                var onLoaded = options.onLoaded;
                options.onLoaded = (function(response, json) {
                    if (Object.isFunction(onLoaded)) onLoaded(response, json, inst);
                }).bind(this);
                
                // onException
                var onException = options.onException;
                options.onException = (function(request, exception) {
                    if (Object.isFunction(onException)) onException(request, exception, inst);
                }).bind(this);
                
                new Ajax.Updater(this.getPanel(), this.getRequest().url, Object.extend({
                    onLoading: function(tabs) {
                        tabs.setTitle('Loading...');
                    }
                }, options || {}));
                
                
            } 
            
            // IFRAME
            else if(loadType == 'iframe') {

                this.iframe = new Element('iframe', {
                    name   : this.getRequest().id,
                    id     : this.getRequest().id,
                    src    : this.getRequest().url
                });
                   
                this.getPanel().update(this.iframe); 
                this.setLoaded(true);
            }
        }
    },
    
    /**
     * reload()
     *
     * @desc Reload the Ajax request or iframe.
     */
    reload: function(options) {
        this.setLoaded(false);
        this.load(options);
    }
    
});

/**
 * Tabs Quick Syntax
 * $(#element).tabs({options})
 *
 * Extend DOM to add method.
 * This extension allows greatly ease the management 
 * and handling of Tabs instances. 
 */
Element.addMethods({
    tabs : function(element, options) {
        var id = element.id.camelize();
        
        // create namespace if dont exist
        if(!Prototype.X.Registry.Tabs[id]) {
            Prototype.X.Registry.Tabs[id] = {};
            var instance = new Tabs(element, options);
            Prototype.X.Registry.Tabs[id].instance = instance;
            return instance;
        } 
        
        var instance = Prototype.X.Registry.Tabs[id].instance;
        instance.setOptions(instance.options, options, true);
        return instance;
    }
});

// -------------------------------------------------------------------
