/*
 * Galleria v 1.2 prerelease 1.1 2010-11-04
 * http://galleria.aino.se
 *
 * Copyright (c) 2010, Aino
 * Licensed under the MIT license.
 */

(function($) {

// some references
var undef,
    window = this,
    doc    = document,
    $doc   = $( doc );

// internal constants
var DEBUG = false,
    NAV   = navigator.userAgent.toLowerCase(),
    HASH  = window.location.hash.replace(/#\//, ''),
    CLICK = function() {
        // use this to make touch devices snappier
        return Galleria.TOUCH ? 'touchstart' : 'click';
    },
    IE    = (function() {
        var v = 3,
            div = doc.createElement( 'div' );
        while (
            div.innerHTML = '<!--[if gt IE '+(++v)+']><i></i><![endif]-->',
            div.getElementsByTagName('i')[0]
        );
        return v > 4 ? v : undef;
    }() ),
    DOM   = function() {
        return {
            html:  doc.documentElement,
            body:  doc.body,
            head:  doc.getElementsByTagName('head')[0],
            title: doc.title
        };
    },

    // the internal timeouts object
    // provides helper methods for controlling timeouts
    _timeouts = {

        trunk: {},

        add: function( id, fn, delay, loop ) {
            loop = loop || false;
            this.clear( id );
            if ( loop ) {
                var old = fn;
                fn = function() {
                    old();
                    _timeouts.add( id, fn, delay );
                };
            }
            this.trunk[ id ] = window.setTimeout( fn, delay );
        },

        clear: function( id ) {

            var del = function( i ) {
                window.clearTimeout( this.trunk[ i ] );
                delete this.trunk[ i ];
            };

            if ( !!id && id in this.trunk ) {
                del.call( _timeouts, id );

            } else if ( typeof id == 'undefined' ) {
                for ( var i in this.trunk ) {
                    del.call( _timeouts, i );
                }
            }
        }
    },

    // the internal gallery holder
    _galleries = [],

    // the transitions holder
    _transitions = {

        fade: function(params, complete) {
            $(params.next).css('opacity', 0).show().animate({
                opacity: 1
            }, params.speed, complete);

            if (params.prev) {
                $(params.prev).css('opacity', 1).show().animate({
                    opacity: 0
                }, params.speed);
            }
        },

        flash: function(params, complete) {
            $(params.next).css('opacity', 0);
            if (params.prev) {
                $(params.prev).animate({
                    opacity: 0
                }, (params.speed / 2), function() {
                    $(params.next).animate({
                        opacity: 1
                    }, params.speed, complete);
                });
            } else {
                $(params.next).animate({
                    opacity: 1
                }, params.speed, complete);
            }
        },

        pulse: function(params, complete) {
            if (params.prev) {
                $(params.prev).hide();
            }
            $(params.next).css('opacity', 0).animate({
                opacity:1
            }, params.speed, complete);
        },

        slide: function(params, complete) {
            var image  = $(params.next).parent(),
                images = this.$('images'), // ??
                width  = this._stageWidth,
                easing = this.getOptions( 'easing' );

            image.css({
                left: width * ( params.rewind ? -1 : 1 )
            });
            images.animate({
                left: width * ( params.rewind ? 1 : -1 )
            }, {
                duration: params.speed,
                queue: false,
                easing: easing,
                complete: function() {
                    images.css('left', 0);
                    image.css('left', 0);
                    complete();
                }
            });
        },

        fadeslide: function(params, complete) {

            var x = 0,
                easing = this.getOptions('easing'),
                distance = this.getStageWidth();

            if (params.prev) {
                x = Utils.parseValue( $(params.prev).css('left') );
                $(params.prev).css({
                    opacity: 1,
                    left: x
                }).animate({
                    opacity: 0,
                    left: x + ( distance * ( params.rewind ? 1 : -1 ) )
                },{
                    duration: params.speed,
                    queue: false,
                    easing: easing
                });
            }

            x = Utils.parseValue( $(params.next).css('left') );

            $(params.next).css({
                left: x + ( distance * ( params.rewind ? -1 : 1 ) ),
                opacity: 0
            }).animate({
                opacity: 1,
                left: x
            }, {
                duration: params.speed,
                complete: complete,
                queue: false,
                easing: easing
            });
        }
    },

    // the Utils singleton
    Utils = (function() {

        return {

            array : function( obj ) {
                return Array.prototype.slice.call(obj);
            },

            create : function( className, nodeName ) {
                nodeName = nodeName || 'div';
                var elem = doc.createElement( nodeName );
                elem.className = className;
                return elem;
            },

            forceStyles : function( elem, styles ) {
                elem = $(elem);
                if ( elem.attr( 'style' ) ) {
                    elem.data( 'styles', elem.attr( 'style' ) ).removeAttr( 'style' );
                }
                elem.css( styles );
            },

            revertStyles : function() {
                $.each( Utils.array( arguments ), function( i, elem ) {

                    elem = $( elem ).removeAttr( 'style' );

                    if ( elem.data( 'styles' ) ) {
                        elem.attr( 'style', elem.data('styles') ).data( 'styles', null );
                    }
                });
            },

            moveOut : function( elem ) {
                Utils.forceStyles( elem, {
                    position: 'absolute',
                    left: -10000
                });
            },

            moveIn : function() {
                Utils.revertStyles.apply( Utils, Utils.array( arguments ) );
            },

            hide : function( elem, speed, callback ) {
                elem = $(elem);

                // save the value if not exist
                if (! elem.data('opacity') ) {
                    elem.data('opacity', elem.css('opacity') );
                }

                // always hide
                var style = { opacity: 0 };

                if (speed) {
                    elem.stop().animate( style, speed, callback );
                } else {
                    elem.css( style );
                };
            },

            show : function( elem, speed, callback ) {
                elem = $(elem);

                // bring back saved opacity
                var saved = parseFloat( elem.data('opacity') ) || 1,
                    style = { opacity: saved };

                // reset save if opacity == 1
                if (saved == 1) {
                    elem.data('opacity', null);
                }

                // animate or toggle
                if (speed) {
                    elem.stop().animate( style, speed, callback );
                } else {
                    elem.css( style );
                };
            },

            addTimer : function() {
                _timeouts.add.apply( _timeouts, Utils.array( arguments ) );
                return this;
            },

            clearTimer : function() {
                _timeouts.clear.apply( _timeouts, Utils.array( arguments ) );
                return this;
            },

            wait : function(options) {
                options = $.extend({
                    until : function() { return false; },
                    success : function() {},
                    error : function() { Galleria.raise('Could not complete wait function.'); },
                    timeout: 3000
                }, options);

                var start = Utils.timestamp(),
                    elapsed,
                    now;

                window.setTimeout(function() {
                    now = Utils.timestamp();
                    elapsed = now - start;
                    if ( options.until( elapsed ) ) {
                        options.success();
                        return false;
                    }

                    if (now >= start + options.timeout) {
                        options.error();
                        return false;
                    }
                    window.setTimeout(arguments.callee, 2);
                }, 2);
            },

            toggleQuality : function( img, force ) {

                if ( !( IE == 7 || IE == 8 ) || !!img === false ) {
                    return;
                }

                if ( typeof force === 'undefined' ) {
                    force = img.style.msInterpolationMode == 'nearest-neighbor';
                }

                img.style.msInterpolationMode = force ? 'bicubic' : 'nearest-neighbor';
            },

            insertStyleTag : function( styles ) {
                var style = doc.createElement( 'style' );
                DOM().head.appendChild( style );

                if ( style.styleSheet ) { // IE
                    style.styleSheet.cssText = styles;
                } else {
                    var cssText = doc.createTextNode( styles );
                    style.appendChild( cssText );
                }
            },

            // a loadscript method that works for local scripts
            loadScript: function( url, callback ) {
                var done = false,
                    script = $('<scr'+'ipt>').attr({
                        src: url,
                        async: true
                    }).get(0);

               // Attach handlers for all browsers
               script.onload = script.onreadystatechange = function() {
                   if ( !done && (!this.readyState ||
                       this.readyState == 'loaded' || this.readyState == 'complete') ) {
                       done = true;

                       if (typeof callback == 'function') {
                           callback.call( this, this );
                       }

                       // Handle memory leak in IE
                       script.onload = script.onreadystatechange = null;
                   }
               };

               var s = doc.getElementsByTagName( 'script' )[0];
               s.parentNode.insertBefore( script, s );
            },

            // parse anything into a number
            parseValue: function( val ) {
                if (typeof val == 'number') {
                    return val;
                } else if (typeof val == 'string') {
                    var arr = val.match(/\-?\d/g);
                    return arr && arr.constructor == Array ? arr.join('') * 1 : 0;
                } else {
                    return 0;
                }
            },

            // timestamp abstraction
            timestamp: function() {
                return new Date().getTime();
            },

            // this is pretty crap, but works for now
            // it will add a callback, but it can't guarantee that the styles can be fetched
            // using getComputedStyle further checking needed, possibly a dummy element
            loadCSS : function( href, id, callback ) {

                var link,
                    ready = false,
                    length;

                // look for manual css
                $('link[rel=stylesheet]').each(function() {
                    if ( new RegExp( href ).test( this.href ) ) {
                        link = this;
                        return false;
                    }
                });

                if ( typeof id == 'function' ) {
                    callback = id;
                    id = undef;
                }

                callback = callback || function() {}; // dirty

                // if already present, return
                if ( link ) {
                    callback.call( link, link );
                    return link;
                }

                // save the length of stylesheets to check against
                length = doc.styleSheets.length;

                // add timestamp if DEBUG is true
                if ( DEBUG ) {
                    href += '?' + Utils.timestamp();
                }

                // check for existing id
                if( $('#'+id).length ) {
                    $('#'+id).attr('href', href);
                    length--;
                    ready = true;
                } else {
                    link = $( '<link>' ).attr({
                        rel: 'stylesheet',
                        href: href,
                        id: id
                    }).get(0);

                    window.setTimeout(function() {
                        var styles = $('link[rel="stylesheet"], style');
                        if ( styles.length ) {
                            styles.get(0).parentNode.insertBefore( link, styles[0] );
                        } else {
                            DOM().head.appendChild( link );
                        }

                        if ( IE ) {
                            link.attachEvent( 'onreadystatechange', function(e) {
                                if( link.readyState == 'complete' ) {
                                    ready = true;
                                }
                            });
                        } else {
                            // what to do here? returning for now.
                            ready = true;
                        }
                    }, 10);
                }

                if (typeof callback == 'function') {

                    Utils.wait({
                        until: function() {
                            return ready && doc.styleSheets.length > length;
                        },
                        success: function() {
                            Utils.addTimer( 'css', function() {
                                callback.call( link, link );
                            }, 100);
                        },
                        error: function() {
                            Galleria.raise( 'Theme CSS could not load' );
                        },
                        timeout: 1000
                    });
                }
                return link;
            }
        };
    })();

/**
    The main Galleria class

    @class

    @example var gallery = new Galleria();

    @author http://aino.se

    @requires jQuery

    @returns {Galleria}
*/

Galleria = function() {

    var self = this;

    // the theme used
    this._theme = undef;

    // internal options
    this._options = {};

    // flag for controlling play/pause
    this._playing = false;

    // internal interval for slideshow
    this._playtime = 5000;

    // internal variable for the currently active image
    this._active = null;

    // the internal queue, arrayified
    this._queue = { length: 0 };

    // the internal data array
    this._data = [];

    // the internal dom collection
    this._dom = {};

    // the internal thumbnails array
    this._thumbnails = [];

    // internal init flag
    this._initialized = false;

    // global stagewidth/height
    this._stageWidth = 0;
    this._stageHeight = 0;

    // target holder
    this._target = undef;

    // instance id
    this._id = Utils.timestamp();

    // add some elements
    var divs =  'container stage images image-nav image-nav-left image-nav-right ' +
                'info info-text info-title info-description info-author ' +
                'thumbnails thumbnails-list thumbnails-container thumb-nav-left thumb-nav-right ' +
                'loader counter tooltip',
        spans = 'current total';

    $.each( divs.split(' '), function( i, elemId ) {
        self._dom[ elemId ] = Utils.create( 'galleria-' + elemId );
    });

    $.each( spans.split(' '), function( i, elemId ) {
        self._dom[ elemId ] = Utils.create( 'galleria-' + elemId, 'span' );
    });

    // the internal keyboard object
    // keeps reference of the keybinds and provides helper methods for binding keys
    var keyboard = this._keyboard = {

        keys : {
            'UP': 38,
            'DOWN': 40,
            'LEFT': 37,
            'RIGHT': 39,
            'RETURN': 13,
            'ESCAPE': 27,
            'BACKSPACE': 8,
            'SPACE': 32
        },

        map : {},

        bound: false,

        press: function(e) {
            var key = e.keyCode || e.which;
            if ( key in keyboard.map && typeof keyboard.map[key] == 'function' ) {
                keyboard.map[key].call(self, e);
            }
        },

        attach: function(map) {
            for( var key in map ) {
                var up = key.toUpperCase();
                if ( up in keyboard.keys ) {
                    keyboard.map[ keyboard.keys[up] ] = map[key];
                }
            }
            if ( !keyboard.bound ) {
                keyboard.bound = true;
                $doc.bind('keydown', keyboard.press);
            }
        },

        detach: function() {
            keyboard.bound = false;
            $doc.unbind('keydown', keyboard.press);
        }
    };

    // internal controls for keeping track of active / inactive images
    var controls = this._controls = {

        0: undef,

        1: undef,

        active : 0,

        swap : function() {
            controls.active = controls.active ? 0 : 1;
        },

        getActive : function() {
            return controls[ controls.active ];
        },

        getNext : function() {
            return controls[ 1 - controls.active ];
        }
    };

    // internal carousel object
    var carousel = this._carousel = {

        // shortcuts
        next: self.$('thumb-nav-right'),
        prev: self.$('thumb-nav-left'),

        // cache the width
        width: 0,

        // track the current position
        current: 0,

        // cache max value
        max: 0,

        // save all hooks for each width in an array
        hooks: [],

        // update the carousel
        // you can run this method anytime, f.ex on window.resize
        update: function() {
            var w = 0,
                h = 0,
                hooks = [0];

            $.each( self._thumbnails, function( i, thumb ) {
                if ( thumb.ready ) {
                    w += thumb.outerWidth || $( thumb.container ).outerWidth( true );
                    hooks[ i+1 ] = w;
                    h = Math.max( h, thumb.outerHeight || $( thumb.container).outerHeight() );
                }
            });
            self.$( 'thumbnails-container' ).toggleClass( 'galleria-carousel', w > self._stageWidth );

            self.$( 'thumbnails' ).css({
                width: w,
                height: h
            });

            carousel.max = w;
            carousel.hooks = hooks;
            carousel.width = self.$( 'thumbnails-list' ).width();
            carousel.setClasses();

            // todo: fix so the carousel moves to the left
        },

        bindControls: function() {

            carousel.next.bind( CLICK(), function(e) {
                e.preventDefault();

                if ( self._options.carousel_steps == 'auto' ) {

                    for ( var i = carousel.current; i < carousel.hooks.length; i++ ) {
                        if ( carousel.hooks[i] - carousel.hooks[ carousel.current ] > carousel.width ) {
                            carousel.set(i - 2);
                            break;
                        }
                    }

                } else {
                    carousel.set( carousel.current + self._options.carousel_steps);
                }
            });

            carousel.prev.bind( CLICK(), function(e) {
                e.preventDefault();

                if ( self._options.carousel_steps == 'auto' ) {

                    for ( var i = carousel.current; i >= 0; i-- ) {
                        if ( carousel.hooks[ carousel.current ] - carousel.hooks[i] > carousel.width ) {
                            carousel.set( i + 2 );
                            break;
                        } else if ( i == 0 ) {
                            carousel.set( 0 );
                            break;
                        }
                    }
                } else {
                    carousel.set( carousel.current - self._options.carousel_steps );
                }
            });
        },

        // calculate and set positions
        set: function( i ) {
            i = Math.max( i, 0 );
            while ( carousel.hooks[i - 1] + carousel.width > carousel.max && i >= 0 ) {
                i--;
            }
            carousel.current = i;
            carousel.animate();
        },

        // get the last position
        getLast: function(i) {
            return ( i || carousel.current ) - 1;
        },

        // follow the active image
        follow: function(i) {

            //don't follow if position fits
            if ( i == 0 || i == carousel.hooks.length - 2 ) {
                carousel.set( i );
                return;
            }

            // calculate last position
            var last = carousel.current;
            while( carousel.hooks[last] - carousel.hooks[ carousel.current ] <
                   carousel.width && last <= carousel.hooks.length ) {
                last ++;
            }

            // set position
            if ( i - 1 < carousel.current ) {
                carousel.set( i - 1 );
            } else if ( i + 2 > last) {
                carousel.set( i - last + carousel.current + 2 );
            }
        },

        // helper for setting disabled classes
        setClasses: function() {
            carousel.prev.toggleClass( 'disabled', !carousel.current );
            carousel.next.toggleClass( 'disabled', carousel.hooks[ carousel.current ] + carousel.width > carousel.max );
        },

        // the animation method
        animate: function(to) {
            carousel.setClasses();
            var num = carousel.hooks[ carousel.current ] * -1;

            if ( isNaN( num ) ) {
                return;
            }

            self.$( 'thumbnails' ).animate({
                left: num
            },{
                duration: self._options.carousel_speed,
                easing: self._options.easing,
                queue: false
            });
        }
    };

    // tooltip control
    // added in 1.2
    var tooltip = this._tooltip = {

        initialized : false,

        active: null,

        open: false,

        init: function() {

            tooltip.initialized = true;

            var css = '.galleria-tooltip{padding:3px 8px;max-width:50%;background:#ffe;color:#000;z-index:3;position:absolute;font-size:11px;line-height:1.3' +
                      'opacity:0;box-shadow:0 0 2px rgba(0,0,0,.4);-moz-box-shadow:0 0 2px rgba(0,0,0,.4);-webkit-box-shadow:0 0 2px rgba(0,0,0,.4);}';

            Utils.insertStyleTag(css);

            self.$( 'tooltip' ).css('opacity', .8);
            Utils.hide( self.get('tooltip') );

        },

        // move handler
        move: function( e ) {
            var mouseX = self.getMousePosition(e).x,
                mouseY = self.getMousePosition(e).y,
                $elem = self.$( 'tooltip' ),
                x = mouseX,
                y = mouseY,
                height = $elem.outerHeight( true ) + 1,
                width = $elem.outerWidth( true ),
                limitY = height + 15;

            var maxX = self._stageWidth - width,
                maxY = self._stageHeight - height;

            if ( !isNaN(x) && !isNaN(y) ) {

                x += 15;
                y -= 35;

                x = Math.max( 0, Math.min( maxX, x ) );
                y = Math.max( 0, Math.min( maxY, y ) );

                if( mouseY < limitY ) {
                    y = limitY;
                }

                $elem.css({ left: x, top: y });
            }
        },

        // bind elements to the tooltip
        // you can bind multiple elementIDs using { elemID : function } or { elemID : string }
        // you can also bind single DOM elements using bind(elem, string)
        bind: function( elem, value ) {

            if (! tooltip.initialized ) {
                tooltip.init();
            }

            var hover = function( elem, value) {

                tooltip.define( elem, value );

                $( elem ).hover(function() {

                    tooltip.active = elem;
                    Utils.clearTimer('switch_tooltip');
                    self.$('container').unbind( 'mousemove', tooltip.move ).bind( 'mousemove', tooltip.move ).trigger( 'mousemove' );
                    tooltip.show( elem );

                    Galleria.utils.addTimer( 'tooltip', function() {
                        self.$( 'tooltip' ).stop();
                        Utils.show( self.get( 'tooltip' ), 400 );
                        tooltip.open = true;

                    }, tooltip.open ? 0 : 1000);

                }, function() {

                    tooltip.active = null;

                    self.$( 'container' ).unbind( 'mousemove', tooltip.move );
                    Utils.clearTimer( 'tooltip' );

                    Utils.hide( self.get( 'tooltip' ), 200, function() {
                        Utils.addTimer('switch_tooltip', function() {
                            tooltip.open = false;
                        }, 1000);
                    });
                });
            };

            if (typeof value == 'string') {
                hover( ( elem in self._dom ? self.get(elem) : elem ), value );
            } else {
                // asume elemID here
                $.each( elem, function( elemID, val ) {
                    hover( self.get(elemID), val );
                });
            }
        },

        show: function( elem ) {
            var text = $(elem).data('tt');
            if ( ! text ) {
                return;
            }
            text = typeof text == 'function' ? text() : text;
            self.$( 'tooltip' ).html( text.replace(/\s/, '&nbsp;') );
        },

        // redefine the tooltip here
        define: function( elem, value ) {

            // we store functions, not strings
            if (typeof value !== 'function') {
                var s = value;
                value = function() {
                    return s;
                };
            }

            if ( elem in self._dom ) {
                elem = self.get( elem );
            }

            $(elem).data('tt', value);

            tooltip.show( elem );

        },

        refresh: function() {
            $.each( arguments, function(i, elem) {
                if ( tooltip.active == elem ) {
                    tooltip.show( elem );
                }
            });
        }
    };

    // internal fullscreen control
    // added in 1.195
    // still kind of experimental
    var fullscreen = this._fullscreen = {
        scrolled: 0,
        enter: function(callback) {

            // hide the image until rescale is complete
            Utils.hide( self.getActiveImage() );

            self.$( 'container' ).addClass( 'fullscreen' );

            fullscreen.scrolled = $(window).scrollTop();

            // begin styleforce
            Utils.forceStyles(self.get('container'), {
                position: 'fixed',
                top: 0,
                left: 0,
                width: '100%',
                height: '100%',
                zIndex: 10000
            });

            var htmlbody = {
                height: '100%',
                overflow: 'hidden',
                margin:0,
                padding:0
            };

            Utils.forceStyles( DOM().html, htmlbody );
            Utils.forceStyles( DOM().body, htmlbody );

            // attach some keys
            self.attachKeyboard({
                escape: self.exitFullscreen,
                right: self.next,
                left: self.prev
            });

            // init the first rescale and attach callbacks
            self.rescale(function() {

                Utils.addTimer('fullscreen_enter', function() {
                    // show the image after 50 ms
                    Utils.show( self.getActiveImage() );

                    if (typeof callback == 'function') {
                        callback.call( self );
                    }

                }, 100);

                self.trigger( Galleria.FULLSCREEN_ENTER );
            });

            // bind the scaling to the resize event
            $(window).resize( function() {
                fullscreen.scale();
            } );
        },

        scale : function() {
            self.rescale();
        },

        exit: function(callback) {

            Utils.hide( self.getActiveImage() );

            self.$('container').removeClass( 'fullscreen' );

            // revert all styles
            Utils.revertStyles( self.get('container'), DOM().html, DOM().body );

            // scroll back
            window.scrollTo(0, fullscreen.scrolled);

            // detach all keyboard events (is this good?)
            self.detachKeyboard();

            self.rescale(function() {
                Utils.addTimer('fullscreen_exit', function() {

                    // show the image after 50 ms
                    Utils.show( self.getActiveImage() );

                    if ( typeof callback == 'function' ) {
                        callback.call( self );
                    }

                }, 50);

                self.trigger( Galleria.FULLSCREEN_EXIT );
            });

            $(window).unbind('resize', fullscreen.scale);
        }
    };

    // the internal idle object for controlling idle states
    // TODO occational event conflicts
    var idle = this._idle = {

        trunk: [],

        bound: false,

        add: function(elem, to) {
            if (!elem) {
                return;
            }
            if (!idle.bound) {
                idle.addEvent();
            }
            elem = $(elem);

            var from = {};

            for (var style in to) {
                from[style] = elem.css(style);
            }
            elem.data('idle', {
                from: from,
                to: to,
                complete: true,
                busy: false
            });
            idle.addTimer();
            idle.trunk.push(elem);
        },

        remove: function(elem) {

            elem = jQuery(elem);

            $.each(idle.trunk, function(i, el) {
                if ( el.length && !el.not(elem).length ) {
                    self._idle.show(elem);
                    self._idle.trunk.splice(i, 1);
                }
            });

            if (!idle.trunk.length) {
                idle.removeEvent();
                Utils.clearTimer('idle');
            }
        },

        addEvent : function() {
            idle.bound = true;
            self.$('container').bind('mousemove click', idle.showAll );
        },

        removeEvent : function() {
            idle.bound = false;
            self.$('container').unbind('mousemove click', idle.showAll );
        },

        addTimer : function() {
            Utils.addTimer('idle', function() {
                self._idle.hide();
            }, self._options.idle_time );
        },

        hide : function() {
            self.trigger( Galleria.IDLE_ENTER );

            $.each( idle.trunk, function(i, elem) {

                var data = elem.data('idle');

                if (! data) {
                    return;
                }

                data.complete = false;

                elem.stop().animate(data.to, {
                    duration: 600,
                    queue: false,
                    easing: 'swing'
                });
            });
        },

        showAll : function() {
            Utils.clearTimer('idle');

            $.each(self._idle.trunk, function( i, elem ) {
                self._idle.show( elem );
            });
        },

        show: function(elem) {

            var data = elem.data('idle');

            if (!data.busy && !data.complete) {

                data.busy = true;

                self.trigger( Galleria.IDLE_EXIT );

                elem.animate(data.from, {
                    duration: 300,
                    queue: false,
                    easing: 'swing',
                    complete: function() {
                        $(this).data('idle').busy = false;
                        $(this).data('idle').complete = true;
                    }
                });
            }
            idle.addTimer();
        }
    };

    // internal lightbox object
    // creates a predesigned lightbox for simple popups of images in galleria
    var lightbox = this._lightbox = {

        width : 0,

        height : 0,

        initialized : false,

        active : null,

        image : null,

        elems : {},

        init : function() {

            // trigger the event
            self.trigger( Galleria.LIGHTBOX_OPEN );

            if ( lightbox.initialized ) {
                return;
            }
            lightbox.initialized = true;

            // create some elements to work with
            var elems = 'overlay box content shadow title info close prevholder prev nextholder next counter image',
                el = {},
                op = self._options,
                css = '',
                cssMap = {
                    overlay:    'position:fixed;display:none;opacity:'+op.overlay_opacity+';top:0;left:0;width:100%;height:100%;background:'+op.overlay_background+';z-index:99990',
                    box:        'position:fixed;display:none;width:400px;height:400px;top:50%;left:50%;margin-top:-200px;margin-left:-200px;z-index:99991',
                    shadow:     'position:absolute;background:#000;width:100%;height:100%;',
                    content:    'position:absolute;background-color:#fff;top:10px;left:10px;right:10px;bottom:10px;overflow:hidden',
                    info:       'position:absolute;bottom:10px;left:10px;right:10px;color:#444;font:11px/13px arial,sans-serif;height:13px',
                    close:      'position:absolute;top:10px;right:10px;height:20px;width:20px;background:#fff;text-align:center;cursor:pointer;color:#444;font:16px/22px arial,sans-serif;z-index:99999',
                    image:      'position:absolute;top:10px;left:10px;right:10px;bottom:30px;overflow:hidden',
                    prevholder: 'position:absolute;width:50%;height:100%;cursor:pointer',
                    nextholder: 'position:absolute;width:50%;height:100%;right:0;cursor:pointer',
                    prev:       'position:absolute;top:50%;margin-top:-20px;height:40px;width:30px;background:#fff;left:20px;display:none;line-height:40px;text-align:center;color:#000',
                    next:       'position:absolute;top:50%;margin-top:-20px;height:40px;width:30px;background:#fff;right:20px;left:auto;display:none;line-height:40px;text-align:center;color:#000',
                    title:      'float:left',
                    counter:    'float:right;margin-left:8px'
                },
                hover = function(elem) {
                    return elem.hover(
                        function() { $(this).css( 'color', '#bbb' ); },
                        function() { $(this).css( 'color', '#444' ); }
                    );
                };

            // create and insert CSS
            $.each(cssMap, function( key, value ) {
                css += '.galleria-lightbox-'+key+'{'+value+'}';
            });

            Utils.insertStyleTag( css );

            // create the elements
            $.each(elems.split(' '), function( i, elemId ) {
                self.addElement( 'lightbox-' + elemId );
                el[ elemId ] = lightbox.elems[ elemId ] = self.get( 'lightbox-' + elemId );
            });

            // initiate the image
            lightbox.image = new Galleria.Picture();

            // append the elements
            self.append({
                'lightbox-box': ['lightbox-shadow','lightbox-content', 'lightbox-close','lightbox-prevholder','lightbox-nextholder'],
                'lightbox-info': ['lightbox-title','lightbox-counter'],
                'lightbox-content': ['lightbox-info', 'lightbox-image'],
                'lightbox-prevholder': 'lightbox-prev',
                'lightbox-nextholder': 'lightbox-next'
            });

            $( el.image ).append( lightbox.image.container );

            $( DOM().body ).append( el.overlay, el.box );

            // add the prev/next nav and bind some controls

            hover( $( el.close ).bind( CLICK(), lightbox.hide ).html('&#215;') );

            $.each( ['Prev','Next'], function(i, dir) {

                var $d = $( el[ dir.toLowerCase() ] ).html( /v/.test( dir ) ? '‹&nbsp;' : '&nbsp;›' );

                $( el[ dir.toLowerCase()+'holder'] ).hover(function() {
                    $d.show();
                }, function() {
                    $d.fadeOut( 200 );
                }).bind( CLICK(), function() {
                    lightbox[ 'show' + dir ]();
                });

            });
            $( el.overlay ).bind( CLICK(), lightbox.hide );

        },

        rescale: function(event) {

            // calculate
            var width = Math.min( $(window).width(), lightbox.width ),
                height = Math.min( $(window).height(), lightbox.height ),
                ratio = Math.min( (width - 60) / lightbox.width, (height - 80) / lightbox.height ),
                destWidth = ( lightbox.width * ratio ) + 40,
                destHeight = ( lightbox.height * ratio ) + 60,
                to = {
                    width: destWidth,
                    height: destHeight,
                    marginTop: Math.ceil( destHeight / 2 ) *- 1,
                    marginLeft: Math.ceil( destWidth / 2 ) *- 1
                };

            // if rescale event, don't animate
            if ( event ) {
                $( lightbox.elems.box ).css( to );
            } else {
                $( lightbox.elems.box ).animate(
                    to,
                    self._options.lightbox_transition_speed,
                    self._options.easing,
                    function() {
                        var image = lightbox.image,
                            speed = self._options.lightbox_fade_speed;

                        self.trigger({
                            type: Galleria.LIGHTBOX_IMAGE,
                            imageTarget: image.image
                        });

                        image.show();
                        Utils.show( image.image, speed );
                        Utils.show( lightbox.elems.info, speed );
                    }
                );
            }
        },

        hide: function() {

            // remove the image
            lightbox.image.image = null;

            $(window).unbind('resize', lightbox.rescale);

            $( lightbox.elems.box ).hide();

            Utils.hide( lightbox.elems.info );

            Utils.hide( lightbox.elems.overlay, 200, function() {
                $( this ).hide().css( 'opacity', self._options.overlay_opacity );
                self.trigger( Galleria.LIGHTBOX_CLOSE );
            });
        },

        showNext: function() {
            lightbox.show( self.getNext( lightbox.active ) );
        },

        showPrev: function() {
            lightbox.show( self.getPrev( lightbox.active ) );
        },

        show: function(index) {

            lightbox.active = index = typeof index == 'number' ? index : self.getIndex();

            if ( !lightbox.initialized ) {
                lightbox.init();
            }

            $(window).unbind('resize', lightbox.rescale );

            var data = self.getData(index),
                total = self.getDataLength();

            Utils.hide( lightbox.elems.info );

            lightbox.image.load( data.image, function( image ) {

                lightbox.width = image.original.width;
                lightbox.height = image.original.height;

                $( image.image ).css({
                    width: '100.5%',
                    height: '100.5%',
                    top: 0,
                    zIndex: 99998,
                    opacity: 0
                });

                lightbox.elems.title.innerHTML = data.title;
                lightbox.elems.counter.innerHTML = (index + 1) + ' / ' + total;
                $(window).resize( lightbox.rescale );
                lightbox.rescale();
            });

            $( lightbox.elems.overlay ).show();
            $( lightbox.elems.box ).show();
        }
    };

    return this;
};

// end Galleria constructor

Galleria.prototype = {

    /**
        Use this function to initialize the gallery and start loading.
        Should only be called once per instance.

        @param {HTML Element} target The target element
        @param {Object} options The gallery options

        @returns {Galleria}
    */

    init: function( target, options ) {

        var self = this;

        // save the instance
        _galleries.push( this );

        // save the original ingredients
        this._original = {
            target: target,
            options: options,
            data: null
        };

        // save the target here
        this._target = this._dom.target = target.nodeName ? target : $( target ).get(0);

        // raise error if no target is detected
        if ( !this._target ) {
             Galleria.raise('Target not found.');
             return;
        }

        // apply options
        this._options = {
            autoplay: false,
            carousel: true,
            carousel_follow: true,
            carousel_speed: 400,
            carousel_steps: 'auto',
            clicknext: false,
            data_config : function( elem ) { return {}; },
            data_selector: 'img',
            data_source: this._target,
            debug: undef,
            easing: 'galleria',
            extend: function(options) {},
            height: 'auto',
            idle_time: 3000,
            image_crop: false,
            image_margin: 0,
            image_pan: false,
            image_pan_smoothness: 12,
            image_position: '50%',
            keep_source: false,
            lightbox_fade_speed: 200,
            lightbox_transition_speed: 500,
            link_source_images: true,
            max_scale_ratio: undef,
            min_scale_ratio: undef,
            on_image: function(img,thumb) {},
            overlay_opacity: .85,
            overlay_background: '#0b0b0b',
            pause_on_interaction: true, // 1.9.96
            popup_links: false,
            preload: 2,
            queue: true,
            show: 0,
            show_info: true,
            show_counter: true,
            show_imagenav: true,
            thumb_crop: true,
            thumb_event_type: CLICK(),
            thumb_fit: true,
            thumb_margin: 0,
            thumb_quality: 'auto',
            thumbnails: true,
            transition: 'fade',
            transition_initial: undef,
            transition_speed: 400,
            width: 'auto'
        };

        // apply debug
        if ( options && options.debug === true ) {
            DEBUG = true;
        }

        // hide all content
        $( this._target ).children().hide();

        // now we just have to wait for the theme...
        if ( Galleria.theme ) {
            this._init();
        } else {
            Utils.addTimer('themeload', function() {
                Galleria.raise( 'No theme found.', true);
            }, 2000);

            $doc.one( Galleria.THEMELOAD, function() {
                Utils.clearTimer( 'themeload' );
                self._init.call( self );
            });
        }
    },

    // the internal _init is called when the THEMELOAD event is triggered
    // this method should only be called once per instance
    // for manipulation of data, use the .load method

    _init: function() {
        var self = this;

        if ( this._initialized ) {
            Galleria.raise( 'Init failed: Gallery instance already initialized.' );
            return this;
        }

        this._initialized = true;

        if ( !Galleria.theme ) {
            Galleria.raise( 'Init failed: No theme found.' );
            return this;
        }

        // merge the theme & caller options
        $.extend( true, this._options, Galleria.theme.defaults, this._original.options );

        // bind the gallery to run when data is ready
        this.bind( Galleria.DATA, function() {

            // save the new data
            this._original.data = this._data;

            // lets show the counter here
            this.get('total').innerHTML = this.getDataLength();

            // cache the container
            var $container = this.$( 'container' );

            // the gallery is ready, let's just wait for the css
            var num = { width: 0, height: 0 };
            var testElem =  Utils.create('galleria-image');

            // check container and thumbnail height
            Utils.wait({
                until: function() {

                    // keep trying to get the value
                    $.each(['width', 'height'], function( i, m ) {

                        if (self._options[ m ] && typeof self._options[ m ] == 'number') {
                            num[ m ] = self._options[ m ];
                        } else {
                            num[m] = Utils.parseValue( self.$( 'target' ).css( m ) ) ||
                                     Utils.parseValue( $container.css( m ) ) ||
                                     self.$( 'target' )[ m ]() ||
                                     $container[ m ]()
                        }

                    });

                    var thumbHeight = function() {
                        return true;
                    };

                    // make sure thumbnails have a height as well
                    if ( self._options.thumbnails ) {
                        self.$('thumbnails').append( testElem );
                        thumbHeight = function() {
                            return !!$( testElem ).height();
                        };
                    }

                    return thumbHeight() && num.width && num.height > 50;

                },
                success: function() {

                    // remove the testElem
                    $( testElem ).remove();

                    // apply the new meassures
                    $container.width( num.width );
                    $container.height( num.height );

                    // for some strange reason, webkit needs a single setTim
