/**
 * Flexible slide show class with UI bindings, Ajax deferred slide loading,
 * proper all-images-in-slide-loaded detection before transition, clean speed
 * adjustments, etc.
 *
 * @created  2008.07.22 14:17
 * @latest   2008.07.24 11:19
 * @version  0.6
 * @author   Christophe Porteneuve <tdd@tddsworld.com>
 * @legals   MIT (see LICENSE file)
 * @requires Prototype 1.6, script.aculo.us 1.8.1's Effects engine.
 *
 * TODO
 * - PDoc public API and options
 * - Full doc on CSS expectations (for slide container)
 * - Customizable transition effects:
 *   • SlideFrom(Top,TopRight,Right,BottomRight,Bottom,BottomLeft,Left,TopLeft,Center) -> new slide animates
 *   • RevealFrom(Top,TopRight,Right,BottomRight,Bottom,BottomLeft,Left,TopLeft,Borders) -> old slide animates
 *   • Appear [default] -> new slide animates
 *   • none -> immediate change
 */
var SlideShow = Class.create({
  // Public API
  
  initialize: function(viewContainer, thumbnailContainer, options) {
    this._speedControls = {};
    this._togglers = [];
    this._viewContainer = $(viewContainer);
    this.setOptions(options);
    this._boundStep = this._step.bind(this);
    this.setSpeed(this.options.speed);
    this._viewIds = this._getViewIdsFromThumbnails(thumbnailContainer);
    this._bindThumbnailClicks(thumbnailContainer);
    this.reset();
  },
  
  bindToUI: function(uiElements) {
    var that = this, counter = 0;
    $w('previous next start resume pause stop suspend toggle').each(function(command) {
      counter += that._bindUIElement(uiElements, command);
    });
    $w('slow regular fast').each(function(speed) {
      counter += that._bindUIElement(uiElements, 'setSpeed', speed);
    });
    return counter;
  },
  
  isRunning: function() { return !!this._timer; },
  
  next: function() {
    return this.skipTo(1, true, this.options.nextStops);
  },
  
  pause: function() {
    if (!arguments[0])
      this._togglers.invoke('removeClassName', this.options.uiActiveClass);
    if (!this.isRunning()) return false;
    clearTimeout(this._timer);
    this._timer = null;
    this._lastTimerSetAt = null;
    return true;
  },
  
  position: function() { return this._position; },
  
  previous: function() {
    return this.skipTo(-1, true, this.options.previousStops);
  },
  
  reset: function() {
    if (this.size() == 0)
      this._position = -1;
    else
      this.skipTo(this._viewIds[0]);
  },
  
  resume: function() {
    if (this.isRunning()) return false;
    if (!arguments[0])
      this._togglers.invoke('addClassName', this.options.uiActiveClass);
    this._setupTimer();
    return true;
  },
  
  size: function() { return this._viewIds.length; },
  
  speed: function() { return this._speed; },
  
  setOptions: function(options) {
    this.options = Object.extend(Object.clone(this.constructor.DefaultOptions), options || {});
    this._viewIdTemplate = new Template(this.options.viewIdTemplate);
    this._prepareIndicator();
    if (this.options.uiBindings)
      this.bindToUI(this.options.uiBindings);
  },
  
  setSpeed: function(speed) {
    if (!Object.isString(speed)) return;
    speed = speed.toLowerCase();
    if (!(speed in this.constructor.SPEEDS) || speed == this.speed()) return;
    for (var s in this.constructor.SPEEDS)
      (this._speedControls[s] || []).invoke((s == speed ? 'add' : 'remove') + 'ClassName', this.options.uiActiveClass);
    this._speed = speed;
    if (this.isRunning()) {
      var now = new Date().getTime(), expectedTime = this._lastTimerSetAt + this.constructor.SPEEDS[speed] * 1000;
      if (expectedTime < now)
        this.next();
      else
        this._setupTimer(true);
    }
  },
  
  skipTo: function(id, relative, stopAnyway) {
    var size = this.size();
    if (size == 0 || this._fetchingPhoto)
      return false;
    var wasRunning = this.isRunning();
    var willRestart = !stopAnyway && (relative || !this.options.skipStops) && wasRunning;
    this.pause(willRestart);
    if (relative) {
      var pos = this.position() + id;
      if ((pos < 0 || pos >= size) && !this.options.cycle)
        return this.pause();
      while (pos < 0)
        pos = pos + size;
      while (pos >= size)
        pos = pos - size;
      this._position = pos;
      id = this._viewIds[pos];
    } else if (!this._viewIds.include(id))
      return this.pause();
    else {
      this._position = this._viewIds.indexOf(id);
    }
    
    var that = this, otherView = this._getView(id);

    function moveToView() {
      var otherView = that._getView(id), oldActiveView = that._activeView, tempView;
      if (!otherView || otherView == that._activeView) {
        that._cleanupLoading();
        return;
      }
      that._viewIds.each(function(viewId) {
        tempView = that._getView(viewId);
        if (!tempView || tempView == otherView || tempView == that._activeView) return;
        tempView.style.zIndex = 0;
      });
      that._activeView && (that._activeView.style.zIndex = 1);
      otherView.style.zIndex = 2;
      var opts = { duration: that.options.effectDuration };
      that._activeView = otherView;
      otherView.appear(Object.extend({
        afterFinish: function() {
          oldActiveView && oldActiveView.hide();
          that._cleanupLoading();
          that._triggerEvent('afterSlideChange', that.position());
        }
      }, opts));
      if (willRestart)
        that.resume();
    };
    
    that._triggerEvent('beforeSlideChange', that.position());
    if (!otherView && this.options.ajaxLoadingURL)
      this._deferViewLoading(id, moveToView);
    else
      moveToView();
  }, // skipTo
  
  toggle: function() {
    this[this.isRunning() ? 'pause' : 'resume']();
  },
  
  unbindFromUI: function(uiElements) {
    if (!this._bindCache) return;
    var elements = Object.values(uiElements);
    for (var obj in this._bindCache)
      if (Object.isElement(obj) && (!uiElements || elements.include(obj))) {
        obj.stopObserving('click', this._bindCache[obj]);
        delete this._bindCache[obj];
        this._togglers = this._togglers.without(obj);
        for (var speed in this._speedControls)
          this._speedControls[speed] = this._speedControls[speed].without(obj);
      }
  },
  
  // Private API

  _bindThumbnailClicks: function(thumbnailContainer) {
    if (!this.options.bindThumbnailClicks) return;
    var that = this;
    $(thumbnailContainer).observe('click', function(e) {
      var activator = e.findElement(that.options.thumbnailSelector);
      if (!activator) return;
      e.stop();
      activator.blur();
      var match = activator.id.match(that.options.thumbnailIdRegex);
      if (!match) return;
      that.skipTo(match[1]);
    });
  },
  
  _bindUIElement: function(uiElements, command, argument) {
    this._bindCache = this._bindCache || {};
    var element = $(uiElements[command]) || $(uiElements[argument]);
    element && element.identify();
    if (!element || this._bindCache[element.id]) return 0;
    this._bindCache[element.id] = this._bindCache[element.id] ||
      this._handleCommand.bindAsEventListener(this, element, command, argument);
    if ('toggle' == command && !this._togglers.include(element))
      this._togglers.push(element);
    else if ('setSpeed' == command) {
      this._speedControls[argument] = this._speedControls[argument] || [];
      this._speedControls[argument].push(element);
    }
    element.observe('click', this._bindCache[element.id]);
    return 1;
  },
  
  _cleanupLoading: function() {
    this._indicator && this._indicator.hide();
    this._fetchingPhoto = false;
  },
  
  _deferViewLoading: function(viewId, callback) {
    this._indicator && this._indicator.show();
    var that = this, maxAttempts = this.constructor.MAX_IMAGE_LOADING_CHECKS;
    var url = this.options.ajaxLoadingURL;
    if (Object.isFunction(url))
      url = url(viewId);
    new Ajax.Updater(this._viewContainer, url.replace('__ID__', viewId), {
      method: 'get', insertion: 'bottom', onSuccess: function() {
        (function() {
          if (that._imagesLoadedForView(viewId)) {
            callback.defer();
            return;
          }
          var attempts = 0;
          new PeriodicalExecuter(function(pe) {
            attempts += 1;
            if (that._imagesLoadedForView(viewId) || attempts >= maxAttempts) {
              callback();
              pe.stop();
            }
          }, 0.5);
        }).defer();
      }
    });
  },
  
  _getViewIdsFromThumbnails: function(thumbnailContainer) {
    var match, that = this;
    return $(thumbnailContainer).select(this.options.thumbnailSelector).map(function(item) {
      match = item.id.match(that.options.thumbnailIdRegex);
      return match && match[1];
    }).compact();
  },
  
  _getView: function(id) {
    return $(this._viewIdTemplate.evaluate({ id: id }));
  },
  
  _handleCommand: function(e, trigger, command, extraArg) {
    e.stop();
    if (Object.isFunction(trigger.blur))
      trigger.blur();
    this[command](extraArg);
  },
  
  _imagesLoadedForView: function(viewId) {
    var root = this._getView(viewId);
    // This relies on “isLoaded” extension method for HTMLImageElement (see bottom of file)…
    return !root || root.select('img').invoke('isLoaded').all();
  },
  
  _prepareIndicator: function() {
    var indic = this.options.indicator;
    if (Object.isString(indic) && $(indic))
      indic = $(indic);
    if (indic == this._indicator)
      return;
    if (this._createdIndicator)
      this._indicator.remove();
    this._createdIndicator = Object.isString(indic);
    if (this._createdIndicator) {
      this._indicator = new Element('div', { style:
        'position: absolute; left: 0; top: 0; width: 100%; height: 100%; z-index: 4242; ' +
        'background: white url(' + indic + ') center center no-repeat; ' +
        'opacity: 0.5; filter: alpha(opacity=50); display: none'
      });
      this._viewContainer.insert(this._indicator);
    } else
      this._indicator = indic;
  },
  
  _setupTimer: function(adjust) {
    var interval = this.constructor.SPEEDS[this.speed()] * 1000;
    if (adjust && this._lastTimerSetAt) {
      interval -= (new Date().getTime() - this._lastTimerSetAt);
    } else {
      this._lastTimerSetAt = new Date().getTime();
    }
    this._timer = setTimeout(this._boundStep, interval);
  },
  
  _step: function() {
    if (!this.isRunning()) return false;
    return this.next();
  },
  
  _triggerEvent: function(event) {
    if (!this.options[event]) return;
    var args = $A(arguments);
    args[0] = this;
    this.options[event].apply(null, args);
  }
}); // SlideShow

Object.extend(SlideShow, {
  DefaultOptions: {
    // ajaxLoadingURL: '/your/fetch/url/with/__ID__', // or function(viewId) -> String
    bindThumbnailClicks: true,
    cycle: false,
    effectDuration: 0.5,
    indicator: '/images/slideshow_spinner.gif', // or false or an ID string or an Element
    nextStops: false,
    previousStops: false,
    scrollThumbnailOnView: true,
    skipStops: true,
    speed: 'regular',
    thumbnailSelector: 'li',
    thumbnailIdRegex: /^.+_([^_]+)$/,
    uiActiveClass: 'active',
    viewIdTemplate: 'view_#{id}'
  },

  MAX_IMAGE_LOADING_CHECKS: 50,
  SPEEDS: { slow: 7, regular: 4, fast: 1 },
  
  start:   SlideShow.prototype.resume,
  stop:    SlideShow.prototype.pause,
  suspend: SlideShow.prototype.pause
});

Element.addMethods('IMG', {
  // Based on code by John-David Dalton and others: see http://pastie.org/pastes/185452
  isLoaded: (function() {
      var img = new Image();
      if ('naturalWidth' in img)
        return function(element) { return 0 !== element.naturalWidth; };
      if ('complete' in img)
        return function(element) { return !!element.complete; }
      return function(element) { return element.readyState == 'complete'; };
    })()
});
