/**
Script: Slideshow.js
  Slideshow - A javascript class for Mootools to stream and animate the presentation of images on your website.

License:
  MIT-style license.

Copyright:
  Copyright (c) 2011 [Aeron Glemann](http://www.electricprism.com/aeron/).

Dependencies:
  Mootools 1.3.1 Core: Fx.Morph, Fx.Tween, Selectors, Element.Dimensions.
  Mootools 1.3.1.1 More: Assets.
*/
(function(){
  WhenPaused = 1 << 0;
  WhenPlaying = 1 << 1;
  OnStart = 1 << 2;
  
  Slideshow = new Class({
    Implements: [Chain, Events, Options],

    options: {/*
      onComplete: $empty,
      onEnd: $empty,
      onStart: $empty,*/
        accesskeys: {'first': {'key': 'shift left', 'label': 'Shift + Leftwards Arrow'}, 'prev': {'key': 'left', 'label': 'Leftwards Arrow'}, 'pause': {'key': 'p', 'label': 'P'}, 'next': {'key': 'right', 'label': 'Rightwards Arrow'}, 'last': {'key': 'shift right', 'label': 'Shift + Rightwards Arrow'}},
      captions: true,
      center: true,
      classes: [/*'slideshow', 'first', 'prev', 'play', 'pause', 'next', 'last', 'images', 'captions', 'controller', 'thumbnails', 'hidden', 'visible', 'inactive', 'active', 'loader'*/],
      controller: true,
      data: null,
      delay: 2000,
      duration: 1000,
      fast: false,
      height: false,
      href: '',
      hu: '',
      linked: false,
      loader: true,
      loop: true,
      match: /\?slide=(\d+)$/,
      overlap: true,
      paused: false,
      random: false,
      replace: [/(\.[^\.]+)$/, 't$1'],
      resize: 'fill',
      slide: 0,
      thumbnails: true,
      titles: false,
      transition: 'sine:in:out',
      width: false
    },

  /**
  Constructor: initialize
    Creates an instance of the Slideshow class.

  Arguments:
    element - (element) The wrapper element.
    data - (array or object) The images and optional thumbnails, captions and links for the show.
    options - (object) The options below.

  Syntax:
    var myShow = new Slideshow(element, data, options);
  */

    initialize: function(el, data, options){
      this.setOptions(options);
      this.el = document.id(el);
      if (!this.el) 
        return;
      var match = window.location.href.match(this.options.match);
      this.slide = this._slide = this.options.match && match ? match[1].toInt() 
        : this.options.slide;
      this.counter = this.timeToNextTransition = this.timeToTransitionComplete = 0;
      this.direction = 'left';
      this.cache = {};
      this.paused = false;
      if (!this.options.overlap)
        this.options.duration *= 2;
      var anchor = this.el.getElement('a') || new Element('a');
      if (!this.options.href)
        this.options.href = anchor.get('href') || '';
      if (this.options.hu.length && !this.options.hu.test(/\/$/)) 
        this.options.hu += '/';
      if (this.options.fast === true)
        this.options.fast = WhenPaused | WhenPlaying;

      // styles

      var keys = 'slideshow first prev play pause next last images captions controller thumbnails hidden visible inactive active loader'.split(' '),
        values = keys.map(function(key, i){
          return this.options.classes[i] || key;
        }, this);
      this.classes = values.associate(keys);
      this.classes.get = function(){
        var str = '.' + this.slideshow;
        for (var i = 0, l = arguments.length; i < l; i++)
          str += '-' + this[arguments[i]];
        return str;
      }.bind(this.classes);

      // data  
      
      if (!data){
        this.options.hu = '';
        data = {};
        var thumbnails = this.el.getElements(this.classes.get('thumbnails') + ' img');
        this.el.getElements(this.classes.get('images') + ' img').each(function(img, i){
          var src = img.src,
            caption = img.alt || img.title,
            href = img.getParent().href,
            thumbnail = thumbnails[i] ? thumbnails[i].src 
              : '';
          data[src] = {'caption': caption, 'href': href, 'thumbnail': thumbnail};
        });
      }
      var loaded = this.load(data);
      if (!loaded)
        return; 

      // events

      this.events = {};
      this.events.push = function(type, fn){
        if (!this[type])
          this[type] = [];
        this[type].push(fn);
        document.addEvent(type, fn);
        return this;
      }.bind(this.events);

      this.accesskeys = {};
      for (action in this.options.accesskeys){
        var obj = this.options.accesskeys[action];
        this.accesskeys[action] = accesskey = {'label': obj.label};
        ['shift', 'control', 'alt'].each(function(modifier){
          var re = new RegExp(modifier, 'i');
          accesskey[modifier] = obj.key.test(re);
          obj.key = obj.key.replace(re, '');
        });
        accesskey.key = obj.key.trim();
      }

      this.events.push('keyup', function(e){
        Object.each(this.accesskeys, function(accesskey, action){
          if (e.key == accesskey.key && e.shift == accesskey.shift && e.control == accesskey.control && e.alt == accesskey.alt)
            this[action]();
        }, this);      
      }.bind(this));   

      // required elements

      var el = this.el.getElement(this.classes.get('images')),
        img = this.el.getElement('img') || new Element('img'),
        images = el ? el.empty() 
          : new Element('div', {'class': this.classes.get('images').substr(1)}).inject(this.el),
        div = images.getSize();
      this.height = this.options.height || div.y;    
      this.width = this.options.width || div.x;
      images.set({'styles': {'height': this.height, 'width': this.width}});
      this.el.store('images', images);
      this.a = this.image = img;
      if (Browser.ie && Browser.version >= 7)
        this.a.style.msInterpolationMode = 'bicubic';
      this.a.set('styles', {'display': 'none'});
      this.b = this.a.clone();
      [this.a, this.b].each(function(img){
        anchor.clone().cloneEvents(anchor).grab(img).inject(images);
      });

      // optional elements

      this.options.captions && new Caption(this);
      this.options.controller && new Controller(this);
      this.options.loader && new Loader(this);
      this.options.thumbnails && new Thumbnails(this);

      // begin show

      this._preload(this.options.fast & OnStart);
    },

  /**
  Public method: go
    Jump directly to a slide in the show.

  Arguments:
    n - (integer) The index number of the image to jump to, 0 being the first image in the show.

  Syntax:
    myShow.go(n);  
  */

    go: function(n, direction){
      var nextSlide = (this.slide + this.data.images.length) % this.data.images.length;
      if (n == nextSlide || Date.now() < this.timeToTransitionComplete)
        return;    
      clearTimeout(this.timer);
      this.timeToNextTransition = 0;    
      this.direction = direction ? direction 
        : n < this._slide ? 'right' 
        : 'left';
      this.slide = this._slide = n;
      if (this.preloader) 
        this.preloader = this.preloader.destroy();
      this._preload((this.options.fast & WhenPlaying) || (this.paused && this.options.fast & WhenPaused));
    },

  /**
  Public method: first
    Goes to the first image in the show.

  Syntax:
    myShow.first();  
  */

    first: function(){
      this.prev(true); 
    },

  /**
  Public method: prev
    Goes to the previous image in the show.

  Syntax:
    myShow.prev();  
  */

    prev: function(first){
      var n = 0;
      if (!first){
        if (this.options.random){
          if (this.showed.i < 2)
            return;
          this.showed.i -= 2;
          n = this.showed.array[this.showed.i];
        }
        else
          n = (this.slide - 1 + this.data.images.length) % this.data.images.length;                  
      }
      this.go(n, 'right');
    },

  /**
  Public method: pause
    Toggles play / pause state of the show.

  Arguments:
    p - (undefined, 1 or 0) Call pause with no arguments to toggle the pause state. Call pause(1) to force pause, or pause(0) to force play.

  Syntax:
    myShow.pause(p);  
  */

    pause: function(p){
      if (p != undefined)
        this.paused = p ? false 
          : true;
      if (this.paused){ // play
        this.paused = false;
        this.timeToTransitionComplete = Date.now() + this.timeToTransitionComplete;    
        this.timer = this._preload.delay(50, this);
        [this.a, this.b].each(function(img){
          ['morph', 'tween'].each(function(p){
            if (this.retrieve(p)) this.get(p).resume();
          }, img);
        });
        this.controller && this.el.retrieve('pause').getParent().removeClass(this.classes.play);
      } 
      else { // pause
        this.paused = true;
        this.timeToTransitionComplete = this.timeToTransitionComplete - Date.now();
        clearTimeout(this.timer);
        [this.a, this.b].each(function(img){
          ['morph', 'tween'].each(function(p){
            if (this.retrieve(p)) this.get(p).pause();
          }, img);
        });
        this.controller && this.el.retrieve('pause').getParent().addClass(this.classes.play);
      }
    },

  /**
  Public method: next
    Goes to the next image in the show.

  Syntax:
    myShow.next();  
  */

    next: function(last){
      var n = last ? this.data.images.length - 1 
        : this._slide;
      this.go(n, 'left');
    },

  /**
  Public method: last
    Goes to the last image in the show.

  Syntax:
    myShow.last();  
  */

    last: function(){
      this.next(true); 
    },

  /**
  Public method: load
    Loads a new data set into the show: will stop the current show, rewind and rebuild thumbnails if applicable.

  Arguments:
    data - (array or object) The images and optional thumbnails, captions and links for the show.

  Syntax:
    myShow.load(data);
  */

    load: function(data){
      this.firstrun = true;
      this.showed = {'array': [], 'i': 0};
      if (typeOf(data) == 'array'){
        this.options.captions = false;      
        data = new Array(data.length).associate(data.map(function(image, i){ return image + '?' + i })); 
      }
      this.data = {'images': [], 'captions': [], 'hrefs': [], 'thumbnails': [], 'targets': [], 'titles': []};
      for (var image in data){
        var obj = data[image] || {},
          image = this.options.hu + image,
          caption = obj.caption ? obj.caption.trim() 
            : '',
          href = obj.href ? obj.href.trim() 
            : this.options.linked ? image 
            : this.options.href,
          target = obj.target ? obj.target.trim() 
            : '_self',
          thumbnail = obj.thumbnail ? this.options.hu + obj.thumbnail.trim() 
            : image.replace(this.options.replace[0], this.options.replace[1]),
          title = caption.replace(/<.+?>/gm, '').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, "'");
        this.data.images.push(image);
        this.data.captions.push(caption);
        this.data.hrefs.push(href);
        this.data.targets.push(target);
        this.data.thumbnails.push(thumbnail);
        this.data.titles.push(title);
      }
      if (this.options.random)
        this.slide = this._slide = Number.random(0, this.data.images.length - 1);

      // only run when data is loaded dynamically into an existing slideshow instance

      if (this.options.thumbnails && this.el.retrieve('thumbnails'))
        this._thumbnails();
      if (this.el.retrieve('images')){
        [this.a, this.b].each(function(img){
          ['morph', 'tween'].each(function(p){
            if (this.retrieve(p)) this.get(p).cancel();
          }, img);
        });
        this.slide = this._slide = this.timeToTransitionComplete = 0;
        this.go(0);    
      }
      return this.data.images.length;
    },

  /**
  Public method: destroy
    Destroys a Slideshow instance.

  Arguments:
    p - (string) The images and optional thumbnails, captions and links for the show.

  Syntax:
    myShow.destroy(p);
  */

    destroy: function(p){
      Object.each(this.events, function(array, e){
        if ('each' in array)
          array.each(function(fn){ document.removeEvent(e, fn); });
      });
      this.pause(1);
      'caption loader thumbnails'.split(' ').each(function(i, timer){
        this.options[i] && (timer = this[i].retrieve('timer')) && clearTimeout(timer);
      }, this);
      typeOf(this.el[p]) == 'function' && this.el[p]();
      delete this.el.uid;
    },

  /**
  Private method: preload
    Preloads the next slide in the show, once loaded triggers the show, updates captions, thumbnails, etc.
  */

    _preload: function(fast){
      var src = this.data.images[this._slide].replace(/([^?]+).*/, '$1'),
        cached = loaded = !!this.cache[src];
      if (!cached){
        if (!this.preloader)
           this.preloader = new Asset.image(src, {
            'onerror': function(){
              // do something
            },
            'onload': function(){
              this.store('loaded', true);
            }
          });
        loaded = this.preloader.retrieve('loaded') && this.preloader.get('width');
      }
      if (loaded && Date.now() > this.timeToNextTransition && Date.now() > this.timeToTransitionComplete){
        var src = this.data.images[this._slide].replace(/([^?]+).*/, '$1');
        if (this.preloader){
          this.cache[src] = {
            'height': this.preloader.get('height'),
            'src': src,
            'width': this.preloader.get('width')
          }
        }
        if (this.stopped){
          if (this.options.captions)
            this.caption.get('morph').cancel().start(this.classes.get('captions', 'hidden'));
          this.pause(1);
          if (this.end)
            this.fireEvent('end');
          this.stopped = this.end = false;
          return;        
        }
        this.image = this.counter % 2 ? this.b 
          : this.a;
        this.image.set('styles', {'display': 'block', 'height': null, 'visibility': 'hidden', 'width': null, 'zIndex': this.counter});
        this.image.set(this.cache[src]);
        this.image.width = this.cache[src].width;
        this.image.height = this.cache[src].height;
        this.options.resize && this._resize(this.image);
        this.options.center && this._center(this.image);
        var anchor = this.image.getParent();
        if (this.data.hrefs[this._slide]){
          anchor.set('href', this.data.hrefs[this._slide]);
          anchor.set('target', this.data.targets[this._slide]);      
        } 
        else {
          anchor.erase('href');
          anchor.erase('target');
        }
        var title = this.data.titles[this._slide];
        this.image.set('alt', title);    
        this.options.titles && anchor.set('title', title);
        this.options.loader && this.loader.fireEvent('hide');
        this.options.captions && this.caption.fireEvent('update', fast);        
        this.options.thumbnails && this.thumbnails.fireEvent('update', fast);       
        this._show(fast);
        this._loaded(fast);
      } 
      else {
        if (Date.now() > this.timeToNextTransition && this.options.loader)
          this.loader.fireEvent('show');
        this.timer = this._preload.delay(50, this, fast); 
      }
    },

  /**
  Private method: show
    Does the slideshow effect.
  */

    _show: function(fast){
      if (!this.image.retrieve('morph')){
        var options =  this.options.overlap ? {'link': 'cancel'} : {'link': 'chain'};
        $$(this.a, this.b).set('morph', Object.merge(options, {'duration': this.options.duration, 'onStart': this._start.bind(this), 'onComplete': this._complete.bind(this), 'transition': this.options.transition}));
      }
      var hidden = this.classes.get('images', (this.direction == 'left' ? 'next' 
        : 'prev')),
        visible = this.classes.get('images', 'visible'),
        img = this.counter % 2 ? this.a 
          : this.b;
      if (fast){      
        img.get('morph').cancel().set(hidden);
        this.image.get('morph').cancel().set(visible);       
      } 
      else {
        if (this.options.overlap){
          img.get('morph').set(visible);
          this.image.get('morph').set(hidden).start(visible);
        } 
        else {
          var fn = function(visible){
            this.image.get('morph').start(visible);
          }.pass(visible, this);
          if (this.firstrun)
            return fn();
          hidden = this.classes.get('images', (this.direction == 'left' ? 'prev' 
            : 'next'));
          this.image.get('morph').set(hidden);        
          img.get('morph').set(visible).start(hidden).chain(fn);
        }
      }
    },

  /**
  Private method: loaded
    Run after the current image has been loaded, sets up the next image to be shown.
  */

    _loaded: function(fast){
      this.counter++;
      this.timeToNextTransition = Date.now() + this.options.duration + this.options.delay;
      this.direction = 'left';      
      this.timeToTransitionComplete = fast ? 0 
        : Date.now() + this.options.duration;
      if (this._slide == (this.data.images.length - 1) && !this.options.loop && !this.options.random)
        this.stopped = this.end = true;
      if (this.options.random){
        this.showed.i++;
        if (this.showed.i >= this.showed.array.length){
          var n = this._slide;
          if (this.showed.array.getLast() != n) this.showed.array.push(n);
          while (this._slide == n)
            this.slide = this._slide = Number.random(0, this.data.images.length - 1);        
        }
        else
          this.slide = this._slide = this.showed.array[this.showed.i];
      }
      else {
        this.slide = this._slide;
        this._slide = (this.slide + 1) % this.data.images.length;
      }
      if (this.image.getStyle('visibility') != 'visible')
        (function(){ this.image.setStyle('visibility', 'visible'); }).delay(1, this);      
      if (this.preloader) 
        this.preloader = this.preloader.destroy();
      this.paused || this._preload();
    },

  /**
  Private method: center
    Center an image.
  */

    _center: function(img){
      var size = img.getSize(), 
        h = size.y, w = size.x; 
      img.set('styles', {'left': (w - this.width) / -2, 'top': (h - this.height) / -2});
    },

  /**
  Private method: resize
    Resizes an image.
  */

    _resize: function(img){
      var h = img.get('height').toFloat(), w = img.get('width').toFloat(),
        dh = this.height / h, dw = this.width / w;
      if (this.options.resize == 'fit')
        dh = dw = dh > dw ? dw 
          : dh;
      if (this.options.resize == 'fill')
        dh = dw = dh > dw ? dh 
          : dw;
      img.set('styles', {'height': Math.ceil(h * dh), 'width': Math.ceil(w * dw)});
    },

  /**
  Private method: start
    Callback on start of slide change.
  */

    _start: function(){    
      this.fireEvent('start');
    },

  /**
  Private method: complete
    Callback on start of slide change.
  */

    _complete: function(){
      if (this.firstrun && this.options.paused)
        this.pause(1);
      this.firstrun = false;
      this.fireEvent('complete');
    }  
  });

  /**
  Private method: captions
    Builds the optional caption element, adds interactivity.
    This method can safely be removed if the captions option is not enabled.
  */

  var Caption = new Class({
    Implements: [Chain, Events, Options],

    options: {/*
      duration: 500,
      fps: 50,
      transition: 'sine:in:out',
      unit: false, */
      delay: 0,
      link: 'cancel'      
    },

    initialize: function(slideshow){
      if (!slideshow)
        return;
      var options = slideshow.options.captions;
      if (options === true)
        options = {};
      this.setOptions(options);
      var el = slideshow.el.getElement(slideshow.classes.get('captions')),
        caption = el ? el.dispose().empty()
          : new Element('div', {'class': slideshow.classes.get('captions').substr(1)});
      slideshow.caption = caption;
      caption.set({
          'aria-busy': false,
          'aria-hidden': false,
        'events': { 'update': this.update.bind(slideshow) },
        'morph': this.options,
        'role': 'description'
      }).store('delay', this.options.delay);
        if (!caption.get('id'))
          caption.set('id', 'Slideshow-' + Date.now());
        slideshow.el.retrieve('images').set('aria-labelledby', caption.get('id'));
      caption.inject(slideshow.el);
    },

    update: function(fast){
        var empty = !this.data.captions[this._slide].length, timer;
      if (timer = this.caption.retrieve('timer'))
        clearTimeout(timer);
        if (fast){
          var p = empty ? 'hidden' 
        : 'visible';
          this.caption.set({'aria-hidden': empty, 'html': this.data.captions[this._slide]}).get('morph').cancel().set(this.classes.get('captions', p));
        }
        else {
          var fn1 = empty ? function(){} 
        : function(caption){
        this.caption.store('timer', setTimeout(function(caption){
              this.caption.set('html', caption).morph(this.classes.get('captions', 'visible'));
        }.pass(caption, this), this.caption.retrieve('delay')));
          }.pass(this.data.captions[this._slide], this);    
          var fn2 = function(){ 
            this.caption.set('aria-busy', false); 
          }.bind(this);
          this.caption.set('aria-busy', true).get('morph').cancel().start(this.classes.get('captions', 'hidden')).chain(fn1, fn2);
        }
    }
  });

  /**
  Private method: controller
    Builds the optional controller element, adds interactivity.
    This method can safely be removed if the controller option is not enabled.
  */

  var Controller = new Class({
    Implements: [Chain, Events, Options],

    options: {/*
      duration: 500,
      fps: 50,
      transition: 'sine:in:out',
      unit: false, */
      link: 'cancel'      
    },

    initialize: function(slideshow){
      if (!slideshow)
        return;
      var options = slideshow.options.captions;
      if (options === true)
        options = {};
      this.setOptions(options);
      var el = slideshow.el.getElement(slideshow.classes.get('controller')),
        controller = el ? el.dispose().empty()
          : new Element('div', {'class': slideshow.classes.get('controller').substr(1)});
      slideshow.controller = controller;
      controller.set({
        'aria-hidden': false, 
        'role': 'menubar'
      });
      var ul = new Element('ul', {'role': 'menu'}).inject(controller),
        i = 0;
      Object.each(slideshow.accesskeys, function(accesskey, action){
        var li = new Element('li', {
          'class': (action == 'pause' && this.options.paused) ? this.classes.play + ' ' + this.classes[action] 
            : this.classes[action]
        }).inject(ul);
        var a = this.el.retrieve(action, new Element('a', {
          'role': 'menuitem', 'tabindex': i++, 'title': accesskey.label
        }).inject(li));
        a.set('events', {
          'click': function(action){
            this[action]()
          }.pass(action, this),
          'mouseenter': function(active){
            this.addClass(active)
          }.pass(this.classes.active, a),
          'mouseleave': function(active){
            this.removeClass(active)
          }.pass(this.classes.active, a)
        });    
      }, slideshow);
      controller.set({
        'events': {
          'hide': this.hide.pass(slideshow.classes.get('controller', 'hidden'), controller),
          'show': this.show.pass(slideshow.classes.get('controller', 'visible'), controller)
        },
        'morph': this.options
      }).store('hidden', false);
      slideshow.events
        .push('keydown', this.keydown.bind(slideshow))
        .push('keyup',  this.keyup.bind(slideshow))
        .push('mousemove',  this.mousemove.bind(slideshow));
      controller.inject(slideshow.el).fireEvent('hide');
    },

    hide: function(hidden){
      if (this.get('aria-hidden') == 'false')
        this.set('aria-hidden', true).morph(hidden);
    },

    keydown: function(e){
      Object.each(this.accesskeys, function(accesskey, action){
        if (e.key == accesskey.key && e.shift == accesskey.shift && e.control == accesskey.control && e.alt == accesskey.alt){
          if (this.controller.get('aria-hidden') == 'true')
            this.controller.get('morph').set(this.classes.get('controller', 'visible'));
          this.el.retrieve(action).fireEvent('mouseenter');
        }          
      }, this);    
    },

    keyup: function(e){
      Object.each(this.accesskeys, function(accesskey, action){
        if (e.key == accesskey.key && e.shift == accesskey.shift && e.control == accesskey.control && e.alt == accesskey.alt){
          if (this.controller.get('aria-hidden') == 'true')
            this.controller.set('aria-hidden', false).fireEvent('hide'); 
          this.el.retrieve(action).fireEvent('mouseleave');
        }          
      }, this);      
    },

    mousemove: function(e){
      var images = this.el.retrieve('images').getCoordinates(),
        action = (e.page.x > images.left && e.page.x < images.right && e.page.y > images.top && e.page.y < images.bottom) ? 'show' 
          : 'hide';
      this.controller.fireEvent(action);
    },

    show: function(visible){
      if (this.get('aria-hidden') == 'true')
        this.set('aria-hidden', false).morph(visible);
    }
  });

  /**
  Private method: loader
    Builds the optional loader element, adds interactivity.
    This method can safely be removed if the loader option is not enabled.
  */

  var Loader = new Class({
    Implements: [Chain, Events, Options],

    options: {/*
      duration: 500,
      transition: 'sine:in:out',
      unit: false, */
      fps: 20,
      link: 'cancel'      
    },

    initialize: function(slideshow){
      if (!slideshow)
        return;
      var options = slideshow.options.loader;
      if (options === true)
        options = {};
      this.setOptions(options);
      var loader = new Element('div', {
        'aria-hidden': false,
        'class': slideshow.classes.get('loader').substr(1),        
        'morph': this.options,
        'role': 'progressbar'
      }).store('animate', false).store('i', 0).store('delay', 1000 / this.options.fps).inject(slideshow.el);
      slideshow.loader = loader;
      var url = loader.getStyle('backgroundImage').replace(/url\(['"]?(.*?)['"]?\)/, '$1').trim();
      if (url){
        if (url.test(/\.png$/) && Browser.ie && Browser.version < 7)
          loader.setStyles({'backgroundImage': 'none', 'filter': 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src="' + url + '", sizingMethod="crop")'});          
        new Asset.image(url, {'onload': function(){
          var size = loader.getSize(),
            width = this.get('width'), 
            height = this.get('height');
          if (width > size.x)
            loader.store('x', size.x).store('animate', 'x').store('frames', (width / size.x).toInt());
          if (height > size.y)
            loader.store('y', size.y).store('animate', 'y').store('frames', (height / size.y).toInt());
        }});
      }
      loader.set('events', {
        'animate': this.animate.bind(loader),
        'hide': this.hide.pass(slideshow.classes.get('loader', 'hidden'), loader),
        'show': this.show.pass(slideshow.classes.get('loader', 'visible'), loader)
      });
      loader.fireEvent('hide');
    },

    animate: function(){
      var animate = this.retrieve('animate');
      if (!animate)
        return;
      var i = (this.retrieve('i').toInt() + 1) % this.retrieve('frames');
      this.store('i', i);
      var n = (i * this.retrieve(animate)) + 'px';
      if (animate == 'x')
        this.setStyle('backgroundPosition', n + ' 0px');      
      if (animate == 'y')
        this.setStyle('backgroundPosition', '0px ' + n);      
    },

    hide: function(hidden){
      if (this.get('aria-hidden') == 'false'){
        this.set('aria-hidden', true).morph(hidden);
        if (this.retrieve('animate'))
          clearTimeout(this.retrieve('timer'));
      }
    },

    show: function(visible){
      if (this.get('aria-hidden') == 'true'){
        this.set('aria-hidden', false).morph(visible);
        if (this.retrieve('animate')){
          this.store('timer', function(){ 
            this.fireEvent('animate') 
          }.periodical(this.retrieve('delay'), this));      
        }
      }
    }
  });

  /**
  Private method: thumbnails
    Builds the optional thumbnails element, adds interactivity.
    This method can safely be removed if the thumbnails option is not enabled.
  */

  var Thumbnails = new Class({
    Implements: [Chain, Events, Options],

    options: {/*
      duration: 500,
      transition: 'sine:in:out',
      unit: false, */
      columns: null,
      fps: 50,
      link: 'cancel',
      position: null,
      rows: null,
      scroll: null
    },

    initialize: function(slideshow){
      var options = (slideshow.options.thumbnails === true) ? {} 
        : slideshow.options.thumbnails;
      this.setOptions(options);
      var el = slideshow.el.getElement(slideshow.classes.get('thumbnails')),
        thumbnails = el ? el.empty() 
          : new Element('div', {'class': slideshow.classes.get('thumbnails').substr(1)});
      slideshow.thumbnails = thumbnails;
      thumbnails.set({'role': 'menubar', 'styles': {'overflow': 'hidden'}});
      var uid = thumbnails.retrieve('uid', 'Slideshow-' + Date.now()),
        ul = new Element('ul', {'role': 'menu', 'styles': {'left': 0, 'position': 'absolute', 'top': 0}, 'tween': {'link': 'cancel'}}).inject(thumbnails);
      slideshow.data.thumbnails.each(function(thumbnail, i){
        var li = new Element('li', {'id': uid + i}).inject(ul),
          a = new Element('a', {
            'class': slideshow.classes.get('thumbnails', 'hidden').substr(1),
            'events': {
              'click': this.click.pass(i, slideshow)
            },
            'href': slideshow.data.images[i],
            'morph': this.options,
            'role': 'menuitem',
            'tabindex': i
          }).store('uid', i).inject(li);
        if (slideshow.options.titles)
          a.set('title', slideshow.data.titles[i]);
        new Asset.image(thumbnail, {
          'onload': this.onload.pass(i, slideshow)
        }).inject(a);
      }, this);
      thumbnails.set('events', {
        'scroll': this.scroll.bind(thumbnails),
        'update': this.update.bind(slideshow)
      });
      var coords = thumbnails.getCoordinates();
      if (!options.scroll)
        options.scroll = (coords.height > coords.width) ? 'y' 
          : 'x';
      var props = (options.scroll == 'y') ? 'top bottom height y width'.split(' ') 
        : 'left right width x height'.split(' ');
      thumbnails.store('props', props).store('delay', 1000 / this.options.fps);
      slideshow.events.push('mousemove', this.mousemove.bind(thumbnails));
      thumbnails.inject(slideshow.el);
    },

    click: function(i){
      this.go(i); 
      return false; 
    },

    mousemove: function(e){
      var coords = this.getCoordinates();
      if (e.page.x > coords.left && e.page.x < coords.right && e.page.y > coords.top && e.page.y < coords.bottom){
        this.store('page', e.page);      
        if (!this.retrieve('mouseover')){
          this.store('mouseover', true);
          this.store('timer', function(){this.fireEvent('scroll');}.periodical(this.retrieve('delay'), this));
        }
      }
      else {
        if (this.retrieve('mouseover')){
          this.store('mouseover', false);        
          clearTimeout(this.retrieve('timer'));
        }
      }      
    },

    onload: function(i){
      var thumbnails = this.thumbnails,
        a = thumbnails.getElements('a')[i];
      if (a){
        (function(a){
          var visible = i == this.slide ? 'active' 
            : 'inactive';          
          a.store('loaded', true).get('morph').set(this.classes.get('thumbnails', 'hidden')).start(this.classes.get('thumbnails', visible));  
        }).delay(Math.max(1000 / this.data.thumbnails.length, 100), this, a);
      }          
      if (thumbnails.retrieve('limit'))
        return;
      var props = thumbnails.retrieve('props'), 
        options = this.options.thumbnails,
        pos = props[1], 
        length = props[2], 
        width = props[4],
        li = thumbnails.getElement('li:nth-child(' + (i + 1) + ')').getCoordinates();
      if (options.columns || options.rows){
        thumbnails.setStyles({'height': this.height, 'width': this.width});
        if (options.columns.toInt())
          thumbnails.setStyle('width', li.width * options.columns.toInt());
        if (options.rows.toInt())
          thumbnails.setStyle('height', li.height * options.rows.toInt());
      }
      var div = thumbnails.getCoordinates();
      if (options.position){
        if (options.position.test(/bottom|top/))
          thumbnails.setStyles({'bottom': 'auto', 'top': 'auto'}).setStyle(options.position, -div.height);
        if (options.position.test(/left|right/))
          thumbnails.setStyles({'left': 'auto', 'right': 'auto'}).setStyle(options.position, -div.width);
      }
      var units = Math.floor(div[width] / li[width]),
        x = Math.ceil(this.data.images.length / units),
        r = this.data.images.length % units,
        len = x * li[length],
        ul = thumbnails.getElement('ul').setStyle(length, len);
      ul.getElements('li').setStyles({'height': li.height, 'width': li.width});
      thumbnails.store('limit', div[length] - len);
    },

    scroll: function(n, fast){
      var div = this.getCoordinates(),
        ul = this.getElement('ul').getPosition(),
        props = this.retrieve('props'),
        axis = props[3], delta, pos = props[0], size = props[2], value,      
        tween = this.getElement('ul').set('tween', {'property': pos}).get('tween');  
      if (n != undefined){
        var uid = this.retrieve('uid'),
          li = document.id(uid + n).getCoordinates();
        delta = div[pos] + (div[size] / 2) - (li[size] / 2) - li[pos];
        value = (ul[axis] - div[pos] + delta).limit(this.retrieve('limit'), 0);
        tween[fast ? 'set' : 'start'](value);
      }
      else{
        var area = div[props[2]] / 3, 
          page = this.retrieve('page'), 
          velocity = -(this.retrieve('delay') * 0.01);      
        if (page[axis] < (div[pos] + area))
          delta = (page[axis] - div[pos] - area) * velocity;
        else if (page[axis] > (div[pos] + div[size] - area))
          delta = (page[axis] - div[pos] - div[size] + area) * velocity;      
        if (delta){      
          value = (ul[axis] - div[pos] + delta).limit(this.retrieve('limit'), 0);
          tween.set(value);
        }
      }        
    },

    update: function(fast){
      var thumbnails = this.thumbnails,
        uid = thumbnails.retrieve('uid');
      thumbnails.getElements('a').each(function(a, i){
        if (a.retrieve('loaded')){
          if (a.retrieve('uid') == this._slide){
            if (!a.retrieve('active', false)){
              a.store('active', true);
              var active = this.classes.get('thumbnails', 'active');              
              if (fast) a.get('morph').set(active);
              else a.morph(active);
            }
          } 
          else {
            if (a.retrieve('active', true)){
              a.store('active', false);
              var inactive = this.classes.get('thumbnails', 'inactive');            
              if (fast) a.get('morph').set(inactive);
              else a.morph(inactive);
            }
          }
        }
      }, this);
      if (!thumbnails.retrieve('mouseover'))
        thumbnails.fireEvent('scroll', [this._slide, fast]);
    }
  });
})();
