var box = {};
box.version = '0.3.7';

(function($) {

var W = this, D = this.document, DE = D.documentElement;

// @todo expand and correct features detect
if (!D.getElementById || !D.getElementsByTagName || !D.createElement || !D.createTextNode) {
    box.disabled = true;
    return;
}

box.disabled = false;

var html = $(DE || 'html').attr('id', 'js');

// flag dom is ready
box.domIsReady = false;
$(D).ready(function() {
    box.domIsReady = true;
});

// flag page is loaded
box.loadIsDone = false;
$(W).load(function() {
    box.loadIsDone = true;
});


// for sharing local resource
var local = {};


// bridge to jQuery
box.dom = function(a1, a2) {
    return $(a1, a2);
};


var uiCache = {};

// get correct component object
box.ui = function(s) {
    if(typeof s == 'string') {
        return uiCache[s];
    }
};

// add plugin ui object
box.addPluginUI = function(id, obj) {
    id = 'plugin.' + id;
    if(!uiCache[id]) {
        uiCache[id] = obj;
    }
    return obj;
};

// remove plugin ui object
box.removePluginUI = function(id) {
    id = 'plugin.' + id;
    delete uiCache[id];
};


// add / remove / manage clicks with delegation
var clickDelegates = {};

$(D).click(function(e) {
    var d, j, element;
    for(var i in clickDelegates) {
        if(clickDelegates.hasOwnProperty(i)) {
            d = clickDelegates[i];
            j = d.deepness || 1000;
            element = e.target;
            while(j-- && element) {
                if(d.test(e, element)) {
                    d.action(e, element);
                    break;
                }
                element = element.parentNode;
            }
        }
    }
});

box.addClick = function(datas) {
    clickDelegates[datas.id] = datas;
};

box.removeClick = function(id) {
    delete clickDelegates[id];
};


// get DOM insertion method from a component config object
var getInsertionMethod = function(datas) {
    var r = {};
    if(datas.appendTo) {
        r.method = 'appendTo';
        r.target = datas.appendTo;
    } else if(datas.prependTo) {
        r.method = 'prependTo';
        r.target = datas.prependTo;
    } else if(datas.insertAfter) {
        r.method = 'insertAfter';
        r.target = datas.insertAfter;
    } else if(datas.insertBefore) {
        r.method = 'insertBefore';
        r.target = datas.insertBefore;
    }
    return r;
};


// simple class-like inherit mecanism
// @todo add superclass property to keep superclass properties?
var inherit = (function() {
    var Fn = function() {};
    return function(fnSub, fnSuper) {
        Fn.prototype = fnSuper.prototype;
        fnSub.prototype = new Fn();
        fnSub.prototype.constructor = fnSub;
    };
})();

// simple class-like extension mecanism
var extend = function(fn, methods) {
    for(var m in methods) {
        if(methods.hasOwnProperty(m)) {
            fn.prototype[m] = methods[m];
        }
    }
};


// @todo add a preload component to manage several preload at the same time
box.preload = function(datas) {
    if(typeof datas.id == 'string') {
        var l, c = 0;
        if(datas.img !== undefined && datas.img.constructor == Array) {
            var i = datas.img.length;
            l = i - 1;
            while(i--) {
                $(new Image()).load(function() {
                    ++c;
                    if(c == l + 1) {
                        box.fire({type: 'ready', component: 'preload', id: datas.id});
                    }
                }).attr('src', datas.img[l - i]);
            }
        } else if(datas.element) {
            var imgs = $('img', datas.element);
            l = imgs.length;
            if(l) {
                imgs.each(function(i, el) {
                    $(new Image()).load(function() {
                        ++c;
                        if(c == l) {
                            box.fire({type: 'ready', component: 'preload', id: datas.id});
                        }
                    }).attr('src', el.src);
                });
            } else {
                box.fire({type: 'ready', component: 'preload', id: datas.id});
            }
        }
    }
};


// get URL hash elements
box.getURLHash = function(key) {
    var hash = location.hash;
    if(hash) {
        hash = hash.replace('#', '');
        var values = {};
        var pairs = hash.split(';'), parts;
        var i = pairs.length, l = i - 1;
        while(i--) {
            parts = pairs[i].split('=');
            values[parts[0]] = parts[1];
        }
        if(typeof key == 'string') {
            return values[key];
        } else {
            return values;
        }
    }
    return null;
};


// lazy load scripts
box.loadScript = function(url, cache) {
    $.ajax({
        type: 'GET',
        url: url,
        dataType: 'script',
        cache: cache !== false
    });
};

var listeners = {};

var execListeners = function(evtName, evtObj, args) {
    var ls = listeners[evtName];
    var r = true;
    if(ls && ls.length) {
        var i = ls.length, l = i - 1, tmp;
        while(i--) {
            if(ls[l - i].apply(evtObj.source || null, args) === false) {
                r = false;
            }
        }
    }
    return r;
};

box.fire = function(evtObj) {
    if(typeof evtObj == 'object' && typeof evtObj.type == 'string' && typeof evtObj.component == 'string') {
        var args = Array.prototype.slice.call(arguments, 1);
        args = [evtObj].concat(args);
        if(typeof evtObj.source == 'undefined') {
            evtObj.source = null;
        }
        
        var evtName = evtObj.type + '.' + evtObj.component;
        if(typeof evtObj.id == 'string') {
            evtName += '.' + evtObj.id;
        }
        if(typeof evtObj.namespace == 'string') {
            evtName += '.' + evtObj.namespace;
        }
        
        var parts = evtName.split('.'), i = parts.length;
        while(i) {
            if(execListeners(parts.slice(0, i).join('.'), evtObj, args) === false) {
                break;
            }
            --i;
        }
    }
};

box.bind = function(bindings) {
    for(var e in bindings) {
        if(bindings.hasOwnProperty(e)) {
            if(!listeners[e]) {
                listeners[e] = [];
            }
            listeners[e][listeners[e].length] = bindings[e];
        }
    }
};

box.unbind = function() {
    var l = arguments.length;
    while(l--) {
        if(listeners[arguments[l]]) {
            delete listeners[arguments[l]];
        }
    }
};

// get box datas in class attribute
$.fn.getBoxDatas = function(key) {
    var datas = null;
    if(this.length) {
        var cls = this[0].className, tmp;
        if(cls && cls.indexOf('box[') > -1) {
            if(typeof key == 'string') {
                var re = new RegExp(key + '=([^;\\]]+)');
                tmp = cls.match(re);
                datas = tmp && tmp[1];
            } else {
                datas = {};
                var box = cls.replace(/.*box\[([^)]+)\].*/, '$1');
                var boxParts = box.split(';');
                var i = boxParts.length, l = i - 1;
                while(i--) {
                    tmp = boxParts[i].split('=');
                    datas[tmp[0]] = tmp[1];
                }
            }
        }
    }
    return datas;
};

// set box datas in class attribute
$.fn.setBoxDatas = function(datas) {
    if(typeof datas == 'object' && this.length) {
        var cls = this[0].className;
        var tmp, first = false;
        if(!cls || cls.indexOf('box[') == -1) {
            $(this).addClass('box[]');
            first = true;
        }
        cls = this[0].className;
        for(var i in datas) {
            tmp = i + '=' + datas[i];
            // @todo should check for duplicates in keys and override if necessary
            if(datas.hasOwnProperty(i) && cls.indexOf(tmp) == -1) {
                cls = cls.replace(']', (first ? '' : ';') + tmp + ']');
            }
        }
        this[0].className = cls;
    }
};

// clear box datas in class attribute
$.fn.clearBoxDatas = function(datas) {
    if(typeof datas == 'object' && this.length) {
        var cls = this[0].className, tmp;
        for(var i in datas) {
            tmp = i + '=' + datas[i];
            if(datas.hasOwnProperty(i) && cls.indexOf(tmp) > -1) {
                cls = cls.replace(tmp, '');
            }
        }
        this[0].className = cls;
    }
};

// clear fields onfocus / restore on blur
var reTextFieldTypes = /(text|password)/i;
var reEmpty = /^\s*$/;

var clearTextFieldValue = function() {
    if(this.value == this.defaultValue) {
        this.value = '';
    }
};

var restoreTextFieldValue = function() {
    if(reEmpty.test(this.value)) {
        this.value = this.defaultValue;
    }
};

$.fn.clearTextFields = function() {
    this.each(function(i, elm) {
        if(elm.nodeName.toLowerCase() == 'input' && reTextFieldTypes.test(elm.type)) {
            $(elm).focus(clearTextFieldValue).blur(restoreTextFieldValue);
        } else {
            $('input[type=text], input[type=password]').focus(clearTextFieldValue).blur(restoreTextFieldValue);
        }
    });
};

// get outer HTML
var divDummyContainer = $('<div></div>');
$.fn.outerHTML = function() {
    divDummyContainer.html('');
    return divDummyContainer.append(this.eq(0).clone()).html();
};

// replace the innerHTML of an element
$.fn.replaceIn = function(s) {
	$(s).empty().append(this);
    return this;
};

// get scroll offset
$.fn.getScroll = function() {
    return {
        top: this.scrollTop(),
        left: this.scrollLeft()
    };
};

// get different size types
var getASize = {
    'viewport-width': function() {
        return $(W).width();
    },'viewport-height': function() {
        return $(W).height();
    },'document-width': function() {
        return $(D).width();
    },'document-height': function() {
        return $(D).height();
    },'content-box-width': function(elm) {
        return elm.width();
    },'content-box-height': function(elm) {
        return elm.height();
    },'padding-box-width': function(elm) {
        return elm.innerWidth();
    },'padding-box-height': function(elm) {
        return elm.innerHeight();
    },'border-box-width': function(elm) {
        return elm.outerWidth();
    },'border-box-height': function(elm) {
        return elm.outerHeight();
    },'margin-box-width': function(elm) {
        return elm.outerWidth(true);
    },'margin-box-height': function(elm) {
        return elm.outerHeight(true);
    }
};

// get a size from a keyword
// if no (recognized) keyword, default to content-box size
var getSizeFromKeyword = function(elm, type, keyword) {
    if(elm[0] === W || elm[0] === D) {
        return getASize['content-box-' + type](elm);
    } else {
        var method = typeof keyword == 'string' ? keyword + '-' + type : 'content-box-' + type;
        if(getASize[method]) {
            return getASize[method](elm);
        } else {
            return getASize['content-box-' + type]();
        }
    }
};

// get size from a keyword, different keywords possible for width / height
// if no keywords, default to content-box size
$.fn.getSize = function(datas) {
    return {
        width: getSizeFromKeyword(this, 'width', datas),
        height: getSizeFromKeyword(this, 'height', datas)
    };
};

// set size, only with numbers, or 'auto' keyword
$.fn.setSize = function(datas) {
    if(typeof datas == 'number' || datas == 'auto') {
        this.width(datas).height(datas);
    } else if(typeof datas == 'object') {
        if(typeof datas.width == 'number' || datas.width == 'auto') {
            this.width(datas.width);
        }
        if(typeof datas.height == 'number' || datas.height == 'auto') {
            this.height(datas.height);
        }
    }
};

// get a position from a keyword
// if no (recognized) keyword, default to offset from the document origin
var getPositionFromKeyword = function(elm, keyword) {
    if(elm[0] === D) {
        return {top: 0, left: 0};
    } else if(elm[0] === W) {
        return elm.getScroll();
    } else if(keyword == 'positioned-ancestor') {
        return elm.position();
    } else {
        return elm.offset();
    }
};

// get position from a keyword, different keywords possible for top / left
// if no keywords, default to offsets from the document origin
$.fn.getPosition = function(datas) {
    return getPositionFromKeyword(this, datas);
};

// set position, only with numbers
$.fn.setPosition = function(datas) {
    if(typeof datas == 'number') {
        this.css({top: datas + 'px', left: datas + 'px'});
    } else if(typeof datas == 'object') {
        var pos = {};
        if(typeof datas.top == 'number') {
            pos.top = datas.top + 'px';
        }
        if(typeof datas.left == 'number') {
            pos.left = datas.left + 'px';
        }
        this.css(pos);
    }
};

var getAPosition = {
    'root': function(curElm, refElm, type) {
        return refElm.offset()[type];
    },'positioned-ancestor': function(curElm, refElm, type) {
        return refElm.position()[type];
    },'before': function(curElm, refElm, type, relType) {
        var curDim = curElm.getSize('border-box')[relType];
        var refPos = refElm.getPosition()[type];
        return refPos - curDim;
    },'start': function(curElm, refElm, type, relType) {
        return refElm.getPosition()[type];
    },'middle': function(curElm, refElm, type, relType) {
        var curDim = curElm.getSize('border-box')[relType];
        var refDim = getSizeFromKeyword(refElm, relType);
        var refPos = refElm.getPosition()[type];
        return refPos + (refDim - curDim) / 2;
    },'end': function(curElm, refElm, type, relType) {
        var curDim = curElm.getSize('border-box')[relType];
        var refDim = getSizeFromKeyword(refElm, relType);
        var refPos = refElm.getPosition()[type];
        return refPos + refDim - curDim;
    }
};

var getAlternateSelectorNames = function(name) {
    var ref = {
        'viewport': W,
        'document': D
    };
    return ref[name] || name;
};

var reDeleteSelector = /(.+:)?/;

var getStyleDim = function(styles, curElm, refElm) {
    var s;
    if(typeof styles.width == 'string' && getASize[styles.width.replace(reDeleteSelector, '') + '-width']) {
        s = styles.width.split(':');
        if(s.length == 2) {
            styles.width = getASize[s[1] + '-width']($(getAlternateSelectorNames(s[0])), 'width');
        } else {
            styles.width = getASize[styles.width + '-width'](refElm, 'width');
        }
    }
    if(typeof styles.height == 'string' && getASize[styles.height.replace(reDeleteSelector, '') + '-height']) {
        s = styles.height.split(':');
        if(s.length == 2) {
            styles.height = getASize[s[1] + '-height']($(getAlternateSelectorNames(s[0])), 'height');
        } else {
            styles.height = getASize[styles.height + '-height'](refElm, 'height');
        }
    }
    return styles;
};

var getStylePos = function(styles, curElm, refElm) {
    var s;
    if(typeof styles.top == 'string' && getAPosition[styles.top.replace(reDeleteSelector, '')]) {
        s = styles.top.split(':');
        if(s.length == 2) {
            styles.top = getAPosition[s[1]](curElm, $(getAlternateSelectorNames(s[0])), 'top', 'height');
        } else {
            styles.top = getAPosition[styles.top](curElm, refElm, 'top', 'height');
        }
        if(!isNaN(styles['min-top'])) {
            styles.top = styles.top < styles['min-top'] ? styles['min-top'] : styles.top;
            delete styles['min-top'];
        }
    }
    if(typeof styles.left == 'string' && getAPosition[styles.left.replace(reDeleteSelector, '')]) {
        s = styles.left.split(':');
        if(s.length == 2) {
            styles.left = getAPosition[s[1]](curElm, $(getAlternateSelectorNames(s[0])), 'left', 'width');
        } else {
            styles.left = getAPosition[styles.left](curElm, refElm, 'left', 'width');
        }
        if(!isNaN(styles['min-left'])) {
            styles.left = styles.left < styles['min-left'] ? styles['min-left'] : styles.left;
            delete styles['min-left'];
        }
    }
    return styles;
};

// apply styles from a reference element or not
$.fn.applyStyles = function(styles, reference) {
    if(this.length && typeof styles == 'object') {
        var curElm = this.eq(0);
        var refElm = reference === undefined ? curElm : $(getAlternateSelectorNames(reference));
        
        // size can affect position, so compute first
        styles = getStyleDim(styles, curElm, refElm);
        
        if(!isNaN(styles.width)) {
            curElm.css('width', styles.width);
            delete styles.width;
        }
        if(!isNaN(styles.height)) {
            curElm.css('height', styles.height);
            delete styles.height;
        }
        
        styles = getStylePos(styles, curElm, refElm);
        
        curElm.css(styles);
    }
    return this;
};

// get styles from a reference element or not
$.fn.getStyles = function(styles, reference) {
    if(this.length && typeof styles == 'object') {
        var curElm = this.eq(0);
        var refElm = reference === undefined ? curElm : $(getAlternateSelectorNames(reference));
        
        styles = getStyleDim(styles, curElm, refElm);
        styles = getStylePos(styles, curElm, refElm);
        
        return styles;
    }
    return null;
};

(function() {
    
    var config = {
        cls: 'draggable'
    };
    
    var counter = 0;
    
    uiCache.draggable = {
        create: function(datas) {
            datas.id = datas.id || $(datas.element).getBoxDatas('id') || 'n' + (++counter);
            if(datas.id) {
                var id = 'draggable.' + datas.id;
                return uiCache[id] || (uiCache[id] = new Draggable(datas));
            }
        },
        
        destroy: function() {
            var i = arguments.length, id;
            while(i--) {
                id = 'draggable.' + arguments[i];
                if(uiCache[id]) {
                    uiCache[id].disable();
                    delete uiCache[id];
                }
            }
        },
        
        configure: function(datas) {
            for(var i in datas) {
                if(datas.hasOwnProperty(i) && config[i] !== undefined) {
                    config[i] = datas[i];
                }
            }
        }
    };
    
    var Draggable = function(datas) {
        this.initialize(datas);
    };
    Draggable.prototype = {
        initialize: function(datas) {
            this.id = datas.id;
            this.element = $(datas.element);
            this.handle = datas.handle ? $(datas.handle) : null;
            if(!this.handle || !this.handle.length) {
                this.handle = this.element;
            }
            
            this.setMinMax(datas);
            this.enable();
            
            box.fire({
                type: 'init',
                component: 'draggable',
                id: this.id,
                source: this
            });
        },
        
        setMinMax: function(datas) {
            if(typeof datas == 'object') {
                if(datas.bindTo) {
                    var bindTo = $(datas.bindTo == 'page' ? D : datas.bindTo == 'viewport' ? W : datas.bindTo);
                    var posRef = bindTo.getPosition('from-root');
                    var dimRef = bindTo.getSize('border-box');
                    var dimElm = $(datas.element).getSize('border-box');
                    this.minX = posRef.left;
                    this.maxX = this.minX + dimRef.width - dimElm.width;
                    this.minY = posRef.top;
                    this.maxY = this.minY + dimRef.height - dimElm.height;
                    this.element.css({top: this.minY, left: this.minX});
                    bindTo = null;
                } else {
                    this.minX = typeof datas.minX == 'number' ? datas.minX : this.minX;
                    this.maxX = typeof datas.maxX == 'number' ? datas.maxX : this.maxX;
                    this.minY = typeof datas.minY == 'number' ? datas.minY : this.minY;
                    this.maxY = typeof datas.maxY == 'number' ? datas.maxY : this.maxY;
                }
            }
            return this;
        },
        
        disable: function() {
            if(this.disabled === false) {
                this.element.removeClass(config.cls);
                this.handle.unbind('mousedown.' + this.id);
                this.disabled = true;
            }
        },
        
        enable: function() {
            var that = this;
            if(that.disabled !== false) {
                that.handle.bind('mousedown.' + that.id, function(e) {
                    that.startMove(e);
                });
                this.element.addClass(config.cls);
                that.disabled = false;
            }
        },
        
        move: function(e) {
            e.preventDefault();
            var y = e.pageY - this.sy, x = e.pageX - this.sx;
            if(this.minX !== undefined) {x = Math.max(x, this.minX);}
            if(this.maxX !== undefined) {x = Math.min(x, this.maxX);}
            if(this.minY !== undefined) {y = Math.max(y, this.minY);}
            if(this.maxY !== undefined) {y = Math.min(y, this.maxY);}
            this.element.css({
                'top': y +'px',
                'left': x +'px'
            });
            box.fire({
                type: 'move',
                component: 'draggable',
                id: this.id,
                source: this
            }, x, y);
        },
        
        startMove: function(e) {
            e.preventDefault();
            var that = this;
            var x = parseFloat(that.element.css('left')) || 0;
            var y = parseFloat(that.element.css('top')) || 0;
            that.element.css('top', y + 'px');
            that.element.css('left', x + 'px');
            that.sx = e.pageX - x;
            that.sy = e.pageY - y;
            $(D).bind('mouseup.' + that.id, function(e) {
                that.endMove(e);
            }).bind('mousemove.' + that.id, function(e) {
                that.move(e);
            });
            box.fire({
                type: 'startmove',
                component: 'draggable',
                id: this.id,
                source: this
            });
        },
        
        endMove: function(e) {
            $(D).unbind('mouseup.' + this.id).unbind('mousemove.' + this.id);
            box.fire({
                type: 'endmove',
                component: 'draggable',
                id: this.id,
                source: this
            });
        }
    };
    
})();

(function() {
    
    var config = {
        wrapScrollbar: '<div class="{$wrapScrollbarCls}">{$content}</div>',
        wrapScrollbarCls: 'scrollbar',
        
        wrapContent: '<div class="{$wrapContentCls}"></div>',
        wrapContentScrollCls: 'scrolled',
        wrapContentNoScrollCls: 'notScrolled',
        
        btnPrev: '<span class="{$btnPrevCls}"></span>',
        btnPrevCls: 'prev',
        
        btnNext: '<span class="{$btnNextCls}"></span>',
        btnNextCls: 'next',
        
        gutter: '<div class="{$gutterCls}">{$bar}</div>',
        gutterCls: 'gutter',
        
        bar: '<a href="#" class="{$barCls}"></a>',
        barCls: 'bar'
    };
    
    var counter = 0;
    
    uiCache.scroll = {
        create: function(datas) {
            datas.id = datas.id || $(datas.element).getBoxDatas('id') || 'n' + (++counter);
            if(datas.id) {
                var id = 'scroll.' + datas.id;
                return uiCache[id] || (uiCache[id] = new Scroll(datas));
            }
        },
        
        destroy: function() {
            var i = arguments.length, id;
            while(i--) {
                id = 'scroll.' + arguments[i];
                if(uiCache[id]) {
                    uiCache[id].disable();
                    uiCache.draggable.destroy(arguments[i] + 'Scroll');
                    var content = uiCache[id].wrapper.html();
                    uiCache[id].element.html(content);
                    delete uiCache[id];
                }
            }
        },
        
        configure: function(datas) {
            for(var i in datas) {
                if(datas.hasOwnProperty(i) && config[i] !== undefined) {
                    config[i] = datas[i];
                }
            }
        }
    };
    
    var getScrollbarHTML = function(bar, buttons) {
        var tmp = bar ? config.gutter.replace('{$bar}', config.bar) : '';
        if(buttons) {
            tmp = config.btnPrev + tmp + config.btnNext;
        }
        var html = config.wrapScrollbar.replace('{$content}', tmp);
        $.each(['wrapScrollbar', 'btnPrev', 'btnNext', 'gutter', 'bar'], function(i, cls) {
            html = html.replace('{$' + cls + 'Cls}', config[cls + 'Cls']);
        });
        return html;
    };
    
    var addBarDraggable = function(scroll) {
        box.ui('draggable').create({
            id: scroll.dragId,
            element: scroll.bar
        });
        
        if(scroll.position == 'top') {
            box.ui('draggable.' + scroll.dragId).setMinMax({
                minX: 0,
                maxX: 0,
                minY: 0
            });
        } else {
            box.ui('draggable.' + scroll.dragId).setMinMax({
                minX: 0,
                minY: 0,
                maxY: 0
            });
        }
        
        var bindings = {};
        bindings['move.draggable.' + scroll.dragId] = function(e, x, y) {
            var coord = arguments[scroll.dragCoord];
            if(coord == Math.round(scroll.size.scrollDiff)) {
                coord = scroll.size.scrollDiff;
            }
            var pos = Math.round(coord / scroll.size.scrollDiff * scroll.size.elementDiff);
            scroll.wrapper.css(scroll.position, - pos + 'px');
        };
        
        box.bind(bindings);
    };
    
    var clickToPosition = function(e, scroll) {
        e.preventDefault();
        var t = $(e.target), pos;
        if(t.hasClass(config.btnPrevCls)) {
            pos = Math.round(scroll.getWrapperOffset() + scroll.moveBy);
            scroll.moveContentTo(pos);
        } else if(t.hasClass(config.btnNextCls)) {
            pos = Math.round(scroll.getWrapperOffset() - scroll.moveBy);
            scroll.moveContentTo(pos);
        } else if(t.hasClass(config.gutterCls)) {
            var coord = scroll.position == 'top' ? e.pageY : e.pageX;
            pos = coord - scroll.gutter.getPosition()[scroll.position] - Math.round(scroll.size.bar / 2);
            scroll.moveBarTo(pos);
        }
        t = null;
    };
    
    var wheelEvent = function(e, scroll) {
        if(!scroll.disabled) {
            e.preventDefault();
            var n = e.detail ? - e.detail / 3 : e.wheelDelta / 120;
            var pos = Math.round(scroll.getWrapperOffset() + (n * scroll.moveBy));
            scroll.moveContentTo(pos);
        }
    };
    local.wheelEventForScroll = wheelEvent;
    
    // @todo add some events
    var Scroll = function(datas) {
        this.initialize(datas);
    };
    Scroll.prototype = {
        initialize: function(datas) {
            var that = this;
            
            that.id = datas.id;
            
            that.direction = datas.horizontal ? 'horizontal' : 'vertical';
            that.position = that.direction == 'vertical' ? 'top' : 'left';
            that.moveBy = (!isNaN(datas.moveBy) && datas.moveBy > 0) ? datas.moveBy : null;
            that.barMinSize = (!isNaN(datas.barMinSize) && datas.barMinSize > 10) ? datas.barMinSize : 10;
            
            that.element = $(datas.element);
            var wrapHTML = config.wrapContent.replace('{$wrapContentCls}', config.wrapContentNoScrollCls);
            if(!that.element.html()) {
                that.element.html(wrapHTML);
            } else {
                that.element.wrapInner(wrapHTML);
            }
            that.wrapper = that.element.children();
            
            var insert = getInsertionMethod(datas);
            if(!insert.method && !insert.target) {
                insert.method = 'prependTo';
                insert.target = that.element;
            }
            
            // @todo add support for scroll without bar
            if(!datas.bar && !datas.buttons) {
                datas.bar = true;
            }
            that.scrollbar = $(getScrollbarHTML(datas.bar, datas.buttons))[insert.method](insert.target);
            if(datas.bar) {
                that.gutter = that.scrollbar.find('.' + config.gutterCls);
                that.bar = that.scrollbar.find('.' + config.barCls);
            }
            
            that.dragId = that.id + 'Scroll';
            that.dragCoord = that.position == 'top' ? 2 : 1;
            addBarDraggable(this);
            
            box.fire({type: 'beforefirstcompute', component: 'scroll', id: that.id, source: that});
            
            that.compute();
            
            if(that.wrapper.find('img').length && !box.loadIsDone) {
                $(W).load(function() {
                    that.compute();
                });
            }
            
            box.fire({type: 'init', component: 'scroll', id: that.id, source: that});
        },
        
        disable: function() {
            if(this.disabled !== true) {
                this.scrollbar.css('visibility', 'hidden');
                this.wrapper.removeClass(config.wrapContentScrollCls).addClass(config.wrapContentNoScrollCls);
                
                box.ui('draggable.' + this.dragId).disable();
                this.element.unbind('DOMMouseScroll').unbind('mousewheel');
                this.scrollbar.unbind('click');
                
                this.disabled = true;
            }
            return this;
        },
        
        enable: function() {
            var that = this;
            if(that.disabled !== false) {
                that.element.bind('DOMMouseScroll', function(e) {
                    wheelEvent(e, that);
                }).bind('mousewheel', function(e) {
                    wheelEvent(e, that);
                });
                
                that.scrollbar.click(function(e) {
                    clickToPosition(e, that);
                });
                
                box.ui('draggable.' + that.dragId).enable();
                
                that.wrapper.removeClass(config.wrapContentNoScrollCls).addClass(config.wrapContentScrollCls);
                
                // scrollbar should always be above the wrapper to be accessible
                var zIndex = parseInt(that.wrapper.css('zIndex'), 10);
                that.scrollbar.css({zIndex: isNaN(zIndex) ? 1 : ++zIndex, visibility: 'visible'});
                
                that.disabled = false;
            }
            return that;
        },
        
        reposition: function() {
            this.wrapper.css('top', 0);
            this.bar.css('top', 0);
            return this;
        },
        
        compute: function() {
			//alert("compute borgne!");
            var totalD, partialD;
            if(this.direction == 'vertical') {
                totalD = 'offsetHeight';
                partialD = 'height';
            } else {
                totalD = 'offsetWidth';
                partialD = 'width';
            }
            
            this.size = {};
            
            this.size.element = this.element[partialD]();
            this.size.wrapper = this.wrapper[0][totalD];
            
            if(this.size.wrapper > this.size.element) {
                this.size.gutter = this.gutter[0][totalD];
                this.size.bar = this.size.element / this.size.wrapper * this.size.gutter;
                
                if(this.size.bar < this.barMinSize) {
                    this.size.bar = this.barMinSize;
                }
                
                // debug IE6 with bottom/right positioning inside bar
                if(W.ie6 && Math.round(this.size.bar) % 2 !== 0) {
                    this.size.bar = Math.round(this.size.bar) - 1;
                }
                
                this.size.scrollDiff = this.size.gutter - this.size.bar;
                this.size.elementDiff = this.size.wrapper - this.size.element;
                
                this.bar.css(partialD, Math.round(this.size.bar) + 'px');
                
                if(!this.moveBy) {
                    var amount = Math.ceil((this.size.gutter - this.size.bar) / this.size.gutter * this.size.bar);
                    this.moveBy = (amount > 10) ? amount : 10;
                }
                
                var dragMax = {};
                if(this.direction == 'horizontal') {
                    dragMax.maxX = Math.round(this.size.scrollDiff);
                } else {
                    dragMax.maxY = Math.round(this.size.scrollDiff);
                }
                box.ui('draggable.' + this.dragId).setMinMax(dragMax);
                
                box.fire({type: 'computesuccess', component: 'scroll', id: this.id, source: this});
                this.enable();
            } else {
                this.disable();
            }
            return this;
        },
        
        getWrapperOffset: function() {
            return parseInt(this.wrapper.css(this.position), 10) || 0;
        },
        
        moveBarTo: function(scrollPos) {
            if(!this.disabled && !isNaN(scrollPos)) {
                if(scrollPos < 0) {
                    scrollPos = 0;
                } else if(scrollPos > this.size.scrollDiff) {
                    scrollPos = this.size.scrollDiff;
                }
                var wrapperPos = - Math.round(Math.abs(scrollPos) / this.size.scrollDiff * this.size.elementDiff);
                this.wrapper.css(this.position, wrapperPos + 'px');
                this.bar.css(this.position, Math.round(scrollPos) + 'px');
            }
            return this;
        },
        
        moveContentTo: function(wrapperPos) {
            if(!this.disabled && !isNaN(wrapperPos)) {
                if(wrapperPos > 0) {
                    wrapperPos = 0;
                } else if(wrapperPos < -this.size.elementDiff) {
                    wrapperPos = -this.size.elementDiff;
                }
                var scrollPos = Math.round(Math.abs(wrapperPos) / this.size.elementDiff * this.size.scrollDiff);
                this.wrapper.css(this.position, Math.round(wrapperPos) + 'px');
                this.bar.css(this.position, scrollPos + 'px');
            }
            return this;
        },
        
        moveToElement: function(elm) {
            if(!this.disabled) {
                if(typeof elm == 'string') {
                    elm = this.wrapper.find(elm);
                }
                if(elm && elm.jquery && elm.length) {
                    var targetStart = elm.getPosition('positioned-ancestor')[this.position];
                    var targetDim = elm.getSize('margin-box')[this.position == 'top' ? 'height' : 'width'];
                    var targetEnd = targetStart + targetDim;
                    
                    var offset = -this.getWrapperOffset();
                    var visibleEnd = offset + this.size.element;
                    
                    if(targetStart < offset) {
                        this.moveContentTo(-targetStart);
                    } else if(targetEnd > visibleEnd) {
                        if(targetDim < this.size.element) {
                            this.moveContentTo(-(targetEnd - this.size.element));
                        } else {
                            this.moveContentTo(-targetStart);
                        }
                    }
                }
            }
            return this;
        }
    };
    
})();

// @todo API addItem, removeItem
// @todo add mousewheel support for "scrolling" the carousel ?
// @todo add support for a movePerItems configuration property
// @todo check pagination (working strangely when carousel is circular)
// @todo add some events

(function() {
    
    var config = {
        btnPrev: '<a href="#" class="{$btnPrevCls} {$btnDisabledCls}">' + l10n.prev + '</a>',
        btnPrevCls: 'prev',
        
        btnNext: '<a href="#" class="{$btnNextCls} {$btnDisabledCls}">' + l10n.next + '</a>',
        btnNextCls: 'next',
        
        btnDisabledCls: 'off',
        
        pagesWrap: '<div class="{$pagesWrapCls}"><ul>{$content}</ul></div>',
        pagesWrapCls: 'pagination',
        
        pagesItem: '<li{$pagesItemActiveCls}><a href="#">{$content}</a></li>',
        pagesItemActiveCls: 'on'
    };
    
    var counter = 0;
    
    uiCache.carousel = {
        create: function(datas) {
            datas.id = datas.id || $(datas.element).getBoxDatas('id') || 'n' + (++counter);
            if(datas.id) {
                var id = 'carousel.' + datas.id;
                return uiCache[id] || (uiCache[id] = new Carousel(datas));
            }
        },
        
        destroy: function() {
            var i = arguments.length, id;
            while(i--) {
                id = 'carousel.' + arguments[i];
                if(uiCache[id]) {
                    // @todo unbind before deleting
                    delete uiCache[id];
                }
            }
        },
        
        configure: function(datas) {
            for(var i in datas) {
                if(datas.hasOwnProperty(i) && config[i] !== undefined) {
                    config[i] = datas[i];
                }
            }
        }
    };
    
    var getBtnsHTML = function(type) {
        type = type == 'next' ? 'btnNext' : 'btnPrev';
        return config[type]
            .replace('{$' + type + 'Cls}', config[type + 'Cls'])
            .replace('{$btnDisabledCls}', config.btnDisabledCls);
    };
    
    var getPosition = function(carousel) {
        return parseInt(carousel.moveable.css(carousel.property), 10) || 0;
    };
    
    var getIndex = function(carousel, index) {
        if(isNaN(index)) {
            return 0;
        } else if(index < 0) {
            return index + carousel.length;
        } else if(index < carousel.length) {
            return index;
        } else {
            return index - carousel.length;
        }
    };
    
    var setCurrent = function(carousel, index) {
        carousel.current = getIndex(carousel, index);
        if(carousel.currentPage !== undefined) {
            var page = Math.ceil((carousel.current + carousel.display) / carousel.display);
            $('li', carousel.pagination)
                .eq(carousel.currentPage - 1)
                    .removeClass(config.pagesItemActiveCls)
                .end()
                .eq(page - 1)
                    .addClass(config.pagesItemActiveCls);
            carousel.currentPage = page;
        }
    };
    
    var prepareCircularMovePrev = function(carousel, index) {
        if(carousel.autoplay) {
            carousel.pauseAutoplay();
        }
        
        carousel.moving = true;
        
        var actualPos = getPosition(carousel);
        var futurePos = actualPos + carousel.moveBy * (carousel.current - index);
        var itemPos = parseInt(carousel.items.eq(carousel.current).css(carousel.property), 10);
        
        var min = index - (carousel.hasOffset && actualPos % carousel.moveBy ? 1 : 0);
        var max = carousel.current;
        var c, pos;
        
        for(var i = min; i < max; i++) {
            c = getIndex(carousel, i);
            pos = itemPos - (carousel.current - i) * carousel.moveBy;
            carousel.items.eq(c).css(carousel.property, pos + 'px');
        }
        
        setCurrent(carousel, index);
        moveToPosition(carousel, futurePos);
    };
    
    var prepareCircularMoveNext = function(carousel, index) {
        if(carousel.autoplaying) {
            carousel.pauseAutoplay();
        }
        
        carousel.moving = true;
        
        var actualPos = getPosition(carousel);
        var futurePos = actualPos + (-carousel.moveBy * (index - carousel.current));
        var itemPos = parseInt(carousel.items.eq(carousel.current).css(carousel.property), 10) + carousel.display * carousel.moveBy;
        
        var min = carousel.current + carousel.display - (carousel.hasOffset && actualPos % carousel.moveBy ? 1 : 0);
        var max = index + carousel.display - (carousel.hasOffset && actualPos % carousel.moveBy ? 1 : 0);
        var c, pos;
        
        for(var i = min; i < max; i++) {
            c = getIndex(carousel, i);
            pos = itemPos + (i - carousel.display - carousel.current) * carousel.moveBy;
            carousel.items.eq(c).css(carousel.property, pos + 'px');
        }
        
        setCurrent(carousel, index);
        moveToPosition(carousel, futurePos);
    };
    
    var prepareMove = function(carousel, index) {
        if(carousel.autoplaying) {
            carousel.pauseAutoplay();
        }
        
        carousel.moving = true;
        
        index = Math.min(index, carousel.length - carousel.display);
        if(carousel.buttons) {
            if(!index) {
                carousel.buttonPrev.addClass(config.btnDisabledCls);
                carousel.buttonNext.removeClass(config.btnDisabledCls);
            } else if(index == carousel.length - carousel.display) {
                carousel.buttonPrev.removeClass(config.btnDisabledCls);
                carousel.buttonNext.addClass(config.btnDisabledCls);
            } else {
                carousel.buttonPrev.removeClass(config.btnDisabledCls);
                carousel.buttonNext.removeClass(config.btnDisabledCls);
            }
        }
        
        setCurrent(carousel, index);
        moveToPosition(carousel, -carousel.moveBy * index);
    };
    
    var positionFirstElements = function(carousel, fromReposition) {
        var min = (carousel.hasOffset && carousel.offset) ? carousel.startAt - 1 : carousel.startAt;
        var max = min + carousel.length;
        var c, pos;
        for(var i = min; i < max; i++) {
            c = getIndex(carousel, i);
            carousel.items.eq(c).css(carousel.property, i * carousel.moveBy + 'px');
        }
    };
    
    var checkRepositionFirstElements = function(carousel, to) {
        if(carousel.circular && to == (-(carousel.length * carousel.moveBy) + carousel.offset)) {
            carousel.moveable.css(carousel.property, carousel.offset + 'px');
            positionFirstElements(carousel, true);
        }
    };
    
    var moveToPosition = function(carousel, to) {
        if(carousel.duration) {
            var p = {};
            p[carousel.property] = to;
            carousel.moveable.animate(p, carousel.duration, function() {
                checkRepositionFirstElements(carousel, to);
                if(carousel.autoplaying) {
                    carousel.startAutoplay(carousel.autoplay);
                }
                carousel.moving = false;
            });
        } else {
            carousel.moveable.css(carousel.property, to + 'px');
            checkRepositionFirstElements(carousel, to);
            if(carousel.autoplaying) {
                carousel.startAutoplay(carousel.autoplay);
            }
            carousel.moving = false;
        }
    };
    
    var getPageNumber = function(carousel) {
        return Math.ceil(carousel.length / carousel.display);
    };
    
    var getPrevPageIndex = function(carousel) {
        var page = carousel.currentPage - 1;
        if(page < 1) {
            page = carousel.circular ? getPageNumber(carousel) : 1;
        }
        return page * carousel.display - carousel.display;
    };
    
    var getNextPageIndex = function(carousel) {
        var page = carousel.currentPage + 1;
        if(page > getPageNumber(carousel)) {
            page = carousel.circular ? 1 : getPageNumber(carousel);
        }
        return page * carousel.display - carousel.display;
    };
    
    var managePagination = function(e, carousel) {
        if(e.target.nodeName.toLowerCase() == 'a') {
            e.preventDefault();
            if(!carousel.moving) {
                carousel.moveToItem((Number(e.target.innerHTML) - 1) * carousel.display + 1);
            }
        }
    };
    
    var Carousel = function(datas) {
        this.initialize(datas);
    };
    Carousel.prototype = {
        initialize: function(datas) {
            var that = this;
            
            that.id = datas.id;
            
            that.property = datas.horizontal ? 'left' : 'top';
            that.buttons = datas.buttons === false ? false : true;
            that.circular = !!datas.circular || false;
            that.duration = !isNaN(datas.duration) ? datas.duration : null;
            that.autoplay = !isNaN(datas.autoplay) && datas.autoplay > 10 && that.circular ? datas.autoplay : null;
            that.hasOffset = !!datas.hasOffset;
            
            that.element = $(datas.element);
            that.moveable = that.element.children().children();
            that.items = that.moveable.children();
            
            that.length = that.items.length;
            that.display = datas.display;
            that.startAt = !isNaN(datas.startAt) ? datas.startAt - 1 : 0;
            // startAt must be >= 0 && < length
            if(that.startAt < 0 || that.startAt >= that.length) {
                that.startAt = 0;
            }
            
            that.offset = parseInt(that.moveable.css(that.property), 10) || 0;
            // negative offset are forbidden
            // if offset, length must be > display + 1
            // no offset possible on a non circular carousel
            // a paginated carousel cannot be circular (too strange)
            if(that.hasOffset && that.offset > 0 && that.length > that.display + 1) {
                ++that.display;
            }
            that.moveBy = that.items.eq(0)[that.property == 'top' ? 'outerHeight' : 'outerWidth'](true);
            
            setCurrent(that, that.startAt);
            
            if(that.property == 'left') {
                that.moveable.width(that.moveBy * that.length);
            }
            if(that.circular) {
                positionFirstElements(that);
            }
            
            if(that.length > that.display) {
                that.disabled = false;
                
                if(!that.circular && that.current > that.length - that.display) {
                    that.current = that.length - that.display;
                }
                
                if(that.current) {
                    that.moveable.css(that.property, -that.moveBy * that.current + that.offset);
                    that.offset = -that.moveBy * that.current + that.offset;
                }
                
                if(that.buttons) {
                    that.buttonNext = $(getBtnsHTML('next')).prependTo(that.element).click(function(e) {
                        that.moveNext(e);
                        e.preventDefault();
                    });
                    that.buttonPrev = $(getBtnsHTML('prev')).prependTo(that.element).click(function(e) {
                        that.movePrev(e);
                        e.preventDefault();
                    });
                    
                    if(that.circular || that.current) {
                        that.buttonPrev.removeClass(config.btnDisabledCls);
                    }
                    if(that.circular || that.current + that.display < that.length) {
                        that.buttonNext.removeClass(config.btnDisabledCls);
                    }
                }
                
                if(datas.paginate) {
                    that.addPagination();
                }
                
                if(that.autoplay) {
                    that.startAutoplay(that.autoplay);
                }
            } else {
                that.disabled = true;
            }
        },
        
        movePrev: function() {
            if(!this.moving) {
                var index = !isNaN(this.currentPage) ? getPrevPageIndex(this) : this.current - 1;
                if(this.circular) {
                    prepareCircularMovePrev(this, index);
                } else if(index > -1) {
                    prepareMove(this, index);
                }
            }
        },
        
        moveNext: function() {
            if(!this.moving) {
                var index = !isNaN(this.currentPage) ? getNextPageIndex(this) : this.current + 1;
                if(this.circular) {
                    prepareCircularMoveNext(this, index);
                } else if(index < this.length) {
                    prepareMove(this, index);
                }
            }
        },
        
        moveToItem: function(i) {
            if(!this.moving && typeof i == 'number') {
                --i;
                if(this.items[i]) {
                    if(this.currentPage) {
                        var page = Math.floor(i / this.display) + 1;
                        i = (page - 1) * this.display;
                    }
                    if(this.circular) {
                        if(i > this.current && i - this.current > this.length - i) {
                            i = i - this.length;
                        } else if(i < this.current && this.current - i > i + this.length - this.current) {
                            i = this.length + i;
                        }
                        if(i < this.current) {
                            prepareCircularMovePrev(this, i);
                        } else if(i > this.current) {
                            prepareCircularMoveNext(this, i);
                        }
                    } else {
                        prepareMove(this, i);
                    }
                }
            }
        },
        
        startAutoplay: function(delay) {
            var that = this;
            if(that.circular && (!isNaN(delay) || that.autoplay)) {
                if(isNaN(delay)) {
                    delay = that.autoplay;
                } else {
                    that.autoplay = delay;
                }
                that.autoplaying = true;
                that.timer = W.setInterval(function() {
                    that.moveNext();
                }, delay);
            }
        },
        
        pauseAutoplay: function() {
            W.clearInterval(this.timer);
            this.timer = null;
        },
        
        endAutoplay: function() {
            this.pauseAutoplay();
            this.autoplaying = false;
        },
        
        addPagination: function() {
            var that = this;
            var html = config.pagesWrap.replace('{$pagesWrapCls}', config.pagesWrapCls);
            var pages = getPageNumber(that);
            var startItem, endItem, items = '';
            for(var i = 1; i <= pages; i++) {
                startItem = (i - 1) * that.display;
                endItem = startItem + that.display - 1;
                if(that.startAt >= startItem && that.startAt <= endItem) {
                    that.currentPage = i;
                    items += config.pagesItem.replace('{$pagesItemActiveCls}', ' class="' + config.pagesItemActiveCls + '"');
                } else {
                    items += config.pagesItem.replace('{$pagesItemActiveCls}', '');
                }
                items = items.replace('{$content}', i);
            }
            html = html.replace('{$content}', items);
            this.pagination = $(html).appendTo(that.element).click(function(e) {
                managePagination(e, that);
            });
        },
        
        removePagination: function() {
            this.pagination.unbind('click').remove();
        }
    };
    
})();

})(jQuery);


