Element.implement({
    isDisplayed: function () {
        return this.getStyle('display') != 'none';
    },
    toggle: function () {
        return this[this.isDisplayed() ? 'hide' : 'show']();
    },
    hide: function () {
        var d;
        try {
            d = this.getStyle('display');
        } catch (e) {}
        return this.store('originalDisplay', d || 'block').setStyle('display', 'none');
    },
    show: function (display) {
        return this.setStyle('display', display || this.retrieve('originalDisplay') || 'block');
    }
});


Array.implement({
    intersect: function (other) {
        var cpy = this.slice();
        this.each(function (el) {
            if (other.indexOf(el) < 0) {
                cpy.splice(cpy.indexOf(el), 1);
            }
        }, this);
        return cpy;
    },
    differentiate: function (other) {
        var src = this.slice();
        var cmp = other.slice();
        other.each(function (elem) {
            if (src.indexOf(elem) > -1) {
                src.splice(src.indexOf(elem), 1);
                cmp.splice(cmp.indexOf(elem), 1);
            }
        }, this);
        return src.combine(cmp);
    },
    getRange: function (start, elements) {
        var res = [];
        var j = 0;
        var upper = start + elements > this.length ? this.length : start + elements;
        if (start >= 0) {
            for (var i = start; i < upper; i++) {
                res[j++] = this[i];
            }
        }
        return res;
    },
    invoke: function (methodName) {
        var args = Array.slice(arguments, 1),
            results = [];
        for (var i = 0, j = this.length; i < j; i++) {
            var item = this[i];
            results.push(item[methodName].apply(item, args));
        }
        return results;
    },
    searchBinary: function (value, insert) {
        var h = this.length,
            l = -1,
            m;
        while (h - l > 1) {
            if (this[m = h + l >> 1] < value) {
                l = m;
            } else {
                h = m;
            }
        }
        return this[h] != value ? insert ? h : -1 : h;
    },
    unique: function () {
        return [].combine(this);
    }
});


Element.implement({
    measure: function (fn) {
        var vis = function (el) {
                return !!(!el || el.offsetHeight || el.offsetWidth);
            };
        if (vis(this)) return fn.apply(this);
        var parent = this.getParent(),
            restorers = [],
            toMeasure = [];
        while (!vis(parent) && parent != document.body) {
            toMeasure.push(parent.expose());
            parent = parent.getParent();
        }
        var restore = this.expose();
        var result = fn.apply(this);
        restore();
        toMeasure.each(function (restore) {
            restore();
        });
        return result;
    },
    expose: function () {
        if (this.getStyle('display') != 'none') return $empty;
        var before = this.style.cssText;
        this.setStyles({
            display: 'block',
            position: 'absolute',
            visibility: 'hidden'
        });
        return function () {
            this.style.cssText = before;
        }.bind(this);
    },
    getDimensions: function (options) {
        options = $merge({
            computeSize: false
        }, options);
        var dim = {};
        var getSize = function (el, options) {
                return (options.computeSize) ? el.getComputedSize(options) : el.getSize();
            };
        var parent = this.getParent('body');
        if (parent && this.getStyle('display') == 'none') {
            dim = this.measure(function () {
                return getSize(this, options);
            });
        } else if (parent) {
            try {
                dim = getSize(this, options);
            } catch (e) {}
        } else {
            dim = {
                x: 0,
                y: 0
            };
        }
        return $chk(dim.x) ? $extend(dim, {
            width: dim.x,
            height: dim.y
        }) : $extend(dim, {
            x: dim.width,
            y: dim.height
        });
    },
    getComputedSize: function (options) {
        options = $merge({
            styles: ['padding', 'border'],
            plains: {
                height: ['top', 'bottom'],
                width: ['left', 'right']
            },
            mode: 'both'
        }, options);
        var size = {
            width: 0,
            height: 0
        };
        switch (options.mode) {
        case 'vertical':
            delete size.width;
            delete options.plains.width;
            break;
        case 'horizontal':
            delete size.height;
            delete options.plains.height;
            break;
        }
        var getStyles = [];
        $each(options.plains, function (plain, key) {
            plain.each(function (edge) {
                options.styles.each(function (style) {
                    getStyles.push((style == 'border') ? style + '-' + edge + '-' + 'width' : style + '-' + edge);
                });
            });
        });
        var styles = {};
        getStyles.each(function (style) {
            styles[style] = this.getComputedStyle(style);
        }, this);
        var subtracted = [];
        $each(options.plains, function (plain, key) {
            var capitalized = key.capitalize();
            size['total' + capitalized] = size['computed' + capitalized] = 0;
            plain.each(function (edge) {
                size['computed' + edge.capitalize()] = 0;
                getStyles.each(function (style, i) {
                    if (style.test(edge)) {
                        styles[style] = styles[style].toInt() || 0;
                        size['total' + capitalized] = size['total' + capitalized] + styles[style];
                        size['computed' + edge.capitalize()] = size['computed' + edge.capitalize()] + styles[style];
                    }
                    if (style.test(edge) && key != style && (style.test('border') || style.test('padding')) && !subtracted.contains(style)) {
                        subtracted.push(style);
                        size['computed' + capitalized] = size['computed' + capitalized] - styles[style];
                    }
                });
            });
        });
        ['Width', 'Height'].each(function (value) {
            var lower = value.toLowerCase();
            if (!$chk(size[lower])) return;
            size[lower] = size[lower] + this['offset' + value] + size['computed' + value];
            size['total' + value] = size[lower] + size['total' + value];
            delete size['computed' + value];
        }, this);
        return $extend(styles, size);
    }
});


Element.Properties.csstext = {
    set: function (css_text) {
        if (this.get('tag') == 'style') {
            this.erase('csstext');
            if (Browser.Engine.trident && this.styleSheet) {
                this.styleSheet.cssText = css_text;
            } else if (this.sheet) {
                this.appendChild(document.createTextNode(css_text));
            }
        }
        return this;
    },
    get: function () {
        if (this.get('tag') == 'style') {
            var css_text = '';
            if (Browser.Engine.trident && this.styleSheet) {
                css_text = this.styleSheet.cssText;
            } else if (this.sheet) {
                css_text = this.innerHTML;
            }
            return css_text;
        }
    },
    erase: function () {
        if (this.get('tag') == 'style') {
            if (Browser.Engine.trident && this.styleSheet) {
                this.styleSheet.cssText = '';
            } else if (this.sheet && this.firstChild) {
                while (this.firstChild) {
                    this.removeChild(this.firstChild);
                }
            }
        }
        return this;
    }
};


Fx.Scroll = new Class({
    Extends: Fx,
    options: {
        offset: {
            'x': 0,
            'y': 0
        },
        wheelStops: true
    },
    initialize: function (element, options) {
        this.element = this.subject = $(element);
        this.parent(options);
        var cancel = this.cancel.bind(this, false);
        if ($type(this.element) != 'element') this.element = $(this.element.getDocument().body);
        var stopper = this.element;
        if (this.options.wheelStops) {
            this.addEvent('start', function () {
                stopper.addEvent('mousewheel', cancel);
            }, true);
            this.addEvent('complete', function () {
                stopper.removeEvent('mousewheel', cancel);
            }, true);
        }
    },
    set: function () {
        var now = Array.flatten(arguments);
        this.element.scrollTo(now[0], now[1]);
    },
    compute: function (from, to, delta) {
        var now = [];
        var x = 2;
        x.times(function (i) {
            now.push(Fx.compute(from[i], to[i], delta));
        });
        return now;
    },
    start: function (x, y) {
        if (!this.check(arguments.callee, x, y)) return this;
        var offsetSize = this.element.getSize(),
            scrollSize = this.element.getScrollSize();
        var scroll = this.element.getScroll(),
            values = {
                x: x,
                y: y
            };
        for (var z in values) {
            var max = scrollSize[z] - offsetSize[z];
            if ($chk(values[z])) values[z] = ($type(values[z]) == 'number') ? values[z].limit(0, max) : max;
            else values[z] = scroll[z];
            values[z] += this.options.offset[z];
        }
        return this.parent([scroll.x, scroll.y], [values.x, values.y]);
    },
    toTop: function () {
        return this.start(false, 0);
    },
    toLeft: function () {
        return this.start(0, false);
    },
    toRight: function () {
        return this.start('right', false);
    },
    toBottom: function () {
        return this.start(false, 'bottom');
    },
    toElement: function (el) {
        var position = $(el).getPosition(this.element);
        return this.start(position.x, position.y);
    }
});


var Drag = new Class({
    Implements: [Events, Options],
    options: {
        snap: 6,
        unit: 'px',
        grid: false,
        style: true,
        limit: false,
        handle: false,
        invert: false,
        preventDefault: false,
        modifiers: {
            x: 'left',
            y: 'top'
        }
    },
    initialize: function () {
        var params = Array.link(arguments, {
            'options': Object.type,
            'element': $defined
        });
        this.element = $(params.element);
        this.document = this.element.getDocument();
        this.setOptions(params.options || {});
        var htype = $type(this.options.handle);
        this.handles = (htype == 'array' || htype == 'collection') ? $$(this.options.handle) : $(this.options.handle) || this.element;
        this.mouse = {
            'now': {},
            'pos': {}
        };
        this.value = {
            'start': {},
            'now': {}
        };
        this.selection = (Browser.Engine.trident) ? 'selectstart' : 'mousedown';
        this.bound = {
            start: this.start.bind(this),
            check: this.check.bind(this),
            drag: this.drag.bind(this),
            stop: this.stop.bind(this),
            cancel: this.cancel.bind(this),
            eventStop: $lambda(false)
        };
        this.attach();
    },
    attach: function () {
        this.handles.addEvent('mousedown', this.bound.start);
        return this;
    },
    detach: function () {
        this.handles.removeEvent('mousedown', this.bound.start);
        return this;
    },
    start: function (event) {
        if (this.options.preventDefault) event.preventDefault();
        this.fireEvent('beforeStart', this.element);
        this.mouse.start = event.page;
        var limit = this.options.limit;
        this.limit = {
            'x': [],
            'y': []
        };
        for (var z in this.options.modifiers) {
            if (!this.options.modifiers[z]) continue;
            if (this.options.style) this.value.now[z] = this.element.getStyle(this.options.modifiers[z]).toInt();
            else this.value.now[z] = this.element[this.options.modifiers[z]];
            if (this.options.invert) this.value.now[z] *= -1;
            this.mouse.pos[z] = event.page[z] - this.value.now[z];
            if (limit && limit[z]) {
                for (var i = 2; i--; i) {
                    if ($chk(limit[z][i])) this.limit[z][i] = $lambda(limit[z][i])();
                }
            }
        }
        if ($type(this.options.grid) == 'number') this.options.grid = {
            'x': this.options.grid,
            'y': this.options.grid
        };
        this.document.addEvents({
            mousemove: this.bound.check,
            mouseup: this.bound.cancel
        });
        this.document.addEvent(this.selection, this.bound.eventStop);
    },
    check: function (event) {
        if (this.options.preventDefault) event.preventDefault();
        var distance = Math.round(Math.sqrt(Math.pow(event.page.x - this.mouse.start.x, 2) + Math.pow(event.page.y - this.mouse.start.y, 2)));
        if (distance > this.options.snap) {
            this.cancel();
            this.document.addEvents({
                mousemove: this.bound.drag,
                mouseup: this.bound.stop
            });
            this.fireEvent('start', this.element).fireEvent('snap', this.element);
        }
    },
    drag: function (event) {
        if (this.options.preventDefault) event.preventDefault();
        this.mouse.now = event.page;
        for (var z in this.options.modifiers) {
            if (!this.options.modifiers[z]) continue;
            this.value.now[z] = this.mouse.now[z] - this.mouse.pos[z];
            if (this.options.invert) this.value.now[z] *= -1;
            if (this.options.limit && this.limit[z]) {
                if ($chk(this.limit[z][1]) && (this.value.now[z] > this.limit[z][1])) {
                    this.value.now[z] = this.limit[z][1];
                } else if ($chk(this.limit[z][0]) && (this.value.now[z] < this.limit[z][0])) {
                    this.value.now[z] = this.limit[z][0];
                }
            }
            if (this.options.grid[z]) this.value.now[z] -= (this.value.now[z] % this.options.grid[z]);
            if (this.options.style) this.element.setStyle(this.options.modifiers[z], this.value.now[z] + this.options.unit);
            else this.element[this.options.modifiers[z]] = this.value.now[z];
        }
        this.fireEvent('drag', this.element);
    },
    cancel: function (event) {
        this.document.removeEvent('mousemove', this.bound.check);
        this.document.removeEvent('mouseup', this.bound.cancel);
        if (event) {
            this.document.removeEvent(this.selection, this.bound.eventStop);
            this.fireEvent('cancel', this.element);
        }
    },
    stop: function (event) {
        this.document.removeEvent(this.selection, this.bound.eventStop);
        this.document.removeEvent('mousemove', this.bound.drag);
        this.document.removeEvent('mouseup', this.bound.stop);
        if (event) this.fireEvent('complete', this.element);
    }
});
Element.implement({
    makeResizable: function (options) {
        return new Drag(this, $merge({
            modifiers: {
                'x': 'width',
                'y': 'height'
            }
        }, options));
    }
});


var Slider = new Class({
    Implements: [Events, Options],
    options: {
        onTick: function (position) {
            if (this.options.snap) position = this.toPosition(this.step);
            this.knob.setStyle(this.property, position);
        },
        snap: false,
        offset: 0,
        range: false,
        wheel: false,
        steps: 100,
        mode: 'horizontal'
    },
    initialize: function (element, knob, options) {
        this.setOptions(options);
        this.element = $(element);
        this.knob = $(knob);
        this.previousChange = this.previousEnd = this.step = -1;
        this.element.addEvent('mousedown', this.clickedElement.bind(this));
        if (this.options.wheel) this.element.addEvent('mousewheel', this.scrolledElement.bindWithEvent(this));
        var offset, limit = {},
            modifiers = {
                'x': false,
                'y': false
            };
        switch (this.options.mode) {
        case 'vertical':
            this.axis = 'y';
            this.property = 'top';
            offset = 'offsetHeight';
            break;
        case 'horizontal':
            this.axis = 'x';
            this.property = 'left';
            offset = 'offsetWidth';
        }
        this.half = this.knob[offset] / 2;
        this.full = this.element[offset] - this.knob[offset] + (this.options.offset * 2);
        this.min = $chk(this.options.range[0]) ? this.options.range[0] : 0;
        this.max = $chk(this.options.range[1]) ? this.options.range[1] : this.options.steps;
        this.range = this.max - this.min;
        this.steps = this.options.steps || this.full;
        this.stepSize = Math.abs(this.range) / this.steps;
        this.stepWidth = this.stepSize * this.full / Math.abs(this.range);
        this.knob.setStyle('position', 'relative').setStyle(this.property, -this.options.offset);
        modifiers[this.axis] = this.property;
        limit[this.axis] = [-this.options.offset, this.full - this.options.offset];
        this.drag = new Drag(this.knob, {
            snap: 0,
            limit: limit,
            modifiers: modifiers,
            onDrag: this.draggedKnob.bind(this),
            onStart: this.draggedKnob.bind(this),
            onComplete: function () {
                this.draggedKnob();
                this.end();
            }.bind(this)
        });
        if (this.options.snap) {
            this.drag.options.grid = Math.ceil(this.stepWidth);
            this.drag.options.limit[this.axis][1] = this.full;
        }
    },
    set: function (step) {
        if (!((this.range > 0) ^ (step < this.min))) step = this.min;
        if (!((this.range > 0) ^ (step > this.max))) step = this.max;
        this.step = Math.round(step);
        this.checkStep();
        this.end();
        this.fireEvent('tick', this.toPosition(this.step));
        return this;
    },
    clickedElement: function (event) {
        var dir = this.range < 0 ? -1 : 1;
        var position = event.page[this.axis] - this.element.getPosition()[this.axis] - this.half;
        position = position.limit(-this.options.offset, this.full - this.options.offset);
        this.step = Math.round(this.min + dir * this.toStep(position));
        this.checkStep();
        this.end();
        this.fireEvent('tick', position);
    },
    scrolledElement: function (event) {
        var mode = (this.options.mode == 'horizontal') ? (event.wheel < 0) : (event.wheel > 0);
        this.set(mode ? this.step - this.stepSize : this.step + this.stepSize);
        event.stop();
    },
    draggedKnob: function () {
        var dir = this.range < 0 ? -1 : 1;
        var position = this.drag.value.now[this.axis];
        position = position.limit(-this.options.offset, this.full - this.options.offset);
        this.step = Math.round(this.min + dir * this.toStep(position));
        this.checkStep();
    },
    checkStep: function () {
        if (this.previousChange != this.step) {
            this.previousChange = this.step;
            this.fireEvent('change', this.step);
        }
    },
    end: function () {
        if (this.previousEnd !== this.step) {
            this.previousEnd = this.step;
            this.fireEvent('complete', this.step + '');
        }
    },
    toStep: function (position) {
        var step = (position + this.options.offset) * this.stepSize / this.full * this.steps;
        return this.options.steps ? Math.round(step -= step % this.stepSize) : step;
    },
    toPosition: function (step) {
        return (this.full * Math.abs(this.min - step)) / (this.steps * this.stepSize) - this.options.offset;
    }
});


if (!$defined(SK)) {
    var SK = {};
}
if (!$defined(console)) {
    var console = {
        log: function () {}
    };
}
if (!$defined(Gallery)) {
    var Gallery = {
        Components: {},
        Plugins: {},
        Effects: {},
        Constants: {
            notificationGeneratingMessage: 'This gallery contains images that are still being generated. Please reload the page in a few seconds.',
            placeholder_width: 10,
            effects: {
                reflex: {
                    min_height: 30,
                    percents_height: 15,
                    opacity: 0.25
                },
                polaroid: {}
            },
            slide_duration: 1000,
            fade_duration: 1000,
            slideshow: {
                pages_to_preload: 5
            },
            thumbnails: {
                pages_to_preload: 1
            },
            slider: {
                pixels_per_second: 50,
                frames_per_second: 50,
                pages_to_preload: 2,
                fast_speed_factor: 8
            },
            viewer: {
                pages_to_preload: 5
            },
            css: {
                trident: {},
                gecko: {},
                webkit: {}
            }
        },
        Utils: {
            instances: {},
            saveInstance: function (id, instance) {
                this.instances['id_' + id] = instance;
            },
            getInstance: function (id) {
                return this.instances['id_' + id];
            },
            log: function () {
                if ($defined(window.console) && $type(window.console.log) == 'function') {
                    window.console.log.apply(window.console, arguments);
                }
            },
            stringToBool: function (str) {
                switch (true) {
                case !$defined(str):
                case str === false:
                case str == 'none':
                    return false;
                    break;
                case str == 'true':
                    return true;
                    break;
                case !isNaN(str):
                    return !!Number(str);
                    break;
                }
                return true;
            },
            convertDataType: function (obj) {
                for (var key in obj) {
                    if (!$defined(obj[key])) {
                        obj[key] = false;
                        continue;
                    }
                    if (obj[key] === true || obj[key] === false) {
                        continue;
                    }
                    if ($type(obj[key]) == 'array') {} else if ($type(obj[key]) == 'object') {
                        obj[key] = this.convertDataType(obj[key]);
                    } else {
                        if (!isNaN(obj[key])) {
                            obj[key] = parseFloat(obj[key]);
                        } else {
                            switch (true) {
                            case obj[key] == 'false':
                            case obj[key] == 'none':
                            case obj[key] == '':
                                obj[key] = false;
                                break;
                            case obj[key] == 'true':
                                obj[key] = true;
                                break;
                            }
                        }
                    }
                }
                return obj;
            },
            Element: {
                getClassSelector: function (element) {
                    return element.get('class').clean().split(' ').map(function (str) {
                        return str.length > 0 ? str.replace(/^/, '.') : ''
                    }).join('');
                },
                getComputedSizeSum: function (element, properties) {
                    var result = 0,
                        value;
                    for (var i = 0; i < properties.length; i++) {
                        var value = element.getComputedStyle(properties[i]).toInt();
                        result += isNaN(value) ? 0 : value;
                    }
                    return result;
                },
                getMarginWidth: function (element) {
                    return this.getComputedSizeSum(element, ['margin-left', 'margin-right']);
                },
                getMarginHeight: function (element) {
                    return this.getComputedSizeSum(element, ['margin-top', 'margin-bottom']);
                },
                getBorderWidth: function (element) {
                    return this.getComputedSizeSum(element, ['border-left-width', 'border-right-width']);
                },
                getBorderHeight: function (element) {
                    return this.getComputedSizeSum(element, ['border-top-width', 'border-bottom-width']);
                },
                getPaddingWidth: function (element) {
                    return this.getComputedSizeSum(element, ['padding-left', 'padding-right']);
                },
                getPaddingHeight: function (element) {
                    return this.getComputedSizeSum(element, ['padding-top', 'padding-bottom']);
                },
                getHorizontalOffsets: function (element) {
                    return this.getComputedSizeSum(element, ['padding-left', 'padding-right', 'margin-left', 'margin-right', 'border-left-width', 'border-right-width']);
                },
                getVerticalOffsets: function (element) {
                    return this.getComputedSizeSum(element, ['padding-top', 'padding-bottom', 'margin-top', 'margin-bottom', 'border-top-width', 'border-bottom-width']);
                }
            }
        },
        PerformanceTest: {
            tests: {},
            start: function (test_id) {
                if (this.tests[test_id] == undefined) {
                    this.tests[test_id] = {
                        start: new Date(),
                        end: null,
                        result: null
                    };
                }
            },
            end: function (test_id) {
                if (this.tests[test_id] && this.tests[test_id].end == null) {
                    this.tests[test_id].end = new Date();
                    this.tests[test_id].result = this.tests[test_id].end.getTime() - this.tests[test_id].start.getTime();
                }
            },
            dump: function (dom_id) {
                if (Browser.Engine.trident && this.resultIE) {
                    this.resultIE.value = '';
                }
                for (test_id in this.tests) {
                    if (Browser.Engine.trident) {
                        if (!this.resultIE) {
                            this.resultIE = new Element('textarea', {
                                style: 'width: 800px; height: 200px;'
                            }).inject(document.body, 'top');
                        }
                        this.resultIE.value += test_id + '=' + this.tests[test_id].result + 'ms\n';
                    }
                    Gallery.Utils.log(test_id, '=', this.tests[test_id].result, 'ms');
                    delete this.tests[test_id];
                }
            }
        }
    }
    if (!window.location.href.test('gallery_performance_test')) {
        for (var property in Gallery.PerformanceTest) {
            if ($type(Gallery.PerformanceTest[property] == 'function')) {
                Gallery.PerformanceTest[property] = $empty;
            }
        }
    }
}
if (!$defined(Gallery.styleSheets)) {
    Gallery.styleSheets = {
        styles: new Element('style', {
            type: 'text/css',
            id: 'ig_css_styles'
        }).inject(document.documentElement.firstChild),
        index: 0,
        writecss: function (selector, properties) {
            var css = selector + ' {';
            for (var property in properties) {
                css += this.createCssProperty(property, properties[property]);
            }
            css += '}';
            if (Browser.Engine.trident && this.styles.styleSheet) {
                Gallery.PerformanceTest.start(this.index + ' ' + css)
                this.styles.styleSheet.cssText += css;
                Gallery.PerformanceTest.end(this.index + ' ' + css);
                this.index++;
            } else if (this.styles.sheet) {
                this.styles.appendChild(document.createTextNode(css));
            }
        },
        createCssProperty: function (property, value) {
            if ($type(value) != 'string') {
                var map = (Element.Styles.get(property.camelCase()) || '@').split(' ');
                value = $splat(value).map(function (val, i) {
                    if (!map[i]) return '';
                    return ($type(val) == 'number') ? map[i].replace('@', Math.round(val)) : val;
                }).join(' ');
            } else if (value == String(Number(value))) {
                value = Math.round(value);
            }
            return property + ':' + value + ';'
        }
    }
    if ($defined(Gallery.Constants.css[Browser.Engine.name])) {
        var browser_css = Gallery.Constants.css[Browser.Engine.name];
        for (var selector in browser_css) {
            Gallery.styleSheets.writecss(selector, browser_css[selector]);
        }
    }
}



Gallery.Components.ComponentBase = new Class({
    domNode: null,
    options: {},
    bounds: {},
    initialize: function (options) {
        for (var key in options) {
            this.options[key] = options[key];
        }
    },
    inject: function (node) {
        node.appendChild(this.domNode);
    },
    appendChild: function (node) {
        this.domNode.appendChild(node);
    },
    getDomNode: function () {
        return $(this.domNode);
    },
    hide: function () {
        return this.domNode.style.display = 'none';
    },
    show: function () {
        return this.domNode.style.display = 'block'
    },
    dispatchEvent: function () {
        this.options.plugin.fireEvent.apply(this.options.plugin, arguments);
    },
    addEvent: function () {
        this.options.plugin.addEvent.apply(this.options.plugin, arguments);
    },
    addEvents: function () {
        this.options.plugin.addEvents.apply(this.options.plugin, arguments);
    },
    removeEvent: function () {
        this.options.plugin.removeEvent.apply(this.options.plugin, arguments);
    },
    setConstant: function () {
        return this.options.plugin.setConstant.apply(this.options.plugin, arguments);
    },
    getConstant: function () {
        return this.options.plugin.getConstant.apply(this.options.plugin, arguments);
    }
});


Gallery.Plugins.PluginBase = new Class({
    Extends: Events,
    Implements: [Options],
    bounds: {
        events: {}
    },
    domNode: null,
    options: {},
    created: false,
    mainEventName: 'gotoItem',
    List: {},
    Navigators: {},
    constants: {},
    type: null,
    currentIndex: null,
    pages: [],
    playOn: false,
    isAdminMode: false,
    notifyGenerating: true,
    admin: {
        initialized: false,
        activated: false,
        resumePlay: false
    },
    initialize: function (wrapper_id, options) {
        Gallery.Utils.convertDataType(options['visualization']);
        Gallery.Utils.convertDataType(options['viewer']);
        this.wrapperId = wrapper_id;
        Gallery.PerformanceTest.start('setOptions');
        this.setOptions(options);
        Gallery.PerformanceTest.end('setOptions');
        this.domNode = $(wrapper_id);
        if (!this.domNode) {
            alert("failed to initialize Gallery - " + this.type);
            return;
        }
        this.instanceId = options.id ? options.id : wrapper_id;
        Gallery.Utils.saveInstance(this.instanceId, this);
        this.domNode.addClass('ig-gallery').addClass('ig-type-' + this.type);
        this.bounds.events.gotoItem = this.gotoItem.bind(this);
        this.bounds.events.buttonNext = this.next.bind(this);
        this.bounds.events.buttonPrev = this.previous.bind(this);
        this.bounds.events.buttonPlay = this.play.bind(this);
        this.bounds.events.buttonPause = this.pause.bind(this);
        this.bounds.events.view = this.view.bind(this);
        this.bounds.events.loadThumbnail = this.loadThumbnail.bind(this);
        this.bounds.events.generateThumbnail = this.generateThumbnail.bind(this);
        this.addEvents(this.bounds.events);
        this.showLoadingAnimation();
        this.attachLoadHandler();
        this.addEvent('afterPreload', function () {
            Gallery.PerformanceTest.end('initialize-' + this.type);
            Gallery.PerformanceTest.dump();
        }.bind(this));
        this.isAdminMode = false;
        this.notifyGenerating = this.isAdminMode;
        if (this.isAdminMode) {
            this.addEvent('buttonPause', this.adminHighlightPause.bind(this));
        }
    },
    attachLoadHandler: function () {
        window.addEvent('domready', this.display.bind(this));
    },
    showLoadingAnimation: function () {
        this.domNode.addClass('ig-loading');
        this.domNode.setStyle('height', this.options.visualization.thumbnail_size.height);
    },
    hideLoadingAnimation: function () {
        this.domNode.setStyle('height', '100%');
        this.domNode.removeClass('ig-loading');
    },
    display: function (index) {
        Gallery.PerformanceTest.start('initialize-' + this.type);
        if (!this.created) {
            Gallery.PerformanceTest.start('createComponents');
            this.createComponents();
            Gallery.PerformanceTest.end('createComponents');
            this.created = true;
            this.hideLoadingAnimation();
        }
        this.fireEvent(this.mainEventName, [$pick(index, 0)]);
    },
    gotoItem: function (index) {
        this.List.gotoItem(index);
    },
    syncNavigation: function (index) {
        for (var key in this.Navigators) {
            this.Navigators[key].syncNavigation(index);
        }
        this.currentIndex = index;
    },
    createComponents: function () {},
    inspectStyleOffsets: function () {
        var elements = {};
        elements.ig_thumbs = this.domNode.getElement('.ig-thumbs');
        elements.ig_thumb = elements.ig_thumbs.getElement('.ig-thumb');
        elements.ig_img = elements.ig_thumb.getElement('.ig-img');
        elements.img = elements.ig_img.getElement('img');
        for (var key in elements) {
            var element = elements[key];
            this.setConstant(key + '_offset_x', Gallery.Utils.Element.getHorizontalOffsets(element));
            this.setConstant(key + '_offset_y', Gallery.Utils.Element.getVerticalOffsets(element));
            if (key == 'ig_thumbs') {
                this.setConstant('ig_thumbs_inner_offset_x', Gallery.Utils.Element.getComputedSizeSum(element, ['padding-left', 'padding-right']));
                this.setConstant('ig_thumbs_inner_offset_y', Gallery.Utils.Element.getComputedSizeSum(element, ['padding-top', 'padding-bottom']));
            }
        }
        var img_width = this.getConstant('img_offset_x') + this.options.visualization.thumbnail_size.width;
        var img_height = this.getConstant('img_offset_y') + this.options.visualization.thumbnail_size.height;
        var reflex_height = 0;
        if (this.options.visualization.thumbnail_effect == 'reflection') {
            var reflex_height = img_height * (Gallery.Constants.effects.reflex.percents_height / 100);
            reflex_height = Math.max(reflex_height, Gallery.Constants.effects.reflex.min_height);
            img_height += reflex_height;
        }
        var image_width = img_width + this.getConstant('ig_img_offset_x');
        var image_height = img_height + this.getConstant('ig_img_offset_y');
        var thumbnail_width = image_width + this.getConstant('ig_thumb_offset_x');
        var thumbnail_height = image_height + this.getConstant('ig_thumb_offset_y');
        this.setConstant('reflex_height', reflex_height);
        this.setConstant('img_width', img_width);
        this.setConstant('img_height', img_height);
        this.setConstant('image_width', image_width);
        this.setConstant('image_height', image_height);
        this.setConstant('thumbnail_width', thumbnail_width);
        this.setConstant('thumbnail_height', thumbnail_height);
    },
    play: function () {
        this.playOn = true;
        this.List.play();
    },
    pause: function () {
        this.playOn = false;
        this.List.pause();
    },
    next: function () {
        this.List.gotoNext.apply(this.List, arguments);
    },
    previous: function () {
        this.List.gotoPrevious.apply(this.List, arguments);
    },
    view: function (index, event) {
        var element = event.target;
        var image_data = this.getImageData(index);
        switch (image_data.link_type) {
        case 'viewer':
            if (!$defined(this.viewer)) {
                new Element('div', {
                    id: this.wrapperId + '_viewer'
                }).inject(document.body);
                this.viewer = new Gallery.Plugins.Viewer(this.wrapperId + '_viewer', $merge(this.options, {
                    id: this.wrapperId + '_viewer'
                }));
                this.viewer.addEvents({
                    onShow: this.viewerShow.bind(this),
                    onHide: this.viewerHide.bind(this)
                });
                this.viewer.display(index, element);
            } else {
                this.viewer.show(index, element);
            }
            break;
        case 'url':
            if (image_data.link_target == '_self') {
                window.location.href = image_data.link_url;
            } else {
                window.open(image_data.link_url);
            }
            break;
        }
    },
    setCss: function (selector, properties) {
        var prefix = '#' + this.wrapperId + ' ';
        selector = prefix + selector.clean().split(/\s?,\s?/).join(', ' + prefix);
        Gallery.styleSheets.writecss(selector, properties)
    },
    getImageCaption: function (index) {
        return this.getImageData(index).caption;
    },
    getImageData: function (index) {
        if (!$defined(this.options.images[index])) {
            return {};
        }
        return this.options.images[index];
    },
    setConstant: function (name, value) {
        if (!$defined(this.constants[name])) {
            this.constants[name] = value;
        }
    },
    getConstant: function (name) {
        return this.constants[name];
    },
    loadThumbnail: function (loader, node) {
        loader.apply(this);
    },
    generateThumbnail: function (thumbnail) {
        if (this.notifyGenerating && !this.notificationGenerating) {
            this.notificationGenerating = new Element('div', {
                "class": "ig-notification-generating"
            }).grab(new Element('span', {
                text: Gallery.Constants.notificationGeneratingMessage
            })).inject(this.domNode, 'before');
        }
    },
    viewerShow: function () {
        this.fireEvent('viewerShow');
        this.viewer.resumePlay = this.playOn;
        if (this.playOn) {
            this.pause();
        }
    },
    viewerHide: function () {
        this.fireEvent('viewerHide');
        if (this.viewer.resumePlay) {
            this.play();
        }
        this.viewer.resumePlay = false
    },
    adminInitialize: function () {
        this.admin.initialized = true;
    },
    adminActivate: function () {
        if (this.admin.activated === false) {
            if (this.admin.initialized === false) {
                this.adminInitialize()
            }
            this.admin.resumePlay = this.playOn;
            if (this.playOn) {
                this.pause();
            }
            this.admin.activated = true;
        }
    },
    adminDeactivate: function () {
        if (this.admin.activated === true) {
            if (this.admin.resumePlay) {
                this.play();
            }
            this.admin.resumePlay = false
            this.admin.activated = false;
        }
    },
    adminHighlightPause: function () {
        this.admin.highlighted = false;
    },
    adminHighlightShow: function () {
        this.admin.highlighted = this.playOn;
        if (this.playOn) {
            this.pause();
        }
    },
    adminHighlightHide: function () {
        if (this.admin.highlighted) {
            this.play();
        }
        this.admin.highlighted = false
    },
    destroy: function () {
        this.removeEvents(this.bounds.events);
        this.pause();
        this.domNode.getElements('*').destroy();
        for (var key in this.options) {
            delete this.options[key];
        }
        this.domNode.erase('class');
        delete this['bounds'];
        delete this['List'];
        delete this['Navigators'];
    }
});


Gallery.Plugins.Slider = new Class({
    Extends: Gallery.Plugins.PluginBase,
    type: 'slider',
    mainEventName: 'gotoOffset',
    options: {
        visualization: {
            thumbnail_size: {
                width: 600,
                height: 400
            },
            caption_visibility: true,
            navigation: "dots",
            inside_buttons: true,
            thumbnail_transition: "slide",
            thumbnail_effect: false,
            play_button: true,
            autoplay: false,
            slideshow_timeout: 5,
            loop: true
        },
        images: []
    },
    initialize: function (wrapper_id, options) {
        this.bounds.events.gotoOffset = this.gotoOffset.bind(this);
        this.bounds.events.holdPlay = this.holdPlay.bind(this);
        this.bounds.events.releasePlay = this.releasePlay.bind(this);
        this.bounds.events.onListMouseover = this.holdPlay.bind(this);
        this.bounds.events.onListMouseout = this.releasePlay.bind(this);
        this.bounds.events.onButtonPrev = this.previous.bind(this);
        this.bounds.events.onButtonPrevDown = this.previous.pass([Gallery.Constants.slider.fast_speed_factor], this);
        this.bounds.events.onButtonPrevOut = this.releasePlay.bind(this);
        this.bounds.events.onButtonNext = this.next.bind(this);
        this.bounds.events.onButtonNextDown = this.next.pass([Gallery.Constants.slider.fast_speed_factor], this);
        this.bounds.events.onButtonNextOut = this.releasePlay.bind(this);
        this.parent(wrapper_id, options);
        var effect = this.options.visualization.thumbnail_effect;
        this.domNode.addClass('ig-effect-' + (effect == false ? 'none' : effect));
    },
    gotoOffset: function (offset) {
        this.List.gotoOffset(offset);
        this.syncNavigation(offset);
    },
    createComponents: function () {
        this.parent();
        this.listHolder = new Element('td');
        this.scrollBarHolder = new Element('td', {
            "class": "ig-scrollbar-wrapper"
        });
        this.domNode.grab(new Element('table', {
            "align": "center",
            "class": "ig-struct"
        }).grab(new Element('tbody').grab(new Element('tr').grab(this.listHolder)))).grab(new Element('table', {
            "align": "center",
            "class": "ig-struct"
        }).grab(new Element('tbody').grab(new Element('tr').grab(this.scrollBarHolder))));
        Gallery.PerformanceTest.start('createComponentListSlider');
        var list_options = {
            plugin: this,
            thumbnail_size: this.options.visualization.thumbnail_size,
            thumbnail_transition: this.options.visualization.thumbnail_transition,
            thumbnail_effect: this.options.visualization.thumbnail_effect,
            autoplay: this.options.visualization.autoplay,
            slideshow_timeout: this.options.visualization.slideshow_timeout,
            loop: this.options.visualization.loop,
            caption_visibility: this.options.visualization.caption_visibility,
            images: this.options.images,
            src_key: 'thumb_url',
            auto_scale: false,
            notify_generating: this.notifyGenerating,
            pages_to_preload: Gallery.Constants.slideshow.pages_to_preload,
            first_placeholder: this.options.first_arrangement_placeholder,
            create_placeholders: $defined(this.options.first_arrangement_placeholder)
        };
        this.List = new Gallery.Components.ComponentListSlider(list_options);
        this.List.inject(this.listHolder);
        Gallery.PerformanceTest.end('createComponentListSlider');
        Gallery.PerformanceTest.start('inspectStyleOffsets');
        this.inspectStyleOffsets();
        Gallery.PerformanceTest.end('inspectStyleOffsets');
        Gallery.PerformanceTest.start('setGalleryDimensions');
        this.setGalleryDimensions();
        Gallery.PerformanceTest.end('setGalleryDimensions');
        Gallery.PerformanceTest.start('createTransitionList');
        this.createTransitionList();
        Gallery.PerformanceTest.end('createTransitionList');
        var list_width = this.List.getContainer().getWidth();
        var list_scroll_width = this.List.getContainer().getScrollWidth()
        if (list_scroll_width > list_width) {
            this.pages = this.List.getPages();
            if (this.options.visualization.inside_buttons || this.options.visualization.play_button) {
                Gallery.PerformanceTest.start('createComponentButtonsSet');
                var buttons_set_options = {
                    plugin: this,
                    first_item: 0,
                    last_item: list_scroll_width - list_width,
                    inside_buttons: this.options.visualization.inside_buttons,
                    play_button: this.options.visualization.play_button,
                    autoplay: this.options.visualization.autoplay,
                    loop: this.options.visualization.loop,
                    events: {
                        buttonPrev: {
                            click: 'onButtonPrev',
                            mouseover: 'onButtonPrev',
                            mouseout: 'onButtonPrevOut',
                            mousedown: 'onButtonPrevDown'
                        },
                        buttonNext: {
                            click: 'onButtonNext',
                            mouseover: 'onButtonNext',
                            mouseout: 'onButtonNextOut',
                            mousedown: 'onButtonNextDown'
                        },
                        buttonPlay: {
                            click: 'onButtonPlay'
                        },
                        buttonPause: {
                            click: 'onButtonPause'
                        }
                    }
                };
                this.Navigators.ButtonsSet = new Gallery.Components.ComponentButtonsSet(buttons_set_options);
                var list_node = this.List.getDomNode();
                this.Navigators.ButtonsSet.inject(list_node);
                Gallery.PerformanceTest.end('createComponentButtonsSet');
                var thumbnail_height = this.getConstant('thumbnail_height');
                if (this.options.visualization.thumbnail_effect == 'reflection') {
                    thumbnail_height -= this.getConstant('reflex_height');
                }
                Gallery.PerformanceTest.start('setButtonPosition');
                var buttons = this.Navigators.ButtonsSet.getComponentsButtons();
                for (var button_name in buttons) {
                    var element = buttons[button_name].getDomNode();
                    var height = Element.getComputedStyle(element, 'height').toInt();
                    if (button_name == 'buttonPlay' || button_name == 'buttonPause') {
                        var style_top = (thumbnail_height - height);
                    } else {
                        var style_top = thumbnail_height / 2;
                    }
                    Element.setStyle(element, 'top', style_top);
                }
                Gallery.PerformanceTest.end('setButtonPosition');
            }
            if (this.options.visualization.scrollbar) {
                Gallery.PerformanceTest.start('createComponentScrollbar');
                var scroll_options = {
                    plugin: this,
                    frameWidth: this.options.visualization.thumbnail_size.width,
                    scrollWidth: this.options.visualization.thumbnail_size.width,
                    contentWidth: this.List.getListScrollWidth(),
                    events: {
                        buttonPrev: {
                            click: 'onButtonPrevOut',
                            mousedown: 'onButtonPrevDown'
                        },
                        buttonNext: {
                            click: 'onButtonNextOut',
                            mousedown: 'onButtonNextDown'
                        },
                        buttonHandle: {
                            click: 'releasePlay',
                            mouseout: 'releasePlay',
                            mouseover: 'holdPlay',
                            mousedown: 'holdPlay'
                        },
                        change: 'gotoOffset'
                    }
                };
                this.Navigators.Scroll = new Gallery.Components.ComponentScrollbar(scroll_options);
                this.Navigators.Scroll.inject(this.scrollBarHolder);
                Gallery.PerformanceTest.end('createComponentScrollbar');
            }
        }
    },
    createTransitionList: function () {
        this.List.createTransitionList(this.domNode.getElements('.ig-slide'));
    },
    setGalleryDimensions: function () {
        this.setCss('.ig-img td', {
            width: this.getConstant('img_width'),
            height: this.getConstant('img_height')
        });
        this.setCss('.ig-thumb', {
            width: this.getConstant('image_width'),
            position: 'relative'
        });
        this.setCss('.ig-thumbs, .ig-mask', {
            width: this.options.visualization.thumbnail_size.width
        });
        this.List.setDimensions();
        if (this.options.visualization.thumbnail_effect == 'reflection') {
            this.setCss('.ig-mask, .ig-thumb', {
                position: 'relative'
            });
            this.setCss('.ig-caption', {
                position: 'absolute',
                top: this.getConstant('img_height') - this.getConstant('reflex_height')
            });
            this.setCss('.ig-caption p', {
                'overflow': 'hidden'
            });
        }
    },
    holdPlay: function () {
        this.List.stop();
    },
    releasePlay: function () {
        this.List.stop();
        if (this.playOn) {
            this.List.gotoNext();
        }
    },
    adminInitialize: function () {
        this.parent();
        var height = this.getConstant('image_height') - this.getConstant('reflex_height');
        this.setCss('.ig-placeholder', {
            width: Gallery.Constants.placeholder_width,
            height: height
        });
        this.setCss('.ig-placeholder-left', {
            left: 0,
            position: 'absolute'
        });
        this.setCss('.ig-placeholder-right', {
            right: 0,
            position: 'absolute'
        });
        this.setCss('.sk-obj-placeholder', {
            width: Gallery.Constants.placeholder_width,
            height: height
        });
    },
    adminActivate: function () {
        if (this.admin.activated === false) {
            if (this.admin.initialized === false) {
                this.adminInitialize()
            }
            this.setCss('.ig-placeholder', {
                display: 'block'
            });
        }
        this.parent();
    },
    adminDeactivate: function () {
        if (this.admin.activated === true) {
            this.setCss('.ig-placeholder', {
                display: 'none'
            });
            this.List.gotoItem(this.currentIndex, null, true);
        }
        this.parent();
    }
});


Gallery.Plugins.Viewer = new Class({
    Extends: Gallery.Plugins.PluginBase,
    type: 'viewer',
    mainEventName: 'gotoItem',
    overlayOpacity: 0.33,
    animationOptions: {
        overlay: {
            show: {
                transition: 'sine:out',
                duration: 600,
                onComplete: null
            },
            hide: {
                transition: 'sine:in',
                duration: 0,
                onComplete: null
            }
        },
        clone: {
            show: {
                transition: 'sine:out',
                duration: 200,
                onComplete: null
            },
            hide: {
                transition: 'sine:in',
                duration: 0,
                onComplete: null
            }
        },
        body: {
            show: {
                transition: 'sine:out',
                duration: 400,
                onComplete: null
            },
            hide: {
                transition: 'sine:in',
                duration: 0,
                onComplete: null
            }
        }
    },
    currentIndex: 0,
    storage: {},
    drag: null,
    options: {
        viewer: {
            zoom: false,
            inside_buttons: false,
            caption_visibility: true,
            loop: true,
            play_button: false,
            navigation: "buttons_and_numbers",
            dim_lights: true,
            autoplay: false,
            slideshow_timeout: 5,
            size_behaviour: 'resizable',
            width: 800,
            height: 400
        },
        images: []
    },
    initialize: function () {
        this.bounds.viewerKeydown = this.onKeyDown.bind(this)
        if (this.options.viewer.size_behaviour == 'static') {
            this.bounds.viewerResize = this.onResizeStatic.bind(this);
        } else {
            this.bounds.viewerResize = this.onResize.bind(this);
        }
        this.bounds.events.listStart = this.closeZoomOnStart.bind(this);
        this.bounds.zoomLoad = this.zoomLoad.bind(this);
        this.bounds.zoomMove = this.zoomMove.bind(this);
        this.bounds.zoomClose = this.zoomClose.bind(this);
        this.createAnimationEvents();
        this.parent.apply(this, arguments);
        this.notifyGenerating = false;
        if (!this.options.viewer.dim_lights) {
            this.overlayOpacity = 0.01;
            this.animationOptions.overlay.show.duration = 1;
            this.animationOptions.overlay.hide.duration = 1;
        }
        this.storage.largeImageCache = new Element('img', {
            events: {
                load: this.onLargeImageLoad.bind(this)
            }
        });
    },
    display: function (index, element) {
        Gallery.PerformanceTest.start('initialize-' + this.type);
        if (!this.created) {
            Gallery.PerformanceTest.start('createComponents');
            this.createComponents();
            Gallery.PerformanceTest.end('createComponents');
            this.created = true;
            this.hideLoadingAnimation();
        }
        this.show(index, element);
    },
    attachLoadHandler: function () {},
    showLoadingAnimation: function () {},
    hideLoadingAnimation: function () {},
    closeZoomOnStart: function () {
        if (this.zoomHolder.retrieve('shown')) {
            this.zoomClose();
        }
    },
    createComponents: function () {
        this.parent();
        this.domNode.set('class', 'ig-viewer');
        this.domNode.setStyles({
            'opacity': 0,
            'visibility': 'hidden'
        });
        var tbody = new Element('tbody');
        this.listHolder = new Element('td');
        this.zoomHolder = new Element('div', {
            "class": "ig-viewer-zoom"
        }).hide();
        this.viewerPanel = new Element('div', {
            "class": "ig-viewer-panel"
        });
        this.captionHolder = new Element('div', {
            "class": "ig-viewer-caption"
        });
        this.navigationHolder = new Element('div', {
            "class": "ig-viewer-nav"
        });
        this.closeButton = new Element('div', {
            "class": "ig-viewer-close"
        });
        this.viewerBody = new Element('div', {
            "class": "ig-viewer-waypoint"
        }).inject(this.domNode).grab(new Element('table', {
            "align": "center",
            "class": "ig-struct"
        }).grab(tbody.grab(new Element('tr').grab(this.listHolder)))).grab(this.closeButton);
        if (this.options.viewer.caption_visibility || this.options.viewer.navigation) {
            tbody.grab(new Element('tr').grab(new Element('td', {
                width: "100%"
            }).grab(this.viewerPanel.grab(new Element('table', {
                "class": "ig-struct",
                width: "100%"
            }).grab(new Element('tbody').grab(new Element('tr').grab(new Element('td', {
                width: "100%"
            }).grab(this.captionHolder)).grab(new Element('td').grab(this.navigationHolder))))))))
        }
        this.overlay = new Element('div', {
            'class': 'ig-overlay',
            'styles': {
                'opacity': 0,
                'visibility': 'visible',
                'height': 0,
                'overflow': 'hidden'
            }
        }).inject(document.body);
        this.computeSize();
        this.domNode.setStyles(this.getBoxStyles());
        this.closeButton.addEvent('click', this.hide.bind(this));
        this.overlay.addEvent('click', this.hide.bind(this));
        if (this.options.viewer.caption_visibility) {
            var caption = new Gallery.Components.ComponentCaption({
                plugin: this,
                duration: Gallery.Constants['fade_duration']
            });
            caption.inject(this.captionHolder);
            this.viewerPanel.addClass('ig-viewer-panel-with-caption');
        }
        var list_options = {
            plugin: this,
            thumbnail_size: this.options.visualization.thumbnail_size,
            thumbnail_transition: 'fade',
            slideshow_timeout: this.options.viewer.slideshow_timeout,
            autoplay: this.options.viewer.autoplay,
            thumbnail_effect: false,
            loop: this.options.viewer.loop,
            caption_visibility: false,
            images: this.options.images,
            src_key: 'large_url',
            auto_scale: true,
            notify_generating: this.notifyGenerating,
            full_src: 'full_url',
            pages_to_preload: Gallery.Constants.viewer.pages_to_preload
        };
        this.List = new Gallery.Components.ComponentListSlideshow(list_options);
        this.List.inject(this.listHolder);
        this.tumbnailInstances = this.List.getTumbnailInstances();
        var items_count = this.List.getItemsCount();
        if (items_count > 1) {
            this.pages = this.List.getPages();
            if (this.options.viewer.inside_buttons || this.options.viewer.play_button) {
                var buttons_set_options = {
                    plugin: this,
                    first_item: this.pages[0],
                    last_item: this.pages.getLast(),
                    inside_buttons: this.options.viewer.inside_buttons,
                    play_button: this.options.viewer.play_button,
                    autoplay: this.options.viewer.autoplay,
                    loop: this.options.viewer.loop,
                    events: {
                        buttonPrev: {
                            click: 'onButtonPrev'
                        },
                        buttonNext: {
                            click: 'onButtonNext'
                        },
                        buttonPlay: {
                            click: 'onButtonPlay'
                        },
                        buttonPause: {
                            click: 'onButtonPause'
                        }
                    }
                };
                this.Navigators.ButtonsSet = new Gallery.Components.ComponentButtonsSet(buttons_set_options);
                this.Navigators.ButtonsSet.inject(this.List.getDomNode());
            }
            if (this.options.viewer.navigation) {
                var navigation_options = {
                    plugin: this,
                    pages: this.pages,
                    first_item: this.pages[0],
                    last_item: this.pages.getLast(),
                    type: this.options.viewer.navigation,
                    loop: this.options.viewer.loop
                };
                this.Navigators.Navigation = new Gallery.Components.ComponentNavigation(navigation_options);
                this.Navigators.Navigation.inject(this.navigationHolder);
                this.viewerPanel.addClass('ig-viewer-panel-with-navigation');
            }
        }
        this.inspectStyleOffsets();
        this.setGalleryDimensions();
        this.List.createTransitionList(this.domNode.getElements('.ig-slide'));
        this.zoomHolder.inject(this.listHolder);
        this.bigImage = new Element('img').hide().addEvent('load', this.bounds.zoomLoad).inject(this.zoomHolder);
        if (Browser.Engine.trident) {
            this.bigImage.ondragstart = $lambda(false);
        }
    },
    inspectStyleOffsets: function () {
        this.parent();
        var viewer_panel_height = 0;
        if (this.options.viewer.caption_visibility || this.options.viewer.navigation) {
            var panel_dimensions = this.viewerPanel.getDimensions({
                computeSize: true,
                styles: ['border', 'padding', 'margin']
            });
            viewer_panel_height = panel_dimensions.totalHeight;
        }
        this.setConstant('viewer_panel_height', viewer_panel_height);
    },
    computeSize: function () {
        var computed_styles;
        if (this.options.viewer.size_behaviour == 'static') {
            computed_styles = ['border', 'padding'];
        } else {
            computed_styles = ['border', 'padding', 'margin'];
        }
        var viewer_size = this.domNode.getDimensions({
            computeSize: true,
            styles: computed_styles
        });
        viewer_size.x = viewer_size.computedLeft + viewer_size.computedRight
        viewer_size.y = viewer_size.computedBottom + viewer_size.computedTop;
        this.setConstant('viewer_size', viewer_size);
    },
    setGalleryDimensions: function () {
        var box_dimensions = this.getBoxStyles();
        var width = box_dimensions.width;
        var height = box_dimensions.height - this.getConstant('viewer_panel_height');
        this.storage.canvasSize = {
            width: width - this.getConstant('ig_thumbs_offset_x'),
            height: height - this.getConstant('ig_thumbs_offset_y')
        };
        this.tumbnailInstances.invoke('setCanvasSize', width, height)
        this.setCss('.ig-viewer-zoom, .ig-img td', {
            width: this.storage.canvasSize.width,
            height: this.storage.canvasSize.height
        });
        this.setCss('.ig-slide', {
            width: this.storage.canvasSize.width
        });
    },
    getBoxSize: function () {
        if (this.options.viewer.size_behaviour == 'static') {
            return {
                x: this.options.viewer.width + this.getConstant('viewer_size').x,
                y: this.options.viewer.height + this.getConstant('viewer_size').y
            }
        } else {
            return window.getSize();
        }
    },
    getBoxStyles: function () {
        var size = this.getBoxSize();
        var styles = {
            width: size.x - this.getConstant('viewer_size').x,
            height: size.y - this.getConstant('viewer_size').y
        };
        if (this.options.viewer.size_behaviour == 'static') {
            styles.marginLeft = -(size.x / 2);
            styles.marginTop = -(size.y / 2);
        } else {
            styles.left = 0;
            styles.top = 0;
        }
        return styles;
    },
    view: function (item_index, e) {
        if (!this.options.viewer.zoom) {
            return;
        }
        e = new Event(e);
        this.pause();
        if (!this.drag) {
            this.drag = new Drag(this.bigImage, {
                modifiers: {
                    x: 'left',
                    y: 'top'
                },
                grid: 1,
                onComplete: this.bounds.zoomMove,
                onCancel: this.bounds.zoomClose
            });
        }
        var coordinates = $(e.target).getCoordinates();
        var scroll = window.getScroll();
        this.storage.clickOffsetsRatios = {
            x: (e.event.offsetX ? e.event.offsetX : e.event.pageX - coordinates.left) / e.target.getWidth(),
            y: (e.event.offsetY ? e.event.offsetY : e.event.pageY - coordinates.top) / e.target.getHeight()
        };
        var data = this.getImageData(item_index);
        var big_image_src = data.full_url ? data.full_url : data.large_url;
        this.List.hide();
        this.zoomHolder.addClass('ig-loading').show('block');
        this.bigImage.show('block').set('src', '').set('src', big_image_src);
    },
    onResize: function () {
        var size = window.getSize();
        this.overlay.setStyles({
            width: size.x,
            height: size.y
        });
        var target_box = this.getBoxStyles();
        this.domNode.setStyles(target_box);
        this.setGalleryDimensions();
        this.setZoomRectangle();
        this.List.gotoItem(this.currentIndex, null, true);
    },
    onResizeStatic: function () {
        var size = window.getSize();
        this.overlay.setStyles({
            width: size.x,
            height: size.y
        });
    },
    onKeyDown: function (e) {
        switch (e.key) {
        case 'backspace':
        case 'left':
            this.previous();
            break;
        case 'space':
        case 'right':
            this.next();
            break;
        case 'esc':
            this.hide();
            break;
        default:
            return;
        }
        e.preventDefault();
    },
    createAnimationEvents: function () {
        this.animationOptions.overlay.show.onComplete = this.overlayShowComplete.bindWithEvent(this);
        this.animationOptions.overlay.hide.onComplete = this.overlayHideComplete.bindWithEvent(this);
        this.animationOptions.clone.show.onComplete = this.cloneShowComplete.bindWithEvent(this);
        this.animationOptions.clone.hide.onComplete = this.cloneHideComplete.bindWithEvent(this);
        this.animationOptions.body.show.onComplete = this.bodyShowComplete.bindWithEvent(this);
        this.animationOptions.body.hide.onComplete = this.bodyHideComplete.bindWithEvent(this);
    },
    applyAnimationOptions: function (action) {
        this.overlay.set('tween', {
            duration: this.animationOptions.overlay[action].duration,
            transition: this.animationOptions.overlay[action].transition,
            onComplete: this.animationOptions.overlay[action].onComplete,
            link: 'cancel'
        });
        this.cloneElement.set('morph', {
            duration: this.animationOptions.clone[action].duration,
            transition: this.animationOptions.clone[action].transition,
            onComplete: this.animationOptions.clone[action].onComplete
        });
        this.domNode.set('tween', {
            duration: this.animationOptions.body[action].duration,
            transition: this.animationOptions.body[action].transition,
            onComplete: this.animationOptions.body[action].onComplete,
            link: 'chain'
        });
    },
    cloneShowComplete: function () {
        this.storage.cloneShowComplete = true;
        if (this.storage.largeImageCached) {
            this.startViewerBodyAnimation();
        }
    },
    onLargeImageLoad: function () {
        this.storage.largeImageCached = true;
        if (this.storage.cloneShowComplete) {
            this.startViewerBodyAnimation();
        }
    },
    startViewerBodyAnimation: function () {
        this.storage.largeImageCached = false;
        this.storage.cloneShowComplete = false;
        this.domNode.setStyles({
            opacity: "0",
            visibility: 'visible',
            display: ''
        });
        this.List.gotoItem(this.currentIndex, null, true);
        this.domNode.addClass('ig-viewer-animation');
        this.domNode.tween("opacity", "1");
    },
    bodyShowComplete: function () {
        this.domNode.removeClass('ig-viewer-animation');
        this.storage.largeImageCache.src = '';
        this.cloneElement.dispose();
        this.overlay.tween('opacity', this.overlayOpacity);
        this.fireEvent('show');
    },
    overlayShowComplete: function () {},
    overlayHideComplete: function () {
        this.overlay.setStyles({
            'height': 0,
            'top': ''
        });
        this.domNode.setStyles({
            'opacity': 0,
            'visibility': 'hidden',
            'display': 'none'
        });
        this.zoomHolder.hide();
        this.List.show();
        this.fireEvent('hide');
    },
    bodyHideComplete: function () {},
    cloneHideComplete: function () {},
    show: function (index, element) {
        this.addViewerEvents();
        this.currentIndex = index;
        this.closed = false;
        var size = window.getSize();
        this.overlay.setStyles({
            width: size.x,
            height: size.y
        });
        var position = element.getPosition();
        var clone_styles = {
            position: 'absolute',
            left: position.x,
            top: position.y,
            width: element.width,
            height: element.height
        }
        this.cloneElement = element.clone();
        this.cloneElement.setStyles(clone_styles);
        this.cloneElement.addClass('ig-thumbnail-clone');
        var viewer_styles = this.getBoxStyles();
        viewer_styles.opacity = "0";
        viewer_styles.display = 'block';
        this.domNode.setStyles(viewer_styles);
        this.applyAnimationOptions('show');
        this.setGalleryDimensions();
        var large_image_size = this.tumbnailInstances[index].getImageSize();
        if (isNaN(large_image_size.width) == false && isNaN(large_image_size.height) == false) {
            var container_position = this.List.getDomNode().getPosition();
            var morph_properties = {
                width: large_image_size.width,
                height: large_image_size.height,
                left: container_position.x + ((this.storage.canvasSize.width - large_image_size.width) / 2),
                top: container_position.y + ((this.storage.canvasSize.height - large_image_size.height) / 2)
            };
            var cloned_image_ratio = clone_styles.width / clone_styles.height;
            var large_image_ratio = large_image_size.width / large_image_size.height;
            if (cloned_image_ratio > large_image_ratio) {
                var new_height = large_image_size.width / cloned_image_ratio;
                morph_properties.top += (morph_properties.height - new_height) / 2;
                morph_properties.height = new_height;
            } else if (cloned_image_ratio < large_image_ratio) {
                var new_width = large_image_size.height * cloned_image_ratio;
                morph_properties.left += (morph_properties.width - new_width) / 2;
                morph_properties.width = new_width;
            }
            this.cloneElement.inject(document.body);
            this.cloneElement.morph(morph_properties);
        } else {
            this.cloneShowComplete();
        }
        this.storage.largeImageCache.src = this.getImageData(index).large_url;
    },
    hide: function (page, anchor) {
        this.closed = true;
        this.removeViewerEvents();
        this.pause();
        this.applyAnimationOptions('hide');
        this.overlay.tween('opacity', "0");
    },
    addViewerEvents: function () {
        window.addEvent('resize', this.bounds.viewerResize);
        document.addEvent('keydown', this.bounds.viewerKeydown);
    },
    removeViewerEvents: function () {
        window.removeEvent('resize', this.bounds.viewerResize);
        document.removeEvent('keydown', this.bounds.viewerKeydown);
    },
    zoomLoad: function () {
        this.zoomHolder.store('shown', true);
        this.storage.imageSize = {
            width: this.bigImage.width,
            height: this.bigImage.height
        };
        this.setZoomRectangle();
        this.zoomHolder.removeClass('ig-loading');
    },
    zoomClose: function () {
        this.zoomHolder.store('shown', false);
        this.zoomHolder.hide();
        this.bigImage.setStyles({
            left: '',
            top: ''
        });
        this.List.show('block');
    },
    zoomMove: function (img) {
        this.storage.clickOffsetsRatios = {
            x: (Math.abs(img.getStyle('left').toInt() - (this.storage.canvasSize.width / 2)) / this.storage.imageSize.width),
            y: (Math.abs(img.getStyle('top').toInt() - (this.storage.canvasSize.height / 2)) / this.storage.imageSize.height)
        }
    },
    setZoomRectangle: function () {
        if (!this.zoomHolder.retrieve('shown')) {
            return;
        }
        var left = this.storage.canvasSize.width - this.storage.imageSize.width;
        var top = this.storage.canvasSize.height - this.storage.imageSize.height;
        if (left > 0) {
            left = left / 2;
        }
        if (top > 0) {
            top = top / 2;
        }
        var ranges = {
            x: {
                min: left,
                max: Math.max(0, left)
            },
            y: {
                min: top,
                max: Math.max(0, top)
            }
        };
        this.drag.setOptions({
            limit: {
                x: [ranges.x.min, ranges.x.max],
                y: [ranges.y.min, ranges.y.max]
            }
        });
        var click_offset_x = Math.floor(this.storage.clickOffsetsRatios.x * this.storage.imageSize.width);
        var click_offset_y = Math.floor(this.storage.clickOffsetsRatios.y * this.storage.imageSize.height);
        var position = {
            x: (this.storage.canvasSize.width / 2) - click_offset_x,
            y: (this.storage.canvasSize.height / 2) - click_offset_y
        }
        for (var z in position) {
            if (position[z] > ranges[z].max) {
                position[z] = ranges[z].max;
            } else if (position[z] < ranges[z].min) {
                position[z] = ranges[z].min;
            }
        }
        this.bigImage.setStyles({
            left: position.x,
            top: position.y
        });
    }
});


Gallery.Effects.Reflection = function (image, options) {
    options = $extend({
        opacity: 0.5,
        container_height: 'auto',
        reflection_height: 150
    }, options || {});
    var parent = $(image).getParent();
    var image_width;
    var image_height;
    if (Browser.Engine.trident) {
        image.inject(document.body).set('style', 'visibility:hidden;position:absolute;left:-10000px;top:-10000px');
        image_width = image.width;
        image_height = image.height;
        image.inject(parent).set('style', '-ms-interpolation-mode: bicubic;');
    } else {
        image_width = image.width;
        image_height = image.height;
    }
    if (Browser.Engine.trident) {
        var reflection = new Element('img', {
            src: image.src,
            style: 'filter: flipv progid:DXImageTransform.Microsoft.Alpha(opacity=' + (options.opacity * 100) + ',style=1,finishOpacity=0,startx=0,starty=0,finishx=0,finishy=' + (options.reflection_height / image_height * 100) + '); -ms-interpolation-mode: bicubic;'
        });
    } else {
        var reflection = new Element("canvas");
        if (!reflection.getContext) return;
        try {
            var context = reflection.setProperties({
                width: image_width,
                height: options.reflection_height
            }).getContext("2d");
            context.save();
            context.translate(0, image_height - 1);
            context.scale(1, -1);
            context.drawImage(image, 0, 0, image_width, image_height);
            context.restore();
            context.globalCompositeOperation = "destination-out";
            var gradient = context.createLinearGradient(0, 0, 0, options.reflection_height);
            gradient.addColorStop(0, "rgba(255, 255, 255, " + (1 - options.opacity) + ")");
            gradient.addColorStop(1, "rgba(255, 255, 255, 1.0)");
            context.fillStyle = gradient;
            context.rect(0, 0, image_width, options.reflection_height);
            context.fill();
        } catch (e) {
            return;
        }
    }
    new Element('table', {
        "class": "ig-struct",
        width: "100%"
    }).grab(new Element('tbody').grab(new Element('tr').grab(new Element('td', {
        style: 'height:' + (options.container_height - options.reflection_height) + 'px'
    }).grab(image))).grab(new Element('tr').grab(new Element('td', {
        style: 'vailign:top;height:' + options.reflection_height + 'px'
    }).grab(new Element('div', {
        'style': 'overflow: hidden; height: ' + options.reflection_height + 'px'
    }).grab(reflection))))).inject(parent);
}


Gallery.Components.ComponentBase = new Class({
    domNode: null,
    options: {},
    bounds: {},
    initialize: function (options) {
        for (var key in options) {
            this.options[key] = options[key];
        }
    },
    inject: function (node) {
        node.appendChild(this.domNode);
    },
    appendChild: function (node) {
        this.domNode.appendChild(node);
    },
    getDomNode: function () {
        return $(this.domNode);
    },
    hide: function () {
        return this.domNode.style.display = 'none';
    },
    show: function () {
        return this.domNode.style.display = 'block'
    },
    dispatchEvent: function () {
        this.options.plugin.fireEvent.apply(this.options.plugin, arguments);
    },
    addEvent: function () {
        this.options.plugin.addEvent.apply(this.options.plugin, arguments);
    },
    addEvents: function () {
        this.options.plugin.addEvents.apply(this.options.plugin, arguments);
    },
    removeEvent: function () {
        this.options.plugin.removeEvent.apply(this.options.plugin, arguments);
    },
    setConstant: function () {
        return this.options.plugin.setConstant.apply(this.options.plugin, arguments);
    },
    getConstant: function () {
        return this.options.plugin.getConstant.apply(this.options.plugin, arguments);
    }
});


Gallery.Components.ComponentButton = new Class({
    Extends: Gallery.Components.ComponentBase,
    domNode: null,
    options: {
        type: '',
        className: null,
        disabledClassName: 'ig-disabled',
        events: {
            mousedown: null,
            mouseout: null,
            mouseover: null,
            click: null,
            enable: null,
            disable: null
        },
        events_arguments: {
            mousedown: [],
            mouseout: [],
            mouseover: [],
            click: [],
            enable: [],
            disable: []
        },
        defaultEnabled: false
    },
    className: '',
    bounds: {
        events: {}
    },
    isDisabled: null,
    plugin: {},
    initialize: function (options) {
        this.parent.apply(this, arguments);
        this.className = this.options.className ? this.options.className : 'ig-btn ig-' + this.options.type;
        this.domNode = new Element('div', {
            'class': this.className
        });
        this.bounds.events["mousedown"] = this.onMousedown.bind(this);
        this.bounds.events["mouseout"] = this.onMouseout.bind(this);
        this.bounds.events["mouseover"] = this.onMouseover.bind(this);
        this.bounds.events["click"] = this.onClick.bind(this);
        if (this.options.defaultEnabled) {
            this.enable();
        }
    },
    onMousedown: function () {
        this.setState('click');
        this.dispatchEvent('mousedown');
    },
    onMouseout: function () {
        this.setState('normal');
        this.dispatchEvent('mouseout');
    },
    onMouseover: function () {
        this.setState('over');
        this.dispatchEvent('mouseover');
    },
    onClick: function (e) {
        this.dispatchEvent('click');
    },
    setState: function (state) {
        this.state = state;
        this.domNode.set('class', this.className);
        if (state != 'normal') {
            this.domNode.addClass('ig-' + state);
        }
    },
    disable: function () {
        if (this.isDisabled === true) {
            return;
        }
        this.isDisabled = true;
        this.domNode.set('class', this.className + ' ' + this.options.disabledClassName);
        this.domNode.removeEvents(this.bounds.events);
        this.dispatchEvent('disable');
    },
    enable: function () {
        if (this.isDisabled === false) {
            return;
        }
        this.isDisabled = false;
        this.domNode.removeClass(this.options.disabledClassName);
        this.domNode.addEvents(this.bounds.events);
        this.dispatchEvent('enable');
    },
    dispatchEvent: function (type) {
        if ($defined(this.options.events[type])) {
            this.parent(this.options.events[type], this.options.events_arguments[type]);
        }
    }
});


Gallery.Components.ComponentButtonsSet = new Class({
    Extends: Gallery.Components.ComponentBase,
    domNode: null,
    options: {
        first_item: 0,
        last_item: 0,
        events: {
            buttonPrev: {},
            buttonNext: {},
            buttonPlay: {},
            buttonPause: {}
        }
    },
    buttons: {},
    bounds: {
        events: {}
    },
    currentIndex: null,
    initialize: function (options) {
        this.parent.apply(this, arguments);
        this.domNode = new Element('div');
        if (this.options.inside_buttons) {
            this.buttons.buttonPrev = new Gallery.Components.ComponentButton({
                plugin: this.options.plugin,
                type: 'prev',
                events: this.options.events.buttonPrev
            });
            this.buttons.buttonNext = new Gallery.Components.ComponentButton({
                plugin: this.options.plugin,
                type: 'next',
                events: this.options.events.buttonNext
            });
        }
        if (this.options.play_button) {
            this.buttons.buttonPlay = new Gallery.Components.ComponentButton({
                plugin: this.options.plugin,
                type: 'play',
                events: this.options.events.buttonPlay,
                defaultEnabled: true
            });
            this.buttons.buttonPause = new Gallery.Components.ComponentButton({
                plugin: this.options.plugin,
                type: 'pause',
                events: this.options.events.buttonPause,
                defaultEnabled: true
            });
            this.buttons.buttonPause.hide();
            this.addEvents({
                listPause: function () {
                    this.buttons.buttonPause.hide();
                    this.buttons.buttonPlay.show();
                }.bind(this),
                listPlay: function () {
                    this.buttons.buttonPlay.hide();
                    this.buttons.buttonPause.show();
                }.bind(this)
            });
        }
    },
    getCurrentIndex: function () {
        return this.currentItemIndex;
    },
    syncNavigation: function (index) {
        if (index == this.currentIndex) {
            return;
        }
        if (this.options.inside_buttons) {
            if (this.options.loop) {
                this.buttons.buttonPrev.enable();
                this.buttons.buttonNext.enable();
            } else if (index == this.options.first_item) {
                this.buttons.buttonPrev.disable();
                this.buttons.buttonNext.enable();
            } else if (index == this.options.last_item) {
                this.buttons.buttonPrev.enable();
                this.buttons.buttonNext.disable();
            } else {
                this.buttons.buttonPrev.enable();
                this.buttons.buttonNext.enable();
            }
        }
        this.currentIndex = index;
    },
    inject: function (element) {
        for (var button_name in this.buttons) {
            this.buttons[button_name].inject(element);
        }
    },
    getComponentsButtons: function () {
        if (arguments.length > 0) {
            var buttons = {};
            for (var i = 0; i < arguments.length; i++) {
                var button_name = arguments[i];
                if (this.buttons[button_name]) {
                    buttons[button_name] = this.buttons[button_name];
                }
            }
            return buttons;
        } else {
            return this.buttons;
        }
    }
});


Gallery.Components.ComponentCaption = new Class({
    Extends: Gallery.Components.ComponentBase,
    options: {
        duration: 'normal'
    },
    initialize: function (options) {
        this.parent.apply(this, arguments);
        this.captionTextHolder = new Element('p');
        this.domNode = new Element('div', {
            "class": "ig-caption"
        }).grab(this.captionTextHolder);
        if (Gallery.Utils.stringToBool(this.options.duration)) {
            this.captionTextHolder.set('tween', {
                duration: this.options.duration / 2,
                link: 'chain',
                transition: 'quart:in:out',
                onComplete: function () {
                    if (this.captionTextHolder.getStyle('opacity') == 0) {
                        this.captionTextHolder.set('html', this.captionText);
                    }
                }.bindWithEvent(this)
            });
        }
        this.addEvent('listStart', this.setCaption.bind(this));
    },
    setCaption: function (index) {
        this.captionText = this.options.plugin.getImageCaption(index);
        if (Gallery.Utils.stringToBool(this.options.duration)) {
            this.captionTextHolder.tween('opacity', '0');
            this.captionTextHolder.tween('opacity', '1');
        } else {
            this.captionTextHolder.set('opacity', '1');
            this.captionTextHolder.set('html', this.captionText);
        }
    }
});


Gallery.Components.ComponentListBase = new Class({
    Extends: Gallery.Components.ComponentBase,
    currentIndex: null,
    itemsCount: 1,
    itemsPerPage: 0,
    options: {
        pages_to_preload: 5
    },
    pages: [],
    notLoadedPages: [],
    componentsPerPage: {},
    tumbnailInstances: [],
    transitionList: {
        set: $lambda(),
        play: $lambda(),
        stop: $lambda(),
        walk: $lambda()
    },
    play: function () {
        this.transitionList.play(true);
    },
    pause: function () {
        this.transitionList.stop();
    },
    gotoItem: function (index, direction, skip_animation) {
        if (index == this.currentIndex) {
            if (skip_animation) {
                this.transitionList.set(index);
            }
            return;
        }
        var prev_item_index = this.currentIndex;
        this.currentIndex = index;
        if (!$defined(direction)) {
            direction = (index > prev_item_index || index == this.pages.getLast()) ? 'next' : 'previous';
        }
        this.preloadPages();
        var transition_list_index = this.pages.indexOf(this.currentIndex);
        if (skip_animation) {
            this.onListStart(transition_list_index, prev_item_index);
            this.transitionList.set(transition_list_index);
            this.onListComplete(transition_list_index);
        } else {
            this.transitionList.walk(transition_list_index, true, direction);
        }
    },
    getCurrentIndex: function () {
        return this.currentIndex;
    },
    getItemsCount: function () {
        return this.itemsCount;
    },
    getPageComponents: function (page) {
        return $splat(this.componentsPerPage['page_' + page]);
    },
    setPageComponent: function (page, component) {
        return this.componentsPerPage['page_' + page] = component;
    },
    preloadPages: function (index) {
        if (this.notLoadedPages.length == 0) {
            return;
        }
        this.dispatchEvent('beforePreload');
        index = this.pages.indexOf($pick(index, this.currentIndex, this.pages[0]));
        var scope = [];
        var slice_arguments = [];
        var pages_length = this.pages.length - 1;
        var start_offset = index - this.options.pages_to_preload;
        var end_offset = index + this.options.pages_to_preload + 1;
        var total_pages = (this.options.pages_to_preload * 2) + 1;
        if (total_pages >= pages_length) {
            slice_arguments.push([0]);
        } else if (start_offset >= 0 && end_offset <= pages_length) {
            slice_arguments.push([start_offset, end_offset]);
        } else if (end_offset >= pages_length) {
            slice_arguments.push([start_offset]);
            slice_arguments.push([0, end_offset - 1 - pages_length]);
        } else if (start_offset < 0) {
            slice_arguments.push([start_offset]);
            slice_arguments.push([0, end_offset]);
        }
        for (var i = 0; i < slice_arguments.length; i++) {
            scope = scope.concat(this.pages.slice.apply(this.pages, slice_arguments[i]))
        }
        var pages = this.notLoadedPages.intersect(scope);
        this.loadedPages(pages);
    },
    loadedPages: function (pages) {
        if (pages.length == 0) {
            return;
        }
        for (var i = 0; i < pages.length; i++) {
            this.getPageComponents(pages[i]).invoke('loadContent');
        }
        this.notLoadedPages = this.notLoadedPages.differentiate(pages);
        this.dispatchEvent('afterPreload');
    },
    getPages: function () {
        return this.pages;
    },
    calulatePrevNextItemIndex: function (direction) {
        var index = this.pages.indexOf($pick(this.currentIndex, this.pages[0]));
        var item_index;
        if (direction == 'previous' && index == 0 && this.options.loop) {
            item_index = this.pages.getLast();
        } else if (direction == 'next' && index == this.pages.getLast() && this.options.loop) {
            item_index = this.pages[0];
        } else {
            item_index = this.pages[index + (direction == 'previous' ? -1 : 1)];
        }
        return item_index;
    },
    gotoNext: function () {
        var index = this.calulatePrevNextItemIndex('next');
        if ($type(index) == 'number') {
            this.gotoItem(index, 'next');
        }
    },
    gotoPrevious: function () {
        var index = this.calulatePrevNextItemIndex('previous');
        if ($type(index) == 'number') {
            this.gotoItem(index, 'previous');
        }
    },
    createTransitionList: function (elements, transition_list_options) {
        var options = $extend({
            type: this.options.thumbnail_transition,
            interval: (this.options.slideshow_timeout || 5) * 1000,
            endlessChain: (this.options.loop && this.options.thumbnail_transition == 'slide' && this.itemsCount > 1),
            onBeforeStart: this.onListBeforeStart.bind(this),
            onStart: this.onListStart.bind(this),
            onComplete: this.onListComplete.bind(this),
            onAfterComplete: this.onListAfterComplete.bind(this),
            onPlayStop: this.dispatchEvent.bind(this, ['listPause']),
            onPlayStart: this.dispatchEvent.bind(this, ['listPlay'])
        }, transition_list_options);
        this.transitionList = new Gallery.Effects.TransitionList(elements, options);
    },
    onListBeforeStart: function (new_index, old_index) {
        this.dispatchEvent('listBeforeStart', [new_index, old_index]);
    },
    onListStart: function (new_index, old_index) {
        this.dispatchEvent('listStart', [new_index, old_index]);
    },
    onListComplete: function (index) {
        index = this.pages[index];
        if (this.transitionList.playOn) {
            this.currentIndex = index;
        }
        this.options.plugin.syncNavigation(index);
        this.preloadPages(index);
        this.dispatchEvent('listComplete', [index])
    },
    onListAfterComplete: function (index) {
        this.dispatchEvent('listAfterComplete', [index]);
    },
    getArrangementPlaceholder: function (index) {
        if (index == 0) {
            return this.options.first_placeholder;
        } else {
            return this.options.images[index - 1].arrangement_placeholder;
        }
    },
    getTumbnailInstances: function () {
        if (this.tumbnailInstances.length == 0) {
            for (var page_id in this.componentsPerPage) {
                this.tumbnailInstances = this.tumbnailInstances.concat($splat(this.componentsPerPage[page_id]).invoke('getTumbnailInstance'));
            }
        }
        return this.tumbnailInstances;
    }
});


Gallery.Components.ComponentListSlider = new Class({
    Extends: Gallery.Components.ComponentListBase,
    options: {
        src_key: 'thumb_url',
        auto_scale: false,
        columns: 1,
        rows: 1,
        autoplay: false,
        loop: false
    },
    isFirstLoad: true,
    pageOffsets: [],
    timer: null,
    scrollTimeOut: 250,
    playOn: false,
    currentOffset: null,
    initialize: function (options) {
        this.parent.apply(this, arguments);
        this.itemsCount = this.options.images.length;
        this.domNode = new Element('div', {
            'class': 'ig-thumbs'
        });
        this.listContainer = new Element('div', {
            "class": "ig-mask"
        }).inject(this.domNode);
        this.listStripholder = new Element('tr');
        new Element('table', {
            "align": "center",
            "class": "ig-struct"
        }).grab(new Element('tbody').grab(this.listStripholder)).inject(this.listContainer).addEvents({
            mouseenter: this.dispatchEvent.bind(this, ['listMouseover']),
            mouseleave: this.dispatchEvent.bind(this, ['listMouseout'])
        });
        for (var i = 0; i < this.itemsCount; i++) {
            this.pages.push(i);
            var list_item_options = this.createOptionsSet(i);
            var list_item = new Gallery.Components.ComponentListItem(list_item_options);
            this.setPageComponent(i, list_item);
            list_item.inject(new Element('td', {
                "class": "ig-slide-td"
            }).inject(this.listStripholder));
        }
        this.notLoadedPages = this.pages;
    },
    setDimensions: function () {
        var images_count = this.options.images.length;
        for (var page = 0; page < images_count; page++) {
            var data = this.options.images[page];
            if ($type(data.thumb_size) == 'object' && !isNaN(parseInt(data.thumb_size.width) * parseInt(data.thumb_size.height))) {
                var list_item = this.componentsPerPage['page_' + page].getDomNode();
                this.setItemDimensions(list_item, data.thumb_size)
            }
        }
    },
    setItemDimensions: function (element, thumb_size) {
        if (this.options.thumbnail_effect == 'reflection') {
            element.setStyles({
                width: this.getConstant('img_offset_x') + this.getConstant('ig_img_offset_x') + thumb_size.width,
                height: this.getConstant('image_height')
            });
            if (this.options.caption_visibility) {
                element.getElement('.ig-caption').setStyle('width', thumb_size.width);
            }
        } else {
            element.setStyles({
                width: this.getConstant('img_offset_x') + this.getConstant('ig_img_offset_x') + thumb_size.width
            });
        }
        element.getElement('.ig-img td').setStyles({
            width: this.getConstant('img_offset_x') + thumb_size.width,
            height: this.getConstant('img_height')
        });
    },
    getListScrollWidth: function () {
        return this.pageOffsets.getLast();
    },
    getContainer: function () {
        return this.listContainer;
    },
    inject: function () {
        var image_holders = this.domNode.getElements('.ig-thumb').addClass('ig-slide');
        this.parent.apply(this, arguments);
    },
    createOptionsSet: function (item_index) {
        var options = {
            plugin: this.options.plugin,
            thumbnail_size: this.options.thumbnail_size,
            thumbnail_effect: this.options.thumbnail_effect,
            thumbnail_transition: this.options.thumbnail_transition,
            caption_visibility: this.options.caption_visibility,
            item_index: item_index,
            image_data: this.options.images[item_index],
            src_key: this.options.src_key,
            auto_scale: this.options.auto_scale,
            notify_generating: this.options.notify_generating
        };
        if (this.options.create_placeholders) {
            options.arrangement_placeholder_left = document.createElement('div');
            options.arrangement_placeholder_left.className = "ig-placeholder ig-placeholder-left";
            options.arrangement_placeholder_left.innerHTML = this.getArrangementPlaceholder(item_index);
            options.arrangement_placeholder_right = document.createElement('div');
            options.arrangement_placeholder_right.className = "ig-placeholder ig-placeholder-right";
            if ((item_index + 1) == this.itemsCount) {
                options.arrangement_placeholder_right.innerHTML = this.getArrangementPlaceholder(this.itemsCount);
            }
        }
        return options;
    },
    gotoOffset: function (offset) {
        $clear(this.timer);
        if (this.isFirstLoad && this.options.autoplay && (this.listContainer.scrollWidth - this.listContainer.getWidth()) > 1) {
            this.options.plugin.play();
        }
        this.isFirstLoad = false;
        this.listContainer.scrollTo(offset, 0);
        this.timer = this.setCurrentIndex.delay(this.scrollTimeOut, this, [offset]);
    },
    setCurrentIndex: function (offset) {
        if ($defined(this.currentOffset)) {
            var left = offset - this.options.thumbnail_size.width;
            var right = offset + this.options.thumbnail_size.width;
            if (this.currentOffset > left && this.currentOffset < right) {
                return;
            }
        }
        var index = this.pageOffsets.searchBinary(offset, true);
        if (index == this.currentIndex) {
            return;
        }
        this.currentIndex = index;
        this.currentOffset = offset;
        this.preloadPages(this.currentIndex);
    },
    preloadPages: function (index) {
        if (this.notLoadedPages.length == 0) {
            return;
        }
        this.dispatchEvent('beforePreload');
        index = this.pages.indexOf($pick(index, this.currentIndex, this.pages[0]));
        var scope = []
        var slice_arguments = []
        var page_offset = this.pageOffsets[index];
        var page_width = this.options.thumbnail_size.width;
        var pages_width = page_width * this.options.pages_to_preload;
        var pages_to_preload = page_width * this.options.pages_to_preload;
        var start_offset = page_offset - (page_width * this.options.pages_to_preload);
        var end_offset = page_offset + (page_width * this.options.pages_to_preload);
        var total_width = this.pageOffsets.getLast() - page_width;
        if (pages_width >= total_width) {
            slice_arguments.push([0]);
        } else if (start_offset >= 0 && end_offset <= total_width) {
            var array_start_oofset = this.pageOffsets.searchBinary(start_offset, true);
            var array_end_offset = this.pageOffsets.searchBinary(end_offset, true);
            slice_arguments.push([array_start_oofset, array_end_offset]);
        } else if (end_offset >= total_width) {
            end_offset = end_offset - total_width - page_width;
            var array_start_oofset = this.pageOffsets.searchBinary(start_offset, true);
            var array_end_offset = this.pageOffsets.searchBinary(end_offset, true);
            slice_arguments.push([array_start_oofset]);
            slice_arguments.push([0, array_end_offset]);
        } else if (start_offset < 0) {
            start_offset = total_width + start_offset;
            var array_start_oofset = this.pageOffsets.searchBinary(start_offset, true);
            var array_end_offset = this.pageOffsets.searchBinary(end_offset, true);
            slice_arguments.push([array_start_oofset]);
            slice_arguments.push([0, array_end_offset]);
        }
        for (var i = 0; i < slice_arguments.length; i++) {
            scope = scope.concat(this.pages.slice.apply(this.pages, slice_arguments[i]))
        }
        var pages = this.notLoadedPages.intersect(scope);
        this.loadedPages(pages);
    },
    gotoItem: function (index, force_direction, skip_animation) {},
    gotoNext: function (speed_factor) {
        this.startScroll('next', speed_factor);
    },
    gotoPrevious: function (speed_factor) {
        this.startScroll('previous', speed_factor);
    },
    play: function () {
        this.dispatchEvent('listPlay');
        this.playOn = true;
        this.startScroll();
    },
    pause: function () {
        this.dispatchEvent('listPause');
        this.playOn = false;
        this.stop();
    },
    stop: function () {
        $clear(this.intervalId);
    },
    startScroll: function (direction, speed_factor) {
        this.stop();
        direction = $pick(direction, 'next');
        speed_factor = $pick(speed_factor, 1);
        var pixels_per_second = Gallery.Constants.slider.pixels_per_second;
        var frames_per_second = Gallery.Constants.slider.frames_per_second;
        var delta = (pixels_per_second / frames_per_second) * speed_factor;
        var interval = 1000 / frames_per_second;
        this.intervalId = this.scrollAnimation.periodical(interval, this, [direction, delta]);
    },
    rewind: function () {
        this.listContainerScroll.toLeft();
    },
    scrollAnimation: function (direction, delta) {
        var current = this.listContainer.scrollLeft;
        var max_scroll_left = this.listContainer.scrollWidth - this.listContainer.getWidth();
        var scroll_left = 0;
        if (direction == 'previous') {
            scroll_left = Math.max(0, current - delta);
        } else {
            scroll_left = Math.min(max_scroll_left, current + delta);
        }
        if (this.playOn && current == max_scroll_left) {
            this.options.plugin.pause();
            this.setCurrentIndex(0);
            this.options.plugin.syncNavigation(0);
            this.rewind();
            return;
        } else {
            if (this.options.loop && max_scroll_left > 1) {
                if (scroll_left == 0 && direction == 'previous') {
                    scroll_left = this.getListScrollWidth();
                } else if (scroll_left == max_scroll_left && direction == 'next') {
                    scroll_left = this.pageOffsets[this.extraItemIndex] - this.listContainer.getWidth();
                    if (scroll_left == max_scroll_left) {
                        scroll_left = 0;
                    }
                }
            }
            this.listContainer.scrollLeft = scroll_left;
            this.onListComplete();
        }
    },
    onListComplete: function () {
        this.setCurrentIndex(this.listContainer.scrollLeft);
        this.options.plugin.syncNavigation(this.listContainer.scrollLeft);
    },
    createTransitionList: function (elements) {
        for (var i = 0; i < elements.length; i++) {
            var element_right_padding = Gallery.Utils.Element.getComputedSizeSum(elements[i], ['padding-right']);
            var element_right_offsets = elements[i].getPosition(this.listContainer).x + elements[i].getWidth() + element_right_padding;
            this.pageOffsets.push(element_right_offsets);
        }
        if (this.options.loop) {
            this.extraItemIndex = this.pageOffsets.searchBinary(this.options.thumbnail_size.width, true);
            if (this.extraItemIndex < this.itemsCount - 1 || this.itemsCount == 1 && this.extraItemIndex == 0) {
                for (var i = 0; i <= this.extraItemIndex; i++) {
                    var list_item_options = this.createOptionsSet(i);
                    var item = new Gallery.Components.ComponentListItem(list_item_options);
                    item.inject(new Element('td', {
                        "class": "ig-slide-td"
                    }).inject(this.listStripholder));
                    item.loadContent();
                    var element = item.getDomNode();
                    element.getElements('*[id*=]').each(function (element) {
                        element.id += '_copy';
                    });
                    element.addClass('ig-slide');
                    var thumb_size = list_item_options.image_data.thumb_size;
                    if ($type(thumb_size) == 'object' && !isNaN(parseInt(thumb_size.width) * parseInt(thumb_size.height))) {
                        this.setItemDimensions(element, thumb_size);
                    }
                }
            }
        }
        this.listContainerScroll = new Fx.Scroll(this.listContainer, {
            wheelStops: false,
            duration: 'long',
            onComplete: this.onListComplete.bind(this)
        });
    }
});


Gallery.Components.ComponentListSlideshow = new Class({
    Extends: Gallery.Components.ComponentListBase,
    options: {
        src_key: 'thumb_url',
        auto_scale: false,
        columns: 1,
        rows: 1,
        autoplay: false,
        loop: false
    },
    isFirstLoad: true,
    createOptionsSet: function (item_index) {
        var options = {
            plugin: this.options.plugin,
            thumbnail_size: this.options.thumbnail_size,
            thumbnail_effect: this.options.thumbnail_effect,
            thumbnail_transition: this.options.thumbnail_transition,
            caption_visibility: this.options.caption_visibility,
            item_index: item_index,
            image_data: this.options.images[item_index],
            src_key: this.options.src_key,
            auto_scale: this.options.auto_scale,
            notify_generating: this.options.notify_generating
        };
        return options;
    },
    gotoItem: function (index, force_direction, skip_animation) {
        this.parent.apply(this, arguments);
        if (this.isFirstLoad && this.itemsCount > 1 && this.options.autoplay) {
            this.options.plugin.play();
        }
        this.isFirstLoad = false;
    },
    onListStart: function (new_index, old_index) {
        this.parent.apply(this, arguments);
        if (!this.options.loop && new_index == this.pages[0] && this.pages.getLast() == old_index) {
            this.options.plugin.pause();
        }
    },
    initialize: function (options) {
        this.parent.apply(this, arguments);
        this.itemsCount = this.options.images.length;
        this.domNode = new Element('div', {
            'class': 'ig-thumbs'
        });
        var use_endless_chain = (this.options.loop && this.options.thumbnail_transition == 'slide' && this.itemsCount > 1);
        for (var i = 0; i < this.itemsCount; i++) {
            this.pages.push(i);
            var list_item_options = this.createOptionsSet(i);
            if (this.options.create_placeholders) {
                list_item_options.arrangement_placeholder_left = document.createElement('div');
                list_item_options.arrangement_placeholder_left.className = "ig-placeholder ig-placeholder-left";
                list_item_options.arrangement_placeholder_left.innerHTML = this.getArrangementPlaceholder(i);
                list_item_options.arrangement_placeholder_right = document.createElement('div');
                list_item_options.arrangement_placeholder_right.className = "ig-placeholder ig-placeholder-right";
                if ((i + 1) == this.itemsCount) {
                    list_item_options.arrangement_placeholder_right.innerHTML = this.getArrangementPlaceholder(this.itemsCount);
                }
            }
            var list_item = new Gallery.Components.ComponentListItem(list_item_options);
            this.setPageComponent(i, list_item);
            list_item.inject(this.domNode);
        }
        if (use_endless_chain) {
            var first_item_copy = new Gallery.Components.ComponentListItem(this.createOptionsSet(0));
            first_item_copy.inject(this.domNode);
            first_item_copy.loadContent();
        }
        this.notLoadedPages = this.pages;
    },
    inject: function () {
        var image_holders = this.domNode.getElements('.ig-thumb').addClass('ig-slide');
        this.parent.apply(this, arguments);
    }
});


Gallery.Components.ComponentListItem = new Class({
    Extends: Gallery.Components.ComponentBase,
    domNode: null,
    options: {
        auto_scale: false,
        image_data: {
            image_attributes: {},
            caption: ''
        },
        caption_visibility: false,
        force_caption_text: false,
        arrangement_placeholder_left: false,
        arrangement_placeholder_right: false
    },
    initialize: function (options) {
        this.parent.apply(this, arguments);
        this.processImageAttributes();
        this.domNode = new Element('div', this.options.image_data.image_attributes);
        this.domNode.className = "ig-thumb";
        if (this.options.arrangement_placeholder_left) {
            this.domNode.appendChild(this.options.arrangement_placeholder_left);
        }
        if (this.options.arrangement_placeholder_right) {
            this.domNode.appendChild(this.options.arrangement_placeholder_right);
        }
        this.thumbnail = new Gallery.Components.ComponentThumbnail(this.options);
        this.thumbnail.inject(this.domNode);
        if (this.options.caption_visibility) {
            var caption = this.options.image_data.caption;
            if ((!$defined(caption) || caption.length == 0) && this.options.force_caption_text) {
                caption = '&nbsp;';
            }
            new Element('div', {
                "class": "ig-caption"
            }).grab(new Element('p', {
                "class": this.options.capption_text_class,
                "html": caption
            })).inject(this.domNode);
        }
    },
    inject: function () {
        this.parent.apply(this, arguments);
    },
    loadContent: function () {
        this.thumbnail.loadContent();
    },
    processImageAttributes: function () {
        if (!$defined(this.options.image_data.image_attributes)) {
            this.options.image_data.image_attributes = {};
        }
        if (!$defined(this.options.image_data.image_attributes.id)) {
            var id = this.options.image_data.id ? this.options.image_data.id : ('e' + (Native.UID++));
            this.options.image_data.image_attributes.id = id;
        }
    },
    getTumbnailInstance: function () {
        return this.thumbnail;
    }
});


Gallery.Components.ComponentNavigation = new Class({
    Extends: Gallery.Components.ComponentBase,
    domNode: null,
    options: {
        first_item: 0,
        last_item: 0
    },
    plugin: {},
    currentIndex: null,
    buttons: {},
    cache: {
        pages: {},
        selectedElement: null
    },
    initialize: function (options) {
        this.parent.apply(this, arguments);
        this.domNode = new Element('div', {
            'class': 'ig-nav'
        });
    },
    inject: function (node) {
        this.parent.apply(this, arguments);
        switch (this.options.type) {
        case 'numbers':
            this.createNumbers();
            break;
        case 'buttons':
            this.createButtons();
            break;
        case 'buttons_and_numbers':
            this.createButtonsAndNumbers();
            break;
        case 'dots':
            this.createDots();
            break;
        }
    },
    syncNavigation: function (index) {
        if (index == this.currentIndex) {
            return;
        }
        var element = this.domNode.getElement('*[item_index=' + index + ']');
        if (element && element.get('state') != 'selected') {
            this.setStateSelected(element, true);
        }
        if ($defined(this.buttons.buttonPrev) && $defined(this.buttons.buttonNext)) {
            this.setButtonsState(index);
        }
        this.currentIndex = index;
    },
    getCurrentIndex: function () {
        return this.currentIndex;
    },
    addElementEvents: function (element) {
        var element_events = {
            click: this.setStateSelected.bind(this, [element]),
            mousedown: this.setStateClicked.bind(this, [element]),
            mouseover: this.setStateRollover.bind(this, [element]),
            mouseout: this.setStateNormal.bind(this, [element])
        }
        element.addEvents(element_events);
        element.store('element_events', element_events);
    },
    attachElementEvents: function (element) {
        var element_events = element.retrieve('element_events');
        element.addEvents(element_events);
    },
    detachElementEvents: function (element) {
        var element_events = element.retrieve('element_events');
        element.removeEvents(element_events);
    },
    setElementStateSelected: function (element) {
        element.set('state', 'selected');
        element.addClass('ig-selected');
    },
    setStateSelected: function (element, skipGotoItem) {
        if (this.cache.selectedElement) {
            if (this.cache.selectedElement == element) {
                return;
            }
            this.attachElementEvents(this.cache.selectedElement);
            this.setStateNormal(this.cache.selectedElement);
        }
        this.cache.selectedElement = element;
        this.setElementStateSelected(element);
        this.detachElementEvents(element);
        if (element.get('item_index') && !skipGotoItem) {
            var index = element.get('item_index').toInt();
            this.dispatchEvent('gotoItem', [index]);
        }
    },
    setStateClicked: function (element) {
        element.set('state', 'clicked');
        element.addClass('ig-click');
    },
    setStateRollover: function (element) {
        element.set('state', 'rollover');
        element.addClass('ig-over');
    },
    setStateNormal: function (element) {
        element.set('state', 'normal');
        element.removeClass('ig-over');
        element.removeClass('ig-click');
        element.removeClass('ig-selected');
    },
    setButtonsState: function (index) {
        if (this.options.loop) {
            this.buttons.buttonPrev.enable();
            this.buttons.buttonNext.enable();
        } else if (index == this.options.first_item) {
            this.buttons.buttonPrev.disable();
            this.buttons.buttonNext.enable();
        } else if (index == this.options.last_item) {
            this.buttons.buttonPrev.enable();
            this.buttons.buttonNext.disable();
        } else {
            this.buttons.buttonPrev.enable();
            this.buttons.buttonNext.enable();
        }
    },
    createNumbers: function () {
        this.domNode.addClass('ig-numbers');
        for (var i = 0; i < this.options.pages.length; i++) {
            var node = new Element('div', {
                'class': 'ig-number',
                item_index: this.options.pages[i]
            }).grab(new Element('p', {
                text: i + 1
            })).inject(this.domNode);
            this.addElementEvents(node);
        }
    },
    createButtons: function () {
        this.domNode.addClass('ig-buttons');
        var holder = new Element('tr');
        this.domNode.grab(new Element('table', {
            "align": "center",
            "class": "ig-struct"
        }).grab(new Element('tbody').grab(holder)));
        this.buttons.buttonPrev = new Gallery.Components.ComponentButton({
            plugin: this.options.plugin,
            type: 'prev',
            events: {
                click: 'onButtonPrev'
            },
            className: 'ig-btn ig-prev'
        });
        this.buttons.buttonPrev.inject(new Element('td').inject(holder));
        this.buttons.buttonNext = new Gallery.Components.ComponentButton({
            plugin: this.options.plugin,
            type: 'next',
            events: {
                click: 'onButtonNext'
            },
            className: 'ig-btn ig-next'
        });
        this.buttons.buttonNext.inject(new Element('td').inject(holder));
    },
    createButtonsAndNumbers: function () {
        this.domNode.addClass('ig-buttons-nums');
        var holder = new Element('tr');
        this.domNode.grab(new Element('table', {
            "align": "center",
            "class": "ig-struct"
        }).grab(new Element('tbody').grab(holder)));
        this.buttons.buttonPrev = new Gallery.Components.ComponentButton({
            plugin: this.options.plugin,
            type: 'prev',
            events: {
                click: 'onButtonPrev'
            },
            className: 'ig-btn ig-prev'
        });
        this.buttons.buttonPrev.inject(new Element('td').inject(holder));
        var number = new Element('div', {
            'class': 'ig-num ig-num1',
            'text': '1'
        });
        new Element('td').inject(holder).grab(number);
        new Element('td').inject(holder).grab(new Element('div', {
            'class': 'ig-num ig-delim',
            'text': '/'
        }));
        new Element('td').inject(holder).grab(new Element('div', {
            'class': 'ig-num ig-num2',
            'text': this.options.pages.length
        }));
        this.buttons.buttonNext = new Gallery.Components.ComponentButton({
            plugin: this.options.plugin,
            type: 'next',
            events: {
                click: 'onButtonNext'
            },
            className: 'ig-btn ig-next'
        });
        this.buttons.buttonNext.inject(new Element('td').inject(holder));
        this.addEvent('listComplete', function (item_index) {
            this.element.set('text', this.pages.indexOf(item_index) + 1)
        }.bind({
            element: number,
            pages: this.options.pages
        }));
    },
    createDots: function () {
        this.domNode.addClass('ig-dots');
        for (var i = 0; i < this.options.pages.length; i++) {
            var node = new Element('div', {
                'class': 'ig-dot',
                item_index: this.options.pages[i]
            }).inject(this.domNode);
            this.addElementEvents(node);
        }
    }
});


Gallery.Components.ComponentScrollbar = new Class({
    Extends: Gallery.Components.ComponentBase,
    options: {
        mode: 'horizontal',
        scrollWidth: 0,
        contentWidth: 0,
        frameWidth: 0,
        onChange: null,
        buttonsStep: 10,
        handleControls: false,
        events: {
            buttonPrev: {},
            buttonNext: {},
            buttonHandle: {},
            change: null
        }
    },
    cancelBubble: false,
    buttons: {},
    scrollElements: {},
    styleOffsets: {},
    axis: null,
    widthProperty: null,
    scrollInterval: null,
    step: 0,
    initialize: function (options) {
        this.parent(options);
        switch (this.options.mode) {
        case 'vertical':
            this.axis = 'y';
            this.widthProperty = 'height';
            this.widthOffset = 'offsetHeight';
            break;
        case 'horizontal':
            this.axis = 'x';
            this.widthProperty = 'width';
            this.widthOffset = 'offsetWidth';
            break;
        }
        this.steps = this.options.contentWidth - this.options.frameWidth;
        this.ratio = this.options.contentWidth / this.options.frameWidth;
        if (this.options.handleControls) {
            this.uniqueId = 'id_' + $random(1000000, 100000000);
            $extend(this.options.events, {
                buttonPrev: {
                    click: 'scrollbarStop_' + this.uniqueId,
                    mousedown: 'scrollbarPrev_' + this.uniqueId,
                    mouseout: 'scrollbarStop_' + this.uniqueId
                },
                buttonNext: {
                    click: 'scrollbarStop_' + this.uniqueId,
                    mousedown: 'scrollbarNext_' + this.uniqueId,
                    mouseout: 'scrollbarStop_' + this.uniqueId
                }
            });
            this.bounds.controlsHandlers = {
                scrollbarNext: this.startScroll.pass('next', this),
                scrollbarPrev: this.startScroll.pass('previous', this),
                scrollbarStop: this.stopScroll.bind(this)
            }
            for (var event_name in this.bounds.controlsHandlers) {
                var event_alias = event_name + '_' + this.uniqueId;
                this.addEvent(event_alias, this.bounds.controlsHandlers[event_name]);
            }
        }
        this.buttons.left = new Gallery.Components.ComponentButton({
            defaultEnabled: true,
            plugin: this.options.plugin,
            className: 'ig-scroll-arrow ig-prev',
            events: this.options.events.buttonPrev || {}
        });
        this.buttons.right = new Gallery.Components.ComponentButton({
            defaultEnabled: true,
            plugin: this.options.plugin,
            className: 'ig-scroll-arrow ig-next',
            events: this.options.events.buttonNext || {}
        });
        this.buttons.handle = new Gallery.Components.ComponentButton({
            defaultEnabled: true,
            plugin: this.options.plugin,
            className: 'ig-scroll-handle',
            events: this.options.events.buttonHandle || {}
        });
        this.scrollElements.body = new Element('div', {
            "class": "ig-scroll-body"
        });
        this.scrollElements.handle = this.buttons.handle.getDomNode()
        this.scrollElements.left = this.buttons.left.getDomNode();
        this.scrollElements.right = this.buttons.right.getDomNode();
        this.scrollElements.handle.inject(this.scrollElements.body);
        this.domNode = new Element('div', {
            "class": "ig-scroll ig-" + this.options.mode
        }).grab(this.scrollElements.left).grab(this.scrollElements.body).grab(this.scrollElements.right);
        this.domNode.setStyle(this.widthProperty, this.options.scrollWidth)
    },
    inject: function (element) {
        this.parent(element);
        this.inspectStyles();
        this.applyStyles();
        this.slider = new Slider(this.scrollElements.body, this.scrollElements.handle, {
            steps: this.steps,
            mode: this.options.mode,
            onChange: this.onChange.bind(this)
        });
        var width = this.scrollElements.handle[this.widthOffset] - this.getConstant('scroll_body_padding_' + this.axis);
        this.scrollElements.handle.setStyle(this.widthProperty, Math.max(width, 5));
        $(document.body).addEvent('mouseleave', this.stopScroll.bind(this));
    },
    inspectStyles: function () {
        for (var key in this.scrollElements) {
            var element = this.scrollElements[key];
            var width = element.getComputedStyle('width').toInt();
            var height = element.getComputedStyle('height').toInt();
            this.setConstant('scroll_' + key + '_x', (isNaN(width) ? 0 : width) + Gallery.Utils.Element.getHorizontalOffsets(element));
            this.setConstant('scroll_' + key + '_y', (isNaN(height) ? 0 : height) + Gallery.Utils.Element.getVerticalOffsets(element));
        }
        this.setConstant('scroll_wrapper_x', Gallery.Utils.Element.getHorizontalOffsets(this.domNode));
        this.setConstant('scroll_wrapper_y', Gallery.Utils.Element.getVerticalOffsets(this.domNode));
        this.setConstant('scroll_body_padding_x', Gallery.Utils.Element.getPaddingWidth(this.scrollElements.body));
        this.setConstant('scroll_body_padding_y', Gallery.Utils.Element.getPaddingHeight(this.scrollElements.body));
    },
    applyStyles: function () {
        var width = this.options.scrollWidth - this.getConstant('scroll_wrapper_' + this.axis);
        this.domNode.setStyle(this.widthProperty, width);
        width -= this.getConstant('scroll_left_' + this.axis);
        width -= this.getConstant('scroll_right_' + this.axis);
        width -= this.getConstant('scroll_body_padding_' + this.axis);
        this.scrollElements.body.setStyle(this.widthProperty, width);
        this.scrollElements.handle.setStyle(this.widthProperty, width / this.ratio);
    },
    scrollPrev: function () {
        var target = this.step - this.options.buttonsStep;
        this.slider.set(target);
    },
    scrollNext: function () {
        var target = this.step + this.options.buttonsStep;
        this.slider.set(target);
    },
    stopScroll: function () {
        this.slider.drag.stop();
        $clear(this.scrollInterval);
    },
    startScroll: function (direction) {
        $clear(this.scrollInterval);
        switch (direction) {
        case 'previous':
            this.scrollInterval = this.scrollPrev.periodical(10, this);
            break;
        case 'next':
            this.scrollInterval = this.scrollNext.periodical(10, this);
            break;
        }
    },
    onChange: function (step) {
        this.step = step;
        if (this.cancelBubble) {
            return;
        }
        if (this.options.events.change) {
            this.dispatchEvent(this.options.events.change, [step]);
        }
        if (this.options.onChange) {
            this.options.onChange.apply(this, arguments);
        }
    },
    syncNavigation: function (offset) {
        $clear(this.scrollInterval);
        this.cancelBubble = true;
        this.slider.set(offset);
        this.cancelBubble = false;
    }
});


Gallery.Components.ComponentThumbnail = new Class({
    Extends: Gallery.Components.ComponentBase,
    domNode: null,
    options: {
        auto_scale: false,
        image_data: {},
        src_key: 'thumb_url',
        thumbnail_effect: false,
        notify_generating: false
    },
    plugin: {},
    imageSource: null,
    imageSourceStatus: 'loading',
    canvasSize: {
        width: null,
        height: null
    },
    imageSize: {
        width: null,
        height: null
    },
    initialize: function (options) {
        this.parent.apply(this, arguments);
        this.imageSource = String(this.options.image_data[this.options.src_key]);
        if (this.imageSource.test(/\.000$/)) {
            this.imageSourceStatus = 'generating';
            if (this.options.notify_generating) {
                this.dispatchEvent('generateThumbnail', [this]);
            }
        }
        this.domNode = document.createElement('div');
        this.image = new Element('img');
        var table = document.createElement('table');
        var table_body = document.createElement('tbody');
        var table_row = document.createElement('tr');
        this.imageHolder = document.createElement('td');
        this.domNode.className = 'ig-img'
        table.align = 'center';
        table.className = "ig-struct";
        this.imageHolder.className = "ig-" + this.imageSourceStatus;
        table.appendChild(table_body);
        table_body.appendChild(table_row);
        table_row.appendChild(this.imageHolder);
        this.imageHolder.appendChild(this.image);
        this.domNode.appendChild(table);
        this.bounds.events = {
            load: this.onImageLoad.bind(this),
            click: this.onImageClick.bindWithEvent(this),
            error: this.onImageError.bind(this)
        };
        this.image.addEvents(this.bounds.events);
    },
    loadContent: function () {
        this.dispatchEvent('loadThumbnail', [this.setSource.bind(this), this.domNode]);
    },
    setSource: function () {
        this.image.src = this.options.image_data[this.options.src_key];
    },
    onImageLoad: function () {
        if (Browser.Engine.trident) {
            this.image.addClass('ig-loading').inject(document.body);
        }
        this.imageSize = {
            width: this.image.width,
            height: this.image.height
        };
        if (Browser.Engine.trident) {
            this.imageHolder.appendChild(this.image).removeClass('ig-loading');
        }
        this.loaded = true;
        if (this.options.auto_scale) {
            this.scaleImage();
        }
        if (Gallery.Utils.stringToBool(this.options.thumbnail_effect)) {
            this.addEffect(this.options.thumbnail_effect);
        }
        this.image.set('tween', {
            duration: 1000,
            transition: 'sine:out'
        });
        this.image.set("opacity", "0");
        this.imageHolder.className = '';
        this.image.tween("opacity", "1");
    },
    onImageError: function () {},
    onImageClick: function (event) {
        this.dispatchEvent('view', [this.options.item_index, event]);
    },
    addEffect: function (effect) {
        if (effect == 'reflection') {
            var options = {
                opacity: Gallery.Constants.effects.reflex.opacity,
                container_height: this.getConstant('img_height'),
                reflection_height: this.getConstant('reflex_height')
            }
            Gallery.Effects.Reflection(this.image, options)
        } else if (effect == 'polaroid') {}
    },
    setCanvasSize: function (width, height) {
        this.canvasSize = {
            width: width,
            height: height
        };
        if (this.options.auto_scale && this.loaded) {
            this.scaleImage();
        }
    },
    scaleImage: function () {
        var image_size = this.getImageSize();
        this.image.width = image_size.width;
        this.image.height = image_size.height;
    },
    getImageSize: function () {
        var options_size_key = this.options.src_key.replace('_url', '') + '_size';
        var image_size = {
            width: $defined(this.imageSize.width) ? this.imageSize.width : parseInt(this.options.image_data[options_size_key].width),
            height: $defined(this.imageSize.height) ? this.imageSize.height : parseInt(this.options.image_data[options_size_key].height)
        };
        if (this.canvasSize.width < image_size.width || this.canvasSize.height < image_size.height) {
            var x_ratio = this.canvasSize.width / image_size.width;
            var y_ratio = this.canvasSize.height / image_size.height;
            if ((x_ratio * image_size.height) <= this.canvasSize.height) {
                image_size.width = Math.ceil(x_ratio * image_size.width);
                image_size.height = Math.ceil(x_ratio * image_size.height);
            } else {
                image_size.width = Math.ceil(y_ratio * image_size.width);
                image_size.height = Math.ceil(y_ratio * image_size.height);
            }
        }
        return image_size;
    }
});


Gallery.Effects.TransitionList = function (items, options) {
    var type = String.toLowerCase(options.type).capitalize();
    if (Gallery.Effects.TransitionList[type] == undefined) {
        return new Gallery.Effects.TransitionList.Base(items, options);
    }
    return new Gallery.Effects.TransitionList[type](items, options);
}
Gallery.Effects.TransitionList.Base = new Class({
    Implements: [Options, Events],
    options: {
        interval: 5000,
        fxOptions: {
            duration: 'long',
            transition: Fx.Transitions.Quart.easeInOut
        },
        index: 0,
        applyCurrentItemHeight: true,
        delayStart: false,
        delayAfterComplete: false,
        stopOnManualTransition: true,
        onBeforeStart: null,
        onStart: null,
        onComplete: null,
        onAfterComplete: null,
        onPlayStop: null,
        onPlayStart: null
    },
    timer: null,
    currentIndex: null,
    previousIndex: null,
    disabled: false,
    queueHolder: [],
    playOn: false,
    initialize: function (items, options) {
        this.setOptions(options);
        if (this.options.delayStart > 0) {
            this.options.interval += parseInt(this.options.delayStart)
        }
        if (this.options.delayAfterComplete > 0) {
            this.options.interval += parseInt(this.options.delayAfterComplete)
        }
        this.options.fxOptions["onComplete"] = this.onComplete.bind(this);
        this.items = items;
        this.prepareItems();
        this.set(this.options.index)
    },
    prepareItems: function () {
        this.container = this.items[0].getParent();
        if (this.options.applyCurrentItemHeight) {
            this.container.set('tween', {
                duration: this.options.fxOptions.duration,
                transition: this.options.fxOptions.transition,
                link: 'chain'
            });
        }
    },
    applyCurrentItemHeight: function () {
        if (this.items[this.currentIndex]) {
            var container_height = this.container.getStyle('height').toInt();
            var current_item_height = this.items[this.currentIndex].getDimensions({
                computeSize: true,
                styles: ['border', 'padding', 'margin']
            }).totalHeight;
            if (current_item_height && container_height != current_item_height) {
                this.container.setStyle('height', container_height);
                this.container.tween('height', current_item_height);
            }
        }
    },
    walk: function (index, is_manual, direction, is_fromqueue) {
        if (this.disabled && !is_fromqueue) {
            this.queueHolder.push(this.walk.bind(this, [index, is_manual, direction, true]));
            return;
        }
        this.disabled = true;
        if (this.playOn === true && is_manual) {
            if (this.options.stopOnManualTransition) {
                this.stop();
            } else {
                $clear(this.timer);
                this.timer = this.next.periodical(this.options.interval, this, [false]);
            }
        }
        this.fireEvent('beforeStart', [index, this.currentIndex]);
        if (this.options.delayStart) {
            this.start.delay(this.options.delayStart, this, [index, direction]);
        } else {
            this.start(index, direction);
        }
    },
    start: function (index, direction) {
        this.fireEvent('start', [index, this.currentIndex]);
        if (this.currentIndex !== index && $defined(this.items[index])) {
            this.previousIndex = this.currentIndex;
            this.currentIndex = index;
            if (this.options.applyCurrentItemHeight) {
                this.applyCurrentItemHeight()
            }
            this.items[this.currentIndex].show('block');
            this.items[this.previousIndex].hide();
        }
        this.onComplete();
    },
    next: function (is_manual) {
        var next = this.currentIndex + 1 < this.items.length ? this.currentIndex + 1 : 0;
        this.walk(next, is_manual, 'next');
    },
    previous: function (is_manual) {
        var previous = this.currentIndex > 0 ? this.currentIndex - 1 : this.items.length - 1;
        this.walk(previous, is_manual, 'previous');
    },
    set: function (index) {
        this.previousIndex = null;
        this.currentIndex = index;
        this.items.hide();
        this.items[index].show('block')
    },
    play: function (wait) {
        if (this.playOn === true) {
            return;
        }
        this.playOn = true;
        this.fireEvent('playStart');
        if (!wait) {
            this.next();
        }
        this.timer = this.next.periodical(this.options.interval, this, [false]);
    },
    stop: function () {
        if (this.playOn === false) {
            return;
        }
        this.playOn = false;
        $clear(this.timer);
        this.fireEvent('playStop');
    },
    onComplete: function () {
        this.fireEvent('complete', [this.currentIndex]);
        if (this.options.delayAfterComplete) {
            this.afterComplete.delay(this.options.delayAfterComplete, this);
        } else {
            this.afterComplete();
        }
    },
    afterComplete: function () {
        this.fireEvent('afterComplete', [this.currentIndex]);
        if (this.queueHolder.length > 0) {
            this.queueHolder.shift().run();
        } else {
            this.disabled = false;
        }
    }
});


Gallery.Effects.TransitionList.Slide = new Class({
    Extends: Gallery.Effects.TransitionList.Base,
    options: {
        fxOptions: {
            wheelStops: false
        },
        endlessChain: false,
        applyCurrentItemHeight: false
    },
    endlessChainHelper: {
        element: null,
        reset: false
    },
    initialize: function (items, options) {
        if (!items || items.length == 0) {
            return;
        }
        this.parent(items, options);
    },
    prepareItems: function () {
        this.container = document.createElement('div');
        var holder = this.items[0].getParent();
        var table = document.createElement('table');
        var table_body = document.createElement('tbody');
        this.stripholder = document.createElement('tr');
        this.container.className = "ig-mask"
        table.align = 'center';
        table.className = "ig-struct";
        holder.appendChild(this.container)
        table.appendChild(table_body);
        table_body.appendChild(this.stripholder);
        this.container.appendChild(table);
        var table_cell;
        for (var i = 0; i < this.items.length; i++) {
            table_cell = document.createElement('td');
            table_cell.className = "ig-slide-td";
            table_cell.appendChild(this.items[i]);
            this.stripholder.appendChild(table_cell);
        }
        if (this.options.endlessChain) {
            this.endlessChainHelper.element = this.items.getLast();
        }
        this.scroll = new Fx.Scroll(this.container, this.options.fxOptions);
    },
    start: function (index, direction) {
        this.fireEvent('start', [index, this.currentIndex]);
        var position = this.getScrollWidth(this.items[index]);
        if (this.options.endlessChain) {
            if (index === this.items.length - 2 && direction == 'previous') {
                this.scroll.set(this.getScrollWidth(this.items.getLast()), 0);
            }
            if (index === 0 && direction == 'next') {
                var position = this.getScrollWidth(this.items.getLast());
                this.endlessChainHelper.reset = true
            }
        }
        this.previousIndex = this.currentIndex;
        this.currentIndex = index;
        this.scroll.start(position, 0);
    },
    getScrollWidth: function (element) {
        if (Browser.Engine.trident) {
            var step_width = Gallery.Utils.Element.getBorderWidth(element) + Gallery.Utils.Element.getMarginWidth(element) + element.clientWidth;
            var position = this.items.indexOf(element);
            return step_width * position;
        } else {
            return element.getPosition(this.container)['x'] - Gallery.Utils.Element.getComputedSizeSum(element, ['margin-left']);
        }
    },
    next: function (manual) {
        var items_length = this.options.endlessChain ? this.items.length - 1 : this.items.length;
        var next = this.currentIndex + 1 < items_length ? this.currentIndex + 1 : 0;
        this.walk(next, manual, 'next');
    },
    set: function (index) {
        this.previousIndex = null;
        this.currentIndex = index;
        var position = this.getScrollWidth(this.items[this.currentIndex]);
        this.scroll.set(position, 0);
    },
    previous: function (manual) {
        var items_length = this.options.endlessChain ? this.items.length - 1 : this.items.length;
        var previous = this.currentIndex > 0 ? this.currentIndex - 1 : items_length - 1;
        this.walk(previous, manual, 'previous');
    },
    onComplete: function () {
        if (this.endlessChainHelper.reset) {
            this.endlessChainHelper.reset = false;
            var position = this.getScrollWidth(this.items[0]);
            this.scroll.set(position, 0);
        }
        this.parent();
    }
});


Gallery.Effects.TransitionList.Fade = new Class({
    Extends: Gallery.Effects.TransitionList.Base,
    initialize: function (items, options) {
        if (!items || items.length == 0) {
            return;
        }
        this.parent(items, options);
    },
    prepareItems: function () {
        this.parent();
        this.container.setStyles({
            position: 'relative'
        });
    },
    start: function (index, direction) {
        this.fireEvent('start', [index, this.currentIndex]);
        if (this.currentIndex !== index) {
            this.previousIndex = this.currentIndex;
            this.currentIndex = index;
            var styles = {
                display: 'block',
                zIndex: 2,
                opacity: 0,
                position: 'absolute',
                top: Gallery.Utils.Element.getComputedSizeSum(this.container, ['padding-top']),
                left: Gallery.Utils.Element.getComputedSizeSum(this.container, ['padding-left'])
            };
            this.items[this.currentIndex].setStyles(styles).set('tween', this.options.fxOptions).tween('opacity', '1');
            if (this.options.applyCurrentItemHeight) {
                this.applyCurrentItemHeight()
            }
            if ($defined(this.items[this.previousIndex])) {
                this.items[this.previousIndex].setStyle('zIndex', '1').set('tween', $merge(this.options.fxOptions, {
                    onComplete: null
                })).tween('opacity', '0');
            }
        } else {
            this.onComplete();
        }
    },
    onComplete: function () {
        if (this.items[this.previousIndex]) {
            this.items[this.previousIndex].setStyles({
                display: 'none',
                'zIndex': '0'
            });
        }
        this.items[this.currentIndex].setStyles({
            left: 0,
            top: 0,
            position: 'relative'
        });
        this.parent();
    },
    set: function (index) {
        this.parent(index);
        var styles = {
            zIndex: 2,
            opacity: 1,
            visibility: 'visible'
        }
        this.items[index].setStyles(styles);
    }
});
