/*
 * Galleria v 1.2 prerelease 1.0 2010-10-20
 * 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 mobile devices snappier
        return Galleria.MOBILE ? '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
        }
    },
    
    // Debug TOOLS
    
    // log wrapper
    log = function() {
        try { 
            window.console.log.apply( window.console, Utils.array(arguments) ); 
        } catch( e ) {
            try {
                opera.postError.apply( opera, arguments ); 
            } catch( er ) { 
                  trace( arguments ); 
            } 
        }
    },
    
    // raise errors
    raise = function( msg, force ) {
        if ( DEBUG || force ) {
            throw new Error( msg );
        }
    },
    
    // cross-browser tracer
    _tracer = {
        
        ts : new Date().getTime(),
        
        initialized : false,
        
        open: true,
        
        init : function() {
            this.initialized = true;
            this.elem = $('<div id="log">').css({
                background: '#fff', opacity: .8, padding: '20px', width: 300, height: '80%',
                position: 'fixed', top:10, left:10, zIndex: 100000,
                color:'#000', overflow:'auto', font: '11px/1.2 arial,sans-serif'
            }).click(function() {
                $(this).css({
                    height: _tracer.open ? 0 : '80%',
                    opacity: _tracer.open ? .1 : .8
                });
                _tracer.open = !_tracer.open;
            }).appendTo( DOM().body );
        
        },
        
        log: function( msg ) {
            
            if ( !this.initialized ) {
                this.init();
            }

            var now = ( new Date().getTime() - _tracer.ts );
            _tracer.elem.html( 
                _tracer.elem.html() + now.toString() + ' ms: <strong>' + msg + '</strong><br>'
            );
            
            // keep scroll in bottom
            _tracer.elem[0].scrollTop = _tracer.elem[0].scrollHeight;
        }
    },
    
    trace = function() {
        $.each( Utils.array( arguments ), function(i, msg) {
            _tracer.log.call( _tracer, msg );
        })
    },
    
    // 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 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() { 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 ) ) {
                        log(this)
                        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() {
                            raise( 'Theme CSS could not load' );
                        },
                        timeout: 1000
                    });
                }
                return link;
            }
        };
    })();


// The private Picture class
// Adds preload, cache and scale functionality
var Picture = function( id ) {
    
        // save the id
        this.id = id || null;
    
        // the image should be null until loaded
        this.image = null;
    
        // Create a new container
        this.container = Utils.create('galleria-image');
    
        // add container styles
        $( this.container ).css({
            overflow: 'hidden',
            position: 'relative' // for IE Standards mode
        });
    
        // saves the original meassurements
        this.original = {
            width: 0,
            height: 0
        };
    
        // flag when the image is ready
        this.ready = false;
        
        // flag when the image is loaded
        this.loaded = false;
        
    };

Picture.prototype = {
    
    // the inherited cache object
    cache: {},
    
    // creates a new image and adds it to cache when loaded
    add: function( src ) {
        
        var self = this;

        // create the image
        var image = new Image();
        
        // force a block display
        $( image ).css( 'display', 'block');
        
        if ( self.cache[ src ] ) {
            // no need to onload if the image is cached
            image.src = src;
            self.loaded = true;
            self.original = {
                height: image.height,
                width: image.width
            };
            return image;
        }

        // begin preload and insert in cache when done
        image.onload = function() {
            self.original = {
                height: this.height,
                width: this.width
            };
            self.cache[ src ] = src; // will override old cache
            self.loaded = true;
        }
        
        image.src = src;
        return image;
        
    },
    
    // show the image on stage
    show: function() {
        Utils.show( this.image );
    },
    
    // hide the image
    hide: function() {
        Utils.moveOut( this.image );
    },
    
    clear: function() {
        this.image = null;
    },
    
    // checks if the image is cached
    isCached: function(src) {
        return !!this.cache[src];
    },
    
    // the load function
    // returns the jquery container object
    load: function(src, callback) {
        
        // save the instance
        var self = this;

        $( this.container ).empty(true);
        
        // add the image to cache and hide it
        this.image = this.add( src );
        Utils.hide( this.image );
        
        // append the image into the container
        $( this.container ).append( this.image );
        
        // check for loaded image using a timeout
        Utils.wait({
            until: function() {
                // TODO this should be properly tested in Opera
                return self.loaded && self.image.complete && self.image.width;
            },
            success: function() {
                // call success
                window.setTimeout(function() { callback.call( self, self ) }, 1 );
            },
            error: function() {
                callback.call( self, self );
                Galleria.raise('image not loaded in 5 seconds: '+ src);
            },
            timeout: 5000
        });
        
        // return the container
        return this.container;
    },
    
    // scales the image into it's container
    scale: function(options) {
        
        // extend some defaults
        options = $.extend({
            width: 0,
            height: 0,
            min: undef,
            max: undef,
            margin: 0,
            complete: function(){},
            position: 'center',
            crop: false
        }, options);
        
        // return the element if no image found
        if (!this.image) {
            return this.container;
        }
        
        // store locale variables of width & height
        var width,
            height,
            self = this,
            $container = $( self.container );
        
        // wait for the width/height
        Utils.wait({
            until: function() {
                
                width  = options.width
                    || $container.width()
                    || Utils.parseValue( $container.css('width') );
                    
                height = options.height
                    || $container.height()
                    || Utils.parseValue( $container.css('height') );

                return width && height;
            },
            success: function() {
                // calculate some cropping
                var newWidth = ( width - options.margin*2 ) / self.original.width,
                    newHeight = ( height- options.margin*2 ) / self.original.height,
                    cropMap = {
                        'true'  : Math.max( newWidth, newHeight ),
                        'width' : newWidth,
                        'height': newHeight,
                        'false' : Math.min( newWidth, newHeight )
                    },
                    ratio = cropMap[ options.crop.toString() ];

                // allow max_scale_ratio
                if ( options.max ) {
                    ratio = Math.min( options.max, ratio );
                }
                
                // allow min_scale_ratio
                if ( options.min ) {
                    ratio = Math.max( options.min, ratio );
                }
                
                $( self.container ).width( width ).height( height );
                
                // round up the width / height
                $.each( ['width','height'], function( i, m ) {
                    $( self.image )[ m ]( self[ m ] = Math.ceil( self.original[ m ] * ratio ) );
                });
                
                // calculate image_position
                var pos = {},
                    mix = {},
                    getPosition = function(value, meassure, margin) {
                        var result = 0;
                        if (/\%/.test(value)) {
                            var flt = parseInt(value) / 100;
                            result = Math.ceil( $( self.image )[ meassure ]() * -1 * flt + margin * flt );
                        } else {
                            result = Utils.parseValue( value );
                        }
                        return result;
                    },
                    positionMap = {
                        'top': { top: 0 },
                        'left': { left: 0 },
                        'right': { left: '100%' },
                        'bottom': { top: '100%' }
                    };
                
                $.each( options.position.toLowerCase().split(' '), function( i, value ) {
                    if ( value == 'center' ) {
                        value = '50%';
                    }
                    pos[i ? 'top' : 'left'] = value;
                });
                
                $.each( pos, function( i, value ) {
                    if ( positionMap.hasOwnProperty( value ) ) {
                        $.extend( mix, positionMap[ value ] );
                    }
                });
                
                pos = pos.top ? $.extend( pos, mix ) : mix;
            
                pos = $.extend({
                    top: '50%',
                    left: '50%'
                }, pos);
                
                // apply position
                $( self.image ).css({
                    position : 'relative',
                    top :  getPosition(pos.top, 'height', height),
                    left : getPosition(pos.left, 'width', width)
                });
                
                // show the image
                self.show();
                
                // flag ready and call the callback
                self.ready = true;
                options.complete.call( self, self );
            },
            error: function() {
                raise('Could not scale image: '+self.image.src);
            },
            timeout: 1000
        });
        return this;
    }
};

// The main Galleria class

var 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
    this._keyboard = {
        
        keys : {
            UP: 38,
            DOWN: 40,
            LEFT: 37,
            RIGHT: 39,
            RETURN: 13,
            ESCAPE: 27,
            BACKSPACE: 8
        },
        
        map : {},
        
        bound: false,
        
        press: function(e) {
            var key = e.keyCode || e.which;
            if ( key in self._keyboard.map && typeof self._keyboard.map[key] == 'function' ) {
                self._keyboard.map[key].call(self, e);
            }
        },
        
        attach: function(map) {
            for( var key in map ) {
                var up = key.toUpperCase();
                if ( up in this.keys ) {
                    this.map[ this.keys[up] ] = map[key];
                }
            }
            if ( !this.bound ) {
                this.bound = true;
                $doc.bind('keydown', this.press);
            }
        },
        
        detach: function() {
            this.bound = false;
            $doc.unbind('keydown', this.press);
        }
    };
        
    // internal controls for keeping track of active / inactive images
    this._controls = {
        
        0: undef,
        
        1: undef,
        
        active : 0,
        
        swap : function() {
            this.active = this.active ? 0 : 1;
        },
        
        getActive : function() {
            return this[this.active];
        },
        
        getNext : function() {
            return this[ 1 - this.active ];
        }
    };
    
    // internal carousel object
    // todo
    this._carousel = {
        
        // shortcuts
        next: this.$('thumb-nav-right'),
        prev: this.$('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
            });

            // use global context in case ov event triggering
            self._carousel.max = w;
            self._carousel.hooks = hooks;
            self._carousel.width = self.$( 'thumbnails-list' ).width();
            self._carousel.setClasses();

            // todo: fix so the carousel moves to the left
        },
        
        bindControls: function() {
            
            var carousel = this
            this.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);
                }
            });
            
            this.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 ( this.hooks[i-1] + this.width > this.max && i >= 0 ) {
                i--;
            }
            this.current = i;
            this.animate();
        },
        
        // get the last position
        getLast: function(i) {
            return ( i || this.current ) -1;
        },
        
        // follow the active image
        follow: function(i) {
            
            //don't follow if position fits
            if ( i == 0 || i == this.hooks.length-2 ) {
                this.set( i );
                return;
            }
            
            // calculate last position
            var last = this.current;
            while( this.hooks[last] - this.hooks[ this.current ] < this.width && last <= this.hooks.length) {
                last ++;
            }
            
            // set position
            if ( i-1 < this.current ) {
                this.set( i-1 );
            } else if ( i+2 > last) {
                this.set( i - last + this.current + 2 );
            }
        },
        
        // helper for setting disabled classes
        setClasses: function() {
            this.prev.toggleClass( 'disabled', !this.current );
            this.next.toggleClass( 'disabled', this.hooks[ this.current ] + this.width > this.max )
        },
        
        // the animation method    
        animate: function(to) {
            this.setClasses();
            var num = this.hooks[this.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.99
    // not implemented yet!
    this._tooltip = {
        
        initialized : false,
        
        active: null,
        
        open: false,
        
        init: function() {
            
            this.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;
            var 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 (! this.initialized ) {
                this.init();
            }
            
            var hover = function( elem, value) {
                
                self._tooltip.define( elem, value );
                
                $( elem ).hover(function() {

                    self._tooltip.active = elem;
                    Utils.clearTimer('switch_tooltip');
                    self.$('container').bind( 'mousemove', self._tooltip.move ).trigger( 'mousemove' );
                    self._tooltip.show( elem );
                    
                    Galleria.utils.addTimer( 'tooltip', function() {
                        
                        Utils.show( self.get( 'tooltip' ), 400 );
                        self._tooltip.open = true;
                        
                    }, self._tooltip.open ? 0 : 1000);
                    
                }, function() {
                    
                    self._tooltip.active = null;
                    
                    self.$( 'container' ).unbind( 'mousemove', self._tooltip.move );
                    Utils.clearTimer( 'tooltip' );
                    
                    Utils.hide( self.get( 'tooltip' ), 200, function() {
                        Utils.addTimer('switch_tooltip', function() {
                            self._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);
            
            self._tooltip.show( elem );

        },
        
        refresh: function() {
            $.each( arguments, function(i, elem) {
                if ( self._tooltip.active == elem ) {
                    self._tooltip.show( elem );
                }
            });
        }
    };
    
    // internal fullscreen control
    // added in 1.195
    // still kind of experimental
    this._fullscreen = {
        scrolled: 0,
        enter: function(callback) {
            
            // hide the image until rescale is complete
            Utils.hide( self.getActiveImage() );
            
            self.$( 'container' ).addClass( 'fullscreen' );
            
            this.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 );
                    }
                    
                }, 50);
                
                self.trigger( Galleria.FULLSCREEN_ENTER );
            });
            
            // bind the scaling to the resize event
            $(window).resize( function() {
                self._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, this.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', this.scale);
        }
    };
    
    // the internal idle object for controlling idle states
    // TODO occational event conflicts
    this._idle = {
        
        trunk: [],
        
        bound: false,
        
        add: function(elem, to) {
            if (!elem) {
                return;
            }
            if (!this.bound) {
                this.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
            });
            this.addTimer();
            this.trunk.push(elem);
        },
        
        remove: function(elem) {
            
            elem = jQuery(elem);
            
            $.each(this.trunk, function(i, el) {
                if ( el.length && !el.not(elem).length ) {
                    self._idle.show(elem);
                    self._idle.trunk.splice(i,1);
                }
            });
            
            if (!this.trunk.length) {
                this.removeEvent();
                Utils.clearTimer('idle');
            }
        },
        
        addEvent : function() {
            this.bound = true;
            self.$('container').bind('mousemove click', this.showAll );
        },
        
        removeEvent : function() {
            this.bound = false;
            self.$('container').unbind('mousemove click', this.showAll );
        },
        
        addTimer : function() {
            Utils.addTimer('idle', function() {
                self._idle.hide();
            }, self._options.idle_time );
        },
        
        hide : function() {
            self.trigger( Galleria.IDLE_ENTER );
            
            $.each(this.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;
                    }
                });
            }
            this.addTimer();
        }
    };
    
    // internal lightbox object
    // creates a predesigned lightbox for simple popups of images in galleria
    this._lightbox = {
        
        width : 0,
        
        height : 0,
        
        initialized : false,
        
        active : null,
        
        image : null,
        
        elems : {},
        
        init : function() {
            
            // trigger the event
            self.trigger( Galleria.LIGHTBOX_OPEN );
            
            if (this.initialized) {
                return;
            }
            this.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 ] = self._lightbox.elems[ elemId ] = self.get( 'lightbox-' + elemId );
            });

            // initiate the image
            this.image = new 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( this.image.container );
            
            $( DOM().body ).append( el.overlay, el.box );
            
            // add the prev/next nav and bind some controls

            hover( $( el.close ).bind( CLICK(), this.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() {
                    self._lightbox[ 'show' + dir ]()
                });
                
            })
            $( el.overlay ).bind( CLICK(), this.hide );

        },
        
        rescale: function(event) {
            
            var lightbox = self._lightbox;
            
            // 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( self._lightbox.elems.info, speed );
                    }
                );
            }
        },
        
        hide: function() {
            
            var lightbox = self._lightbox;
            
            // 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() {
            self._lightbox.show( self.getNext( self._lightbox.active ) );
        },
        
        showPrev: function() {
            self._lightbox.show( self.getPrev( self._lightbox.active ) );
        },
        
        show: function(index) {
            
            this.active = index = typeof index == 'number' ? index : self.getIndex();
            
            if (!this.initialized) {
                this.init();
            }
            
            $(window).unbind('resize', this.rescale );
            
            var data = self.getData(index),
                total = self.getDataLength();
            
            Utils.hide( this.elems.info );
            
            var lightbox = this;
            
            this.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();
        }
    };
};

// end Galleria constructor

Galleria.prototype = {
    
    // the public init() is used to assign target, options and a THEMELOAD event
    init: function( target, options ) {

        var self = this;
        
        // save the instance
        Galleria.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 ) {
             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() {
                raise( 'No theme found. ');
            }, 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 ) {
            raise( 'Init failed: Gallery instance already initialized.' );
            return this;
        }
        
        this._initialized = true;
        
        if ( !Galleria.theme ) {
            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] = $container[ m ]() || 
                                     Utils.parseValue( $container.css( m ) ) || 
                                     self.$( 'target' )[ m ]() ||
                                     Utils.parseValue( self.$( 'target' ).css( m ) );
                        }
                        
                        $container[m]( num[ 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;
                
                },
                
                success: function() {
                    
                    // remove the testElem
                    $( testElem ).remove();
                    
                    // for some strange reason, webkit needs a single setTimeout to play ball
                    if ( Galleria.WEBKIT ) {
                        window.setTimeout( function() {
                            self._run()
                        }, 1);
                    } else {
                        self._run();
                    }
                },
                error: function() {
                    raise('Width & Height not found.')
                },
                timeout: 2000
            });
        });
        
        // postrun some stuff after the gallery is ready
        // make sure it only runs once
        var one = false;
        
        this.bind( Galleria.READY, function() {
            
            // show counter
            Utils.show( this.get('counter') );
            
            // bind clicknext
            if ( this._options.clicknext ) {
                $.each( this._data, function( i, data ) {
                    delete data.link;
                });
                this.$( 'stage' ).css({ cursor : 'pointer' }).bind( CLICK(), function(e) {
                    self.next();
                });
            }
            
            // bind carousel nav
            if ( this._options.carousel ) {
                this._carousel.bindControls();
            }
            
            // start autoplay
            if ( this._options.autoplay ) {
                
                this.pause();
                
                if ( typeof this._options.autoplay == 'number' ) {
                    this._playtime = this._options.autoplay;
                }
                
                this.trigger( Galleria.PLAY );
                this._playing = true;
            }
            
            // if second load, just do the show and return
            if ( one ) {
                this.show( this._options.show );
                return;
            }
            
            one = true;

            // initialize the History plugin
            if ( Galleria.History ) {

                // bind the show method
                Galleria.History.change(function(e) {
                    
                    // grab history ID
                    var val = parseInt( e.value.replace( /\//, '' ) );
                    
                    // if ID is NaN, the user pressed back from the first image
                    // return to previous address
                    if (isNaN(val)) {
                        window.history.go(-1);
                    
                    // else show the image
                    } else {
                        self.show( val, undef, true );
                    }
                });
            }
            
            // call the theme init method
            Galleria.theme.init.call( this, this._options );
            
            // call the extend option
            this._options.extend.call( this, this._options );
            
            // show the initial image
            // first test for permalinks in history
            if ( /^[0-9]{1,4}$/.test( HASH ) && Galleria.History ) {
                this.show( HASH, undef, true );
                
            } else {
                this.show( this._options.show );
            }
            
        });
        
        // build the gallery frame
        this.append({
            'info-text' :
                ['info-title', 'info-description', 'info-author'],
            'info' : 
                ['info-text'],
            'image-nav' : 
                ['image-nav-right', 'image-nav-left'],
            'stage' : 
                ['images', 'loader', 'counter', 'image-nav'],
            'thumbnails-list' :
                ['thumbnails'],
            'thumbnails-container' : 
                ['thumb-nav-left', 'thumbnails-list', 'thumb-nav-right'],
            'container' : 
                ['stage', 'thumbnails-container', 'info', 'tooltip']
        });
        
        Utils.hide( this.$( 'counter' ).append(
            this.get( 'current' ),
            ' / ',
            this.get( 'total' )
        ) );
        
        this.setCounter('&#8211;');
        
        // add images to the controls
        $.each( new Array(2), function(i) {
            
            // create a new Picture instance
            var image = new Picture();
            
            // apply some styles
            $( image.container ).css({
                position: 'absolute',
                top: 0,
                left: 0
            });
            
            // append the image
            self.$( 'images' ).append( image.container );
            
            // reload the controls
            self._controls[i] = image;
            
        });
        
        // some forced generic styling
        this.$( 'images' ).css({
            position: 'relative',
            top: 0,
            left: 0,
            width: '100%',
            height: '100%'
        });
        
        this.$( 'thumbnails, thumbnails-list' ).css({
            overflow: 'hidden',
            position: 'relative'
        });
        
        // bind image navigation arrows
        this.$( 'image-nav-right, image-nav-left' ).bind( CLICK(), function(e) {
            
            // tune the clicknext option
            if ( self._options.clicknext ) {
                e.stopPropagation();
            }
            
            // pause if options is set
            if ( self._options.pause_on_interaction ) {
                self.pause();
            }
            
            // navigate
            var fn = /right/.test( this.className ) ? 'next' : 'prev';
            self[ fn ]();

        });
        
        // hide controls if chosen to
        $.each( ['info','counter','image-nav'], function( i, el ) {
            if ( self._options[ 'show_'+el.replace(/-/,'') ] === false ) {
                Utils.moveOut( self.get( el ) );
            }
        });
        
        // load up target content
        this.load();
        
        // now it's usually safe to remove the content
        // IE will never stop loading if we remove it, so let's keep it hidden for IE (it's usually fast enough anyway)
        if ( !this._options.keep_source && !IE ) {
            this._target.innerHTML = '';
        }
        
        // append the gallery frame
        this.$( 'target' ).append( this.get( 'container' ) );
        
        // parse the carousel on each thumb load
        if ( this._options.carousel ) {
            this.bind( Galleria.THUMBNAIL, function() {
                this.updateCarousel()
            });
        }
        
        // bind on_image helper
        this.bind( Galleria.IMAGE, function( e ) {
            this._options.on_image.call( this, e.imageTarget, e.thumbTarget );
        });
        
        return this;
    },
    
    // the internal _run method should be called after loading data into galleria 
    // creates thumbnails and makes sure the gallery has proper meassurements
    _run : function() {
        // shortcuts
        var self = this,
            o = this._options,
            
            // width/height for calculations
            width  = 0,
            height = 0,
            
            // cache the thumbnail option
            optval = typeof o.thumbnails == "string" ? o.thumbnails.toLowerCase() : null;
        
        // loop through data and create thumbnails
        for( var i=0; this._data[i]; i++ ) {
            
            var thumb,
                data = this._data[i],
                $container;
            
            if ( o.thumbnails === true ) {
                
                // add a new Picture instance
                thumb = new Picture(i);
                
                // get source from thumb or image
                var src = data.thumb || data.image;
                
                // append the thumbnail
                this.$( 'thumbnails' ).append( thumb.container );
                
                // cache the container
                $container = $( thumb.container );
                
                // move some data into the instance
                thumb.data = {
                    width  : Utils.parseValue( $container.css('width') ),
                    height : Utils.parseValue( $container.css('height') ),
                    order  : i
                };
                
                // grab & reset size for smoother thumbnail loads
                $container.css(( o.thumb_fit && o.thumb_crop !== true ) ?
                    { width: 0, height: 0 } :
                    { width: thumb.data.width, height: thumb.data.height });
                
                // load the thumbnail
                thumb.load( src, function( thumb ) {
                    
                    // scale when ready
                    thumb.scale({
                        width:    thumb.data.width,
                        height:   thumb.data.height,
                        crop:     o.thumb_crop,
                        margin:   o.thumb_margin,
                        complete: function( thumb ) {
                            
                            // shrink thumbnails to fit
                            var top = ['left', 'top'];
                            var arr = ['Width', 'Height'];
                            
                            // calculate shrinked positions
                            $.each(arr, function( i, meassure ) {
                                var m = meassure.toLowerCase();
                                if ( (o.thumb_crop !== true || o.thumb_crop == m ) && o.thumb_fit ) {
                                    var css = {};
                                    css[m] = thumb[m];
                                    $( thumb.container ).css( css )
                                    css = {};
                                    css[top[i]] = 0;
                                    $( thumb.image ).css( css);
                                }
                                
                                // cache outer meassures
                                thumb['outer' + meassure] = $( thumb.container )['outer' + meassure]( true );
                            });
                            
                            // set high quality if downscale is moderate
                            Utils.toggleQuality( thumb.image, 
                                o.thumb_quality === true || 
                                ( o.thumb_quality == 'auto' && thumb.original.width < thumb.width * 3 )
                            );
                            
                            // trigger the THUMBNAIL event
                            self.trigger({
                                type: Galleria.THUMBNAIL,
                                thumbTarget: thumb.image,
                                thumbOrder: thumb.data.order
                            });
                        }
                    });
                });
                
                // preload all images here
                if ( o.preload == 'all' ) {
                    thumb.add( data.image );
                }
            
            // create empty spans if thumbnails is set to 'empty'
            } else if ( optval == 'empty' || optval == 'numbers' ) {
                
                thumb = {
                    container:  Utils.create( 'galleria-image' ),
                    image: Utils.create( 'img', 'span' ),
                    ready: true
                };
                
                // create numbered thumbnails
                if ( optval == 'numbers' ) {
                    $( thumb.image ).text( i + 1 );
                }
                
                
                $( thumb.container ).append( thumb.image );
                this.$( 'thumbnails' ).append( thumb.container );
                
                self.trigger({
                    type: Galleria.THUMBNAIL,
                    thumbTarget: thumb.image,
                    thumbOrder: i
                });
            
            
            // create null object to silent errors
            } else {
                thumb = {
                    container: null,
                    image: null
                }
            }
            
            // add events for thumbnails
            // you can control the event type using thumb_event_type
            // we'll add the same event to the source if it's kept
            
            $( thumb.container ).add( o.keep_source && o.link_source_images ? data.original : null )
                .data('index', i).bind(o.thumb_event_type, function(e) {
                    // pause if option is set
                    if ( o.pause_on_interaction ) {
                        self.pause();
                    }
                    
                    // extract the index from the data
                    var index = $( e.currentTarget ).data( 'index' );
                    if ( self.getIndex() !== index ) {
                        self.show( index );
                    }
                    
                    e.preventDefault();
            });
            
            this._thumbnails.push( thumb );
        }
        
        // make sure we have a stageHeight && stageWidth
        
        Utils.wait({
            
            until: function() {
                self._stageWidth  = self.$( 'stage' ).width();
                self._stageHeight = self.$( 'stage' ).height();
                return( self._stageWidth && self._stageHeight > 50 ); // what is an acceptable height?
            },
            
            success: function() {
                self.trigger( Galleria.READY );
            },
            
            error: function() {
                raise('stageM not found')
            }
            
        })
    },
    
    // the public load method loads new data into galleria
    // triggers the DATA event when ready
    load : function( source, selector, config ) {
        
        var self = this;
        
        // empty the data array
        this._data = [];
        
        // empty the thumbnails
        this._thumbnails = [];
        this.$('thumbnails').empty();
        
        // shorten the arguments
        if ( typeof selector == 'function' ) {
            config = selector;
            selector = null;
        }

        // use the source set by target
        source = source || this._options.data_source;
        
        // use selector set by option
        selector = selector || this._options.data_selector;
        
        // use the data_config set by option
        config = config || this._options.data_config;
        
        // check if the data is an array already
        if ( source.constructor == Array ) {
            if ( this.validate( source) ) {
                this._data = source;
                this.trigger( Galleria.DATA );
            } else {
                raise( 'Load failed: JSON Array not valid.' );
            }
            return this;
        }
        // loop through images and set data
        $( source ).find( selector ).each( function( i, img ) {
            var data = {},
                img = $( img ),
                parent = img.parent(),
                href = parent.attr( 'href' );
            
            // check if it's a link to another image
            if ( /\.(png|gif|jpg|jpeg)$/i.test(href) ) {
                data.image = href;
            
            // else assign the href as a link if it exists
            } else if ( href ) {
                data.link = href;
            }
            
            // mix default extractions with the hrefs and config
            // and push it into the data array
            self._data.push( $.extend({
                
                title:       img.attr('title'),
                thumb:       img.attr('src'),
                image:       img.attr('src'),
                description: img.attr('alt'),
                link:        img.attr('longdesc'),
                original:    img.get(0) // saved as a reference
                
            }, data, config( img ) ) );
            
        });
        // trigger the DATA event and return
        if ( this.getDataLength() ) {
            this.trigger( Galleria.DATA );
        } else {
            raise('Load failed: no data found.');
        }
        return this;
        
    },
    
    _getActive: function() {
        return this._controls.getActive();
    },
    
    validate : function( data ) {
        // todo: validate a custom data array
        return true;
    },
    
    bind : function(type, fn) {
        this.$( 'container' ).bind( type, this.proxy(fn) );
        return this;
    },
    
    unbind : function(type) {
        this.$( 'container' ).unbind( type );
    },
    
    trigger : function( type ) {
        type = typeof type == 'object' ? 
            $.extend( type, { scope: this } ) : 
            { type: type, scope: this };
        this.$( 'container' ).trigger( type );
        return this;
    },
    
    addIdleState: function() {
        this._idle.add.apply( this._idle, Utils.array( arguments ) );
        return this;
    },
    
    removeIdleState: function() {
        this._idle.remove.apply( this._idle, Utils.array( arguments ) );
        return this;
    },
    
    enterIdleMode: function() {
        this._idle.hide();
        return this;
    },
    
    exitIdleMode: function() {
        this.idle._show();
        return this;
    },
    
    enterFullscreen: function() {
        this._fullscreen.enter.apply( this, Utils.array( arguments ) );
        return this;
    },
    
    exitFullscreen: function() {
        this._fullscreen.exit.apply( this, Utils.array( arguments ) );
        return this;
    },
    
    bindTooltip: function() {
        this._tooltip.bind.apply( this._tooltip, Utils.array(arguments) );
    },
    
    defineTooltip: function() {
        this._tooltip.define.apply( this._tooltip, Utils.array(arguments) );
    },
    
    refreshTooltip: function() {
        this._tooltip.refresh.apply( this._tooltip, Utils.array(arguments) );
    },
    
    openLightbox: function() {
        this._lightbox.show.apply( this._lightbox, Utils.array( arguments ) );
    },
    
    closeLightbox: function() {
        this._lightbox.hide.apply( this._lightbox, Utils.array( arguments ) );
    },
    
    getActiveImage: function() {
        return this._getActive().image || undef;
    },
    
    getActiveThumb: function() {
        return this._thumbnails[ this._active ].image || undef;
    },
    
    getMousePosition : function(e) {
        return {
            x: e.pageX - this.$( 'stage' ).offset().left,
            y: e.pageY - this.$( 'stage' ).offset().top
        };
    },
    
    // the public addPan method adds a panning effect to the image
    addPan : function( img ) {

        if ( this._options.image_crop === false ) {
            return;
        }
        
        img = $( img || this.getActiveImage() );
        
        // define some variables and methods
        var self = this,
            x = img.width() / 2,
            y = img.height() / 2,
            curX = destX = parseInt( img.css( 'left' ) ) || 0,
            curY = destY = parseInt( img.css( 'top' ) ) || 0,
            distX = 0,
            distY = 0,
            active = false,
            ts = Utils.timestamp(),
            cache = 0,
            move = 0,
            
            // positions the image
            position = function( dist, cur, pos ) {
                if ( dist > 0 ) {
                    move = Math.round( Math.max( dist * -1, Math.min( 0, cur ) ) );
                    if ( cache != move ) {
                        
                        cache = move;
                        
                        if ( IE == 8 ) { // scroll is faster for IE
                            img.parent()[ 'scroll' + pos ]( move * -1 );
                        } else {
                            var css = {};
                            css[ pos.toLowerCase() ] = move;
                            img.css(css);
                        }
                    }
                }  
            },
            
            // calculates mouse position after 50ms
            calculate = function(e) {
                if (Utils.timestamp() - ts < 50) {
                    return;
                }
                active = true;
                x = self.getMousePosition(e).x;
                y = self.getMousePosition(e).y;
            },
            
            // the main loop to check
            loop = function(e) {
                
                if (!active) {
                    return;
                }

                distX = img.width() - self._stageWidth;
                distY = img.height() - self._stageHeight;
                destX = x / self._stageWidth * distX * -1;
                destY = y / self._stageHeight * distY * -1;
                curX += ( destX - curX ) / self._options.image_pan_smoothness;
                curY += ( destY - curY ) / self._options.image_pan_smoothness;
                
                position( distY, curY, 'Top' );
                position( distX, curX, 'Left' );

            };
        
        // we need to use scroll in IE8 to speed things up
        if ( IE == 8 ) {
            
            img.parent().scrollTop( curY * -1 ).scrollLeft( curX * -1 );
            img.css({ 
                top: 0, 
                left: 0 
            });
            
        }
        
        // unbind and bind event
        this.$( 'stage' ).unbind( 'mousemove', calculate ).bind( 'mousemove', calculate );
        
        // loop the loop
        Utils.addTimer('pan', loop, 50, true);
        
        return this;
    },
    
    proxy : function( fn, scope ) {
        if ( typeof fn !== 'function' ) {
            return function() {};
        }
        scope = scope || this;
        return function() {
            return fn.apply( scope, Utils.array( arguments ) );
        };
    },
    
    removePan: function() {
        
        if ( IE == 8 ) {
            // todo: doublecheck this
        }
        this.$( 'stage' ).unbind( 'mousemove' );
        
        Utils.clearTimer('pan');
        
        this.rescale();
        
        return this;
    },
    
    addElement : function() {
        
        var dom = this._dom;
        
        $.each( Utils.array(arguments), function( i, blueprint ) {
           dom[ blueprint ] = Utils.create( 'galleria-' + blueprint );
        });
        
        return this;
    },
    
    attachKeyboard : function() {
        this._keyboard.attach.apply( this._keyboard, Utils.array( arguments ) );
        return this;
    },
    
    detachKeyboard : function() {
        this._keyboard.detach.apply( this._keyboard, Utils.array( arguments ) );
        return this;
    },
    
    // Fast helper for appending galleria elements by blueprint
    appendChild : function( parent, child ) {
        this.$( parent ).append( this.get( child ) || child );
        return this;
    },
    
    // Fast helper for prepending galleria elements by blueprint
    prependChild : function( parent, child ) {
        this.$( parent ).prepend( this.get( child ) || child );
        return this;
    },
    
    // Remove galleria elements by blueprint
    remove : function() {
        this.$( Utils.array( arguments ).join(',') ).remove();
        return this;
    },
    
    // a fast helper for building dom structures
    append : function(data) {
        for( var i in data) {
            if ( data[i].constructor == Array ) {
                for( var j=0; data[i][j]; j++ ) {
                    this.appendChild( i, data[i][j] );
                }
            } else {
                this.appendChild( i, data[i] );
            }
        }
        return this;
    },
    
    // an internal helper for scaling according to options
    _scaleImage : function( image, options ) {
        
        options = $.extend({
            width:    this._stageWidth, 
            height:   this._stageHeight, 
            crop:     this._options.image_crop, 
            max:      this._options.max_scale_ratio,
            min:      this._options.min_scale_ratio,
            margin:   this._options.image_margin,
            position: this._options.image_position
        }, options );
        
       ( image || this._controls.getActive() ).scale( options );
        
        return this;
    },
    
    updateCarousel : function() {
        this._carousel.update();
    },
    
    // A public method for rescaling the gallery
    rescale : function( width, height, callback ) {
        
        var self = this;
        
        // allow rescale(fn)
        if ( typeof width == 'function' ) {
            callback = width;
            width = undef;
        }
        
        var scale = function() {
            
            // shortcut
            var o = self._options;
            
            // set stagewidth
            self._stageWidth = width || self.$( 'stage' ).width();
            self._stageHeight = height || self.$( 'stage' ).height();
            
            // scale the active image
            self._scaleImage();
            
            if ( self._options.carousel ) {
                self.updateCarousel();
            }
            
            self.trigger( Galleria.RESCALE );
            
            if ( typeof callback == 'function' ) {
                callback.call( self );
            }
        };
        
        if ( Galleria.WEBKIT && !width && !height ) {
            Utils.addTimer( 'scale', scale, 5 );// webkit is too fast
        } else {
            scale.call( self ); 
        }
    },
    
    // the public proxy for displaying the image
    // adds the image to a transition queue if set
    show : function( index, rewind, history ) {
        // do nothing if queue is false and transition is in progress
        if ( !this._options.queue && this._queue.stalled ) {
            return;
        }
        index = Math.max( 0, Math.min( parseInt(index), this.getDataLength() - 1 ) );
        
        rewind = typeof rewind != 'undefined' ? !!rewind : index < this.getIndex();
        
        history = history || false;
        
        // do the history thing and return
        if ( !history && Galleria.History ) {
            Galleria.History.value( index.toString() );
            return;
        }
        
        this._active = index;
        
        Array.prototype.push.call( this._queue, {
            index : index,
            rewind : rewind
        });
        if ( !this._queue.stalled ) {
            this._show();
        }
        
        return this;
    },
    
    // the internal _show method does the actual showing
    _show : function() {

        // shortcuts
        var self = this,
            queue = this._queue[ 0 ],
            data = this.getData( queue.index ),
            src = data.image,
            active = this._controls.getActive(),
            next = this._controls.getNext(),
            cached = next.isCached( src ),
            thumb = this._thumbnails[ queue.index ];
            
            // to be fired when loading & transition is complete:
        var complete = function() {
            
            // remove stalled
            self._queue.stalled = false;
            
            // optimize quality
            Utils.toggleQuality( next.image, self._options.image_quality );
            
            // swap
            $( active.container ).css({
                zIndex: 0,
                opacity: 0
            });
            $( next.container ).css({
                zIndex: 1,
                opacity: 1
            });
            self._controls.swap();
            
            // add pan according to option 
            if ( self._options.image_pan ) {
                self.addPan( next.image );
            }
            
            // make the image link
            if ( data.link ) {
                $( next.image ).css({
                    cursor: 'pointer'
                }).bind( CLICK(), function() {
                    
                    // popup link
                    if ( self._options.popup_links ) {
                        var win = window.open( data.link, '_blank' );
                    } else {
                        window.location.href = data.link;
                    }
                });
            }
            
            // remove the queued image
            Array.prototype.shift.call( self._queue );
            
            // if we still have images in the queue, show it
            if ( self._queue.length ) {
                self._show();
            }
            
            // check if we are playing
            self._playCheck();
            
            // trigger IMAGE event
            self.trigger({
                type:        Galleria.IMAGE,
                index:       queue.index,
                imageTarget: next.image,
                thumbTarget: thumb.image
            });
        };
        
        // let the carousel follow
        if ( this._options.carousel && this._options.carousel_follow ) {
            this._carousel.follow( queue.index );
        }
        
        // preload images
        if ( this._options.preload ) {
            
            var p,
                n = this.getNext();
                
            try {
                for ( var i = this._options.preload; i > 0; i-- ) {
                    p = new Picture();
                    p.add( self.getData( n ).image );
                    n = self.getNext( n );
                }
            } catch(e) {}
        }
        
        // show the next image, just in case
        Utils.show( next.container );
        
        // add active classes
        $( self._thumbnails[ queue.index ].container )
            .addClass( 'active' )
            .siblings( '.active' )
            .removeClass( 'active' );
        
        // trigger the LOADSTART event
        self.trigger( {
            type: Galleria.LOADSTART,
            cached: cached,
            index: queue.index,
            imageTarget: next.image,
            thumbTarget: thumb.image
        });
        
        // begin loading the next image
        next.load( src, function( next ) {
            self._scaleImage( next, {
                
                complete: function( next ) {

                    Utils.show( next.container );
                    
                    // toggle low quality for IE
                    if ( 'image' in active ) {
                        Utils.toggleQuality( active.image, false );
                    }
                    Utils.toggleQuality( next.image, false );
                    
                    // stall the queue
                    self._queue.stalled = true;
                    
                    // remove the image panning, if applied
                    self.removePan();
                    
                    // set the captions and counter
                    self.setInfo( queue.index );
                    self.setCounter( queue.index );
                    
                    // trigger the LOADFINISH event
                    self.trigger({
                        type: Galleria.LOADFINISH,
                        cached: cached,
                        index: queue.index,
                        imageTarget: next.image,
                        thumbTarget: self._thumbnails[ queue.index ].image
                    });
                    
                    var transition = active.image === null && self._options.transition_initial ? 
                        self._options.transition_initial : self._options.transition;
                    
                    // validate the transition
                    if ( transition in Galleria.transitions === false ) {
                        
                        complete();
                    
                    } else {
                        var params = {
                            prev:   active.image,
                            next:   next.image,
                            rewind: queue.rewind,
                            speed:  self._options.transition_speed || 400
                        };
                        
                        // call the transition function and send some stuff
                        Galleria.transitions[ transition ].call(self, params, complete );
                        
                    }
                }
            });
        });
    },
    
    // getNext() returns the next index, or he first if you are at the last
    // base is optional and defines where to start looking
    getNext : function( base ) {
        base = typeof base == 'number' ? base : this.getIndex();
        return base == this.getDataLength() - 1 ? 0 : base + 1;
    },
    
    // getPrev() returns the previous index, or he last if you are at the first
    // base is optional and defines where to start looking
    getPrev : function( base ) {
        base = typeof base == 'number' ? base : this.getIndex();
        return base === 0 ? this.getDataLength() - 1 : base - 1;
    },
    
    // next() shows the next image in line
    next : function() {
        if ( this.getDataLength() > 1 ) {
            this.show( this.getNext(), false );
        }
        return this;
    },
    
    // prev() shows the previous image in line
    prev : function() {
        if ( this.getDataLength() > 1 ) {
            this.show( this.getPrev(), true );
        }
        return this;
    },
    
    // get() returns the internal DOM element
    get : function( elemId ) {
        return elemId in this._dom ? this._dom[ elemId ] : null;
    },
    
    // getData() returns the data object
    // if no index specified it will take the currently active image
    getData : function( index ) {
        return index in this._data ? 
            this._data[ index ] : this._data[ this._active ];
    },
    
    // getDataLength() returns the length of the data object
    getDataLength : function() {
        return this._data.length;
    },
    
    // getIndex() returns the current index
    getIndex : function() {
        return typeof this._active === 'number' ? this._active : 0;
    },
    
    // get the stage height
    getStageHeight : function() {
        return this._stageHeight;
    },
    
    // get the stage width
    getStageWidth : function() {
        return this._stageWidth;
    },
    
    // get options
    // if no key specified it will return all options
    getOptions : function( key ) {
        return typeof key == 'undefined' ? this._options : this._options[ key ];
    },
    
    // set options
    // you can set options using setOptions( 'autoplay', true ) or setOptions({ autoplay: true });
    setOptions : function( key, value ) {
        if ( typeof key == 'object' ) {
            $.extend( this._options, key );
        } else {
            this._options[ key ] = value;
        }
    },
    
    play : function(delay) {
        
        this.trigger( Galleria.PLAY );
        
        this._playing = true;
        this._playtime = delay || this._playtime;
        
        this._playCheck();
        
        return this;
    },
    
    pause : function() {
        this.trigger( Galleria.PAUSE );
        this._playing = false;
        return this;
    },
    
    _playCheck : function() {
        var self = this,
            played = 0,
            interval = 20,
            now = Utils.timestamp();
            
        if ( this._playing ) {
            
            Utils.clearTimer('play');
            var fn = function() {
                
                played = Utils.timestamp() - now;
                if ( played >= self._playtime && self._playing ) {
                    Utils.clearTimer('play');
                    self.next();
                    return;
                }
                if ( self._playing ) {
                    
                    // trigger the PROGRESS event
                    self.trigger({
                        type:         Galleria.PROGRESS,
                        percent:      Math.ceil( played / self._playtime * 100 ),
                        seconds:      Math.floor( played / 1000 ),
                        milliseconds: played
                    });
                    
                    Utils.addTimer( 'play', fn, interval );
                }
            };
            Utils.addTimer( 'play', fn, interval );
        }
    },
    
    // manually set another index
    setIndex: function( val ) {
        this._active = val;
        return this;
    },
    
    // manually modify the counter
    setCounter: function( index ) {

        if ( typeof index == 'number' ) {
            index++;
        } else if ( typeof index == 'undefined' ) {
            index = this.getIndex()+1;
        }

        this.get( 'current' ).innerHTML = index;
        
        if ( IE == 8 ) { // weird IE8 bug
            
            var opacity = this.$( 'counter' ).css( 'opacity' );
            this.$( 'counter' ).css( 'opacity', opacity );
            
        }
        
        return this;
    },
    
    // manually set captions
    setInfo : function( index ) {
        
        var self = this,
            data = this.getData( index );
        
        $.each( ['title','description','author'], function( i, type ) {
            
            var elem = self.$( 'info-' + type );
            
            if ( !!data[type] ) {
                elem[ data[ type ].length ? 'show' : 'hide' ]().html( data[ type ] );
            } else {
               elem.empty().hide();
            }
        });
        
        return this;
    },
    
    // check if the data contains any captions
    hasInfo : function( index ) {
        
        var d = this.getData( index );
        var check = 'title description'.split(' ');
        for ( var i=0; check[i]; i++ ) {
            if ( !!this.getData( index )[ check[i] ] ){
                return true;
            }
        }
        return false;
        
    },
    
    // returns a jQuery object of elements based on elemId
    // you can call for multiple IDs separated with commas
    jQuery : function( str ) {
        
        var self = this,
            ret = [];
        
        $.each( str.split(','), function( i, elemId ) {
            elemId = $.trim( elemId );
            
            if ( self.get( elemId ) ) {
                ret.push( elemId );
            }
        });
        
        var jQ = $( self.get( ret.shift() ) );
        
        $.each( ret, function( i, elemId ) {
            jQ = jQ.add( self.get( elemId ) );
        })

        return jQ;
    },
    
    // shortcut for jQuery
    $ : function() {
        return this.jQuery.apply( this, Utils.array( arguments ) );
    }

};

// End of Galleria prototype

// Begin statics

$.extend( Galleria, {
    
    // Events
    DATA:             'data',
    READY:            'ready',
    THUMBNAIL:        'thumbnail',
    LOADSTART:        'loadstart',
    LOADFINISH:       'loadfinish',
    IMAGE:            'image',
    THEMELOAD:        'themeload',
    PLAY:             'play',
    PAUSE:            'pause',
    PROGRESS:         'progress',
    FULLSCREEN_ENTER: 'fullscreen_enter',
    FULLSCREEN_EXIT:  'fullscreen_exit',
    IDLE_ENTER:       'idle_enter',
    IDLE_EXIT:        'idle_exit',
    RESCALE:          'rescale',
    LIGHTBOX_OPEN:    'lightbox_open',
    LIGHTBOX_CLOSE:   'lightbox_close',
    LIGHTBOX_IMAGE:   'lightbox_image',
    
    // Browser helpers
    IE9:     IE == 9,
    IE8:     IE == 8,
    IE7:     IE == 7,
    IE6:     IE == 6,
    IE:      !!IE,
    WEBKIT:  /webkit/.test( NAV ),
    SAFARI:  /safari/.test( NAV ),
    CHROME:  /chrome/.test( NAV ),
    QUIRK:   ( IE && doc.compatMode && doc.compatMode == "BackCompat" ),
    MAC:     /mac/.test( navigator.platform.toLowerCase() ),
    OPERA:   !!window.opera,
    IPHONE:  /iphone/.test( NAV ),
    IPAD:    /ipad/.test( NAV ),
    ANDROID: /android/.test( NAV ),
    MOBILE:  !!( /iphone/.test( NAV ) || /ipad/.test( NAV ) || /android/.test( NAV ) ),
    
    // addTheme
    addTheme: function( theme ) {

        // make sure we have a name
        if ( !!theme['name'] === false ) {
            raise('No theme name specified');
        }
        
        if ( typeof theme.defaults != 'object' ) {
            theme.defaults = {};
        }
        
        if ( typeof theme.css == 'string' ) {
            
            var css;
            
            // look for the absolute path
            $('script').each(function( i, script ) {
                
                // look for the theme script
                var reg = new RegExp( 'galleria\\.' + theme.name.toLowerCase() + '\\.' );
                if( reg.test( script.src )) {

                    // we have a match
                    css = script.src.replace(/[^\/]*$/, "") + theme.css;

                    Utils.addTimer( "css", function() {
                        Utils.loadCSS( css, 'galleria-theme', function() {
                            Galleria.theme = theme;
                            $doc.trigger( Galleria.THEMELOAD );
                        });
                    }, 1);

                }
            });
            
            if ( !css ) {
                raise('No theme CSS loaded');
            }
        }
        return theme;
    },
    
    // loadTheme
    loadTheme: function( src, options ) {
        
        var loaded = false,
            length = Galleria.galleries.length;
        
        // first clear the current theme, if exists
        Galleria.theme = undef;
        
        // load the theme
        Utils.loadScript( src, function() {
            loaded = true;
        } );

        // set a 1 sec timeout, then display a hard error if no theme is loaded
        Utils.wait({
            until: function() {
                return loaded;
            },
            error: function() {
                raise( "Theme at " + src + " could not load, check theme path.", true )
            },
            success: function() {
                
                // check for existing galleries and reload them with the new theme
                if ( length ) {
                
                    // temporary save the new galleries
                    var refreshed = [];

                    // refresh all instances
                    // when adding a new theme to an existing gallery, all options will be resetted but the data will be kept
                    // you can apply new options as a second argument
                    $.each( Galleria.get(), function(i, instance) {

                        // mix the old data and options into the new instance
                        var op = $.extend( instance._original.options, {
                            data_source: instance._data
                        }, options);

                        // remove the old container
                        instance.$('container').remove();

                        // create a new instance 
                        var g = new Galleria();

                        // move the id
                        g._id = instance._id;

                        // initialize the new instance
                        g.init( instance._original.target, op );

                        // push the new instance
                        refreshed.push( g );
                    });

                    // now overwrite the old holder with the new instances
                    Galleria.galleries = refreshed;
                }
            },
            timeout: 1000
        });
    },
    
    // array of instances
    galleries: [],
    
    // get any instance
    get: function( index ) {
        if ( !!this.galleries[ index ] ) {
            return this.galleries[ index ];
        } else if ( typeof index !== 'number' ) {
            return this.galleries;
        } else {
            raise('Gallery index ' + index + ' not found');
        }
    },
    
    // transitions object
    transitions: {
        
        fade:      function(params, complete) {
            jQuery(params.next).css('opacity',0).show().animate({
                opacity: 1
            }, params.speed, complete);

            if (params.prev) {
                jQuery(params.prev).css('opacity',1).show().animate({
                    opacity: 0
                }, params.speed);
            }
        },
        
        flash:     function(params, complete) {
            jQuery(params.next).css('opacity',0);
            if (params.prev) {
                jQuery(params.prev).animate({
                    opacity: 0
                }, (params.speed/2), function() {
                    jQuery(params.next).animate({
                        opacity: 1
                    }, params.speed, complete);
                });
            } else {
                jQuery(params.next).animate({
                    opacity: 1
                }, params.speed, complete);
            }
        },
        
        pulse:     function(params, complete) {
            if (params.prev) {
                $(params.prev).hide();
            }
            jQuery(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");
            
            if (params.prev) {
                x = Utils.parseValue( $(params.prev).css("left") );
                $(params.prev).css({
                    opacity: 1,
                    left: x
                }).animate({
                    opacity: 0,
                    left: x + ( 50 * ( params.rewind ? 1 : -1 ) )
                },{
                    duration: params.speed,
                    queue: false,
                    easing: easing
                });
            }
            
            x = Utils.parseValue( $(params.next).css("left") );
            
            $(params.next).css({
                left: x + ( 50 * ( params.rewind ? -1 : 1 ) ), 
                opacity: 0
            }).animate({
                opacity: 1,
                left: x
            }, {
                duration: params.speed,
                complete: complete,
                queue: false,
                easing: easing
            });
        }
    },
    
    // add your own transition
    addTransition: function( name, fn ) {
        this.transitions[name] = fn;
    },

    // bring the utils
    utils: Utils,
    
    // bring the Picture class
    Picture: Picture,
    
    // bring the log
    log: log,
    
    // bring raise
    raise: raise
    
});

// our own easings
$.extend( $.easing, {
    galleria: function (_, t, b, c, d) {
        if ((t/=d/2) < 1) { 
            return c/2*t*t*t*t + b;
        }
        return -c/2 * ((t-=2)*t*t*t - 2) + b;
    },
    galleriaIn: function (_, t, b, c, d) {
		return c*(t/=d)*t*t*t + b;
	},
	galleriaOut: function (_, t, b, c, d) {
		return -c * ((t=t/d-1)*t*t*t - 1) + b;
	}
});

// the plugin initializer
$.fn.galleria = function( options ) {
    
    return this.each(function() {
        
        var gallery = new Galleria();
        gallery.init( this, options )

    });
};

// expose Galleria
window.Galleria = Galleria;

// phew

})( jQuery );
