//.........................................................................................
//  File: SlideShow.js                                               Date:  Apr 7, 2009
//                            Application:
//  Author: R. Berry
//
//  Requires Prototype for:
//          $()
//          Prototype.Browser.WebKit
//          Object.isArray
//
//  Copyright 2009 (c) Rawdon Berry
//
//  Modified:
//.........................................................................................

//................................................................................................
// SlideShowThumb
//
//................................................................................................
function SlideShowThumb(slide,thumbDir,imageDir)
{
  this.source = null;
  this.imgWidth = 0;
  this.imgHeight = 0;
  this.errorLoading = false;
  this.slideParent = slide;
  var item = slide.listItemParent;
  if (item)
  {
    if (item.source)
    {
      if (!thumbDir && !item.thumb)
      {
        this.source = (imageDir || '')+item.source;
      }
      else
      {
        this.source  = (thumbDir || '')+(item.thumb || item.source);
      }
    }
    this.imgWidth  = (item.thumbWidth || item.imgWidth || 0);
    this.imgHeight = (item.thumbHeight || item.imgHeight || 0);
  }
}

//................................................................................................
// SlideShowThumbManager
//
//................................................................................................
function SlideShowThumbManager(slide_show_owner,options)
{
  this.parentSlideShowManager = slide_show_owner;
  this.stage = null;
  this.wrapThumbs = false;
  this.dispFirst = 0;
  this.dispLast = 0;
  this.thumbAreaWidth = 0;
  this.thumbAreaHeight = 0;
  this.interThumbSpacing = 0;
  this.isVertical = false;
  this.thumbFixedDim = 0;
  this.onThumbPointerChange = null;
  this.thumbBorder = 0;
  this.currentIdx = -2;  // -2 means this. is a newly created object

  this.thumbLoadingInProgress = false;

  try
  {
    if ( (this.stage = $(options.stageId)) == null)
      throw "Invalid id for thumb area stage ["+((options==null) ? null : options.stageId)+"]";
  }
  catch(ex)
  {
    alert("Failed creating slide show thumbnail manager:\n"+ex);
    return;
  }
  if (options.wrapThumbs)
    this.wrapThumbs = true;
  if (typeof(options.thumbSpacing)!='undefined')
    this.interThumbSpacing = options.thumbSpacing;
  if (typeof(options.orientation)!='undefined')
  {
    switch(options.orientation)
    {
      case 'vert':
      case 'vertical':
        this.isVertical = true;
        break;
    }
  }
  if (this.isVertical)
  {
    this.thumbFixedDim = (options.fixedThumbWidth || 50);
  }
  else
  {
    this.thumbFixedDim = (options.fixedThumbHeight || 50);
  }
  if (typeof(options.thumbBorder)!='undefined')
    this.thumbBorder = options.thumbBorder;
  if (typeof(options.onThumbAreaBeforeRedraw)!='undefined')
    this.onThumbAreaBeforeRedraw = options.onThumbAreaBeforeRedraw;
  if (typeof(options.onThumbAreaAfterRedraw)!='undefined')
    this.onThumbAreaAfterRedraw = options.onThumbAreaAfterRedraw;
  if (typeof(options.onThumbPointerChange)!='undefined')
    this.onThumbPointerChange = options.onThumbPointerChange;
  if (typeof(options.onThumbAreaSizeChange)!='undefined')
    this.onThumbAreaSizeChange = options.onThumbAreaSizeChange;
  if (typeof(options.onThumbClick)!='undefined')
    this.onThumbClick = options.onThumbClick;
}
SlideShowThumbManager.prototype.setThumbFolder = function(folder)
{
  this.thumbFolder = folder;
  if (this.thumbFolder != null)
  {
    if (!this.thumbFolder.endsWith('/'))
        this.thumbFolder += '/';
  }
}

SlideShowThumbManager.prototype.internalOnThumbClick = function(ev)
{
  if (!window.attachEvent) 
    ev.currentTarget.thisSlideShowThumbManager.onThumbClick(ev.currentTarget);
  else
  {
    if (ev.currentTarget)
    { // We have Opera. The trigger is the container
      ev.currentTarget.thisSlideShowThumbManager.onThumbClick(ev.currentTarget);
    }
    else
    { // We have IE. The trigger is the image, NOT the container
      this.onThumbClick(ev.srcElement.parentNode);
    }
  }
}

SlideShowThumbManager.prototype.newBatch = function(startIdx)
{
  var slides = this.parentSlideShowManager.slides;
  if (!slides)
    return(-1);

  var thumbManagerInstance = this;
  if (startIdx < 0 || startIdx >= slides.length)
  {
    if (this.wrapThumbs)
    {
      if ( (startIdx = (startIdx % slides.length)) < 0)
        startIdx += slides.length;
    }
    else
      return(0);
  }

  if (this.onThumbAreaBeforeRedraw)
    this.onThumbAreaBeforeRedraw();

  var container,
      thumbnail;
  var dimTot;
  var www,hhh,containerW,containerH;
  var tidx;
  var haveLooped;

  var loadCount = 0;
  var expectedCount = -1;
  var loadTimer = null;
  var abnormalLoadTimer = null;
  var forceDone = false;
  var thisThumb;

  if ( (startIdx == (this.dispFirst-1)) || ((this.dispFirst==0) && (startIdx==(slides.length-1))))
  { // Going back by a bank
    dimTot = 0;
    haveLooped = false;
    this.dispLast = startIdx;
    if (this.isVertical)
    {
      www = this.thumbFixedDim;
      containerW = www + 2 * this.thumbBorder;
    }
    else
    {
      hhh = this.thumbFixedDim;
      containerH = hhh + 2 * this.thumbBorder;
    }
    while(true)
    {
      if (this.dispLast < 0)
      {
        if (this.wrapThumbs && !haveLooped)
        {
          this.dispLast = slides.length-1;
          haveLooped=true;
        }
        else
        {
          this.dispLast++;
          break;
        }
      }

      thisThumb = slides[this.dispLast].thumbChild;

      if (this.isVertical)
      {
        hhh = Math.floor((www * thisThumb.imgHeight)/thisThumb.imgWidth);
        containerH = hhh + this.thumbBorder;
        if ((dimTot+containerH+this.thumbBorder) > this.thumbAreaHeight)
        {
          this.dispLast++;
          break;
        }
        dimTot += containerH+this.interThumbSpacing;
      }
      else
      {
        www = Math.floor((hhh * thisThumb.imgWidth)/thisThumb.imgHeight);
        containerW = www + this.thumbBorder;
        if ((dimTot+containerW+this.thumbBorder) > this.thumbAreaWidth)
        {
          this.dispLast++;
          break;
        }
        dimTot += containerW+this.interThumbSpacing;
      }
      this.dispLast--;
    }
    startIdx = this.dispLast;
  }

  dimTot = 0;
  haveLooped = false;
  if (this.images)
  {
    for(tidx = 0; tidx < this.images.length; tidx++)
    {
      this.images[tidx].container.removeChild(this.images[tidx].thumb);
      this.stage.removeChild(this.images[tidx].container);
      delete this.images[tidx].container;
      delete this.images[tidx].thumb;
    }
    delete this.images;
  }
  if (this.isVertical)
  {
    www = this.thumbFixedDim;
    containerW = www + 2 * this.thumbBorder;
  }
  else
  {
    hhh = this.thumbFixedDim;
    containerH = hhh + 2 * this.thumbBorder;
  }

  tidx = 0;
  this.dispFirst = startIdx;
  this.dispLast = startIdx;
  this.thumbLoadingInProgress = true;
  while(true)
  {
    if (this.dispLast == slides.length)
    {
      if (this.wrapThumbs && !haveLooped)
      {
        this.dispLast = 0;
        haveLooped=true;
      }
      else
      {
        this.dispLast--;
        tidx--;
        break;
      }
    }

    thisThumb = slides[this.dispLast].thumbChild;

    if (this.isVertical)
    {
      hhh = Math.floor((www * thisThumb.imgHeight)/thisThumb.imgWidth);
      containerH = hhh + this.thumbBorder;
      if ((dimTot+containerH+this.thumbBorder) > this.thumbAreaHeight)
      {
        this.dispLast--;
        tidx--;
        break;
      }
    }
    else
    {
      www = Math.floor((hhh * thisThumb.imgWidth)/thisThumb.imgHeight);
      containerW = www + this.thumbBorder;
      if ((dimTot+containerW+this.thumbBorder) > this.thumbAreaWidth)
      {
        this.dispLast--;
        tidx--;
        break;
      }
    }
    if (!this.images)
    {
      this.images = new Array();
    }
    if (tidx == this.images.length)
    {
      container = document.createElement("div");
      container.id = 'thumb'+tidx+'Container';
      this.stage.appendChild(container);
      container.style.position='absolute';
      container.className='thumbImageContainerStyle';

      thumbnail = document.createElement("img");
      thumbnail.id = 'thumb'+tidx;
      container.appendChild(thumbnail);
      thumbnail.style.position='absolute';
      thumbnail.className='thumbImageStyle';
      thumbnail.style.visibility = 'hidden';
      this.images.push({ container: $(container), thumb: $(thumbnail) });
    }
    else
    {
      container = this.images[tidx].container;
      thumbnail = this.images[tidx].thumb;
    }
    container.relIdx = tidx;
    container.absIdx = this.dispLast;
    container.className='thumbImageContainerStyle'; // To reset any left over highlighting
    if (this.onThumbClick)
    {
      container.thisSlideShowThumbManager = this;   // So we have access to "this" within the event handler
      if (window.attachEvent) 
      {
        container.attachEvent("onclick",this.internalOnThumbClick);
      }
      else 
      {
        container.onclick=this.internalOnThumbClick;
      }
    }
    if (this.isVertical)
    {
      container.style.left = Math.max(0,Math.round((this.thumbAreaWidth-containerW)/2))+'px';
      container.style.top = dimTot+'px';
      dimTot += containerH+this.interThumbSpacing;
      container.style.width = containerW+'px';
      container.style.height = (containerH+this.thumbBorder)+'px';
    }
    else
    {
      container.style.left = dimTot+'px';
      container.style.top = Math.max(0,Math.round((this.thumbAreaHeight-containerH)/2))+'px';
      dimTot += containerW+this.interThumbSpacing;
      container.style.width = (containerW+this.thumbBorder)+'px';
      container.style.height = (containerH+5)+'px';
    }
    if (this.thumbBorder)
    {
      thumbnail.style.left = this.thumbBorder+'px';
      thumbnail.style.top  = this.thumbBorder+'px';
    }
    else
    {
      thumbnail.style.left = '0px';
      thumbnail.style.top  = '0px';
    }
    thumbnail.style.width = www+'px';
    thumbnail.style.height = hhh+'px';

    thumbnail.onload = function()  { onThumbImageLoad(this,false); };
    thumbnail.onerror = function() { onThumbImageLoad(this,true); };
    thumbnail.onabort = function() { onThumbImageLoad(this,"abort"); };
    thumbnail.src = thisThumb.source;
    if (this.isWebKitNasty)
    {
      // For Safari & Chrome (WebKit) we also need to check thumbnail.complete
      // because onload will NOT be called if the thumb is already cached.
      // This interferes with nicer browsers that call onload regardless
      // of the cache situation.
      // Because we are counting images to know when they have all been loaded
      // using thumbnail.complete on nice browsers may cause a double count.
      // We need to add a property so that the event handler knows if it
      // has checked the thumb or not. This will be done by the handler.
      var thisimg = this.img;
      window.setTimeout(function() { help_around_WebKit_nasty(thisimg); },500);
//@@      if (thumbnail.complete)
//@@        onThumbImageLoad(thumbnail,"complete");
    }
    this.dispLast++;
    tidx++;
  }
  // [tidx] is the index of the last visible item
  expectedCount = tidx+1;

  www = dimTot + this.thumbBorder - this.interThumbSpacing;
  if (this.onThumbAreaSizeChange)
  {
    if (this.isVertical)
      containerH = www;
    else
      containerW = www;
    this.onThumbAreaSizeChange(containerW,containerH);
  }
  if (!doneLoading(this.onThumbAreaAfterRedraw))
  {
    abnormalLoadTimer = window.setTimeout(function() { forceDone=true; doneLoading(this.onThumbAreaAfterRedraw); },10000);
    loadTimer = window.setInterval(function() { doneLoading(this.onThumbAreaAfterRedraw); },500);
  }
  return(www);

  function help_around_WebKit_nasty(thisimg)
  {
    if (thisimg.complete)
    {
      onThumbImageLoad(thisimg,"complete");
    }
  }
  function onThumbImageLoad(ctrl,isErr)
  {
    // See comments above regarding WebKit and using ctrl.complete
    if (ctrl.hasBeenHandled)
      return;
    ctrl.hasBeenHandled = true;

    loadCount++;
    ctrl.parentNode.style.display='block';
    ctrl.style.visibility = 'visible';
  }
  function doneLoading(onDoneFunc)
  {
    if (expectedCount < 0 || loadCount!=expectedCount)
    {
      if (!forceDone)
        return(false);
    }
    if (loadTimer != null)
      window.clearInterval(loadTimer);
    if (abnormalLoadTimer != null)
      window.clearTimeout(abnormalLoadTimer);
    thumbManagerInstance.thumbLoadingInProgress = false;
    if (thumbManagerInstance.onThumbLoadingDone_Internal)
    {
      thumbManagerInstance.onThumbLoadingDone_Internal();
      delete thumbManagerInstance.onThumbLoadingDone_Internal;
    }
    if (onDoneFunc)
      onDoneFunc();
    return(true);
  }
}

SlideShowThumbManager.prototype.afterResize = function(stageAreaWidth,stageAreaHeight)
{
  this.thumbAreaWidth = (stageAreaWidth || stage.offsetWidth);
  this.thumbAreaHeight = (stageAreaHeight || stage.offsetHeight);
  return(this.newBatch(this.dispFirst));
}
SlideShowThumbManager.prototype.removeAllThumbs = function()
{
  if (typeof(this.prevImagesIdx)!='undefined')
    delete this.prevImagesIdx;
}

SlideShowThumbManager.prototype.jumpToIdx = function(idx)
{
  if (idx == this.currentIdx)
    return;

  var needNew = false;
  // Do we need a new set?
  if (this.dispFirst < this.dispLast)
  {
    if (idx < this.dispFirst || idx > this.dispLast)
      needNew = true;
  }
  else
  {
    if (idx < this.dispFirst && idx > this.dispLast)
      needNew = true;
  }


  var prevImagesObject = (typeof(this.prevImagesIdx)=='undefined') ? null : this.images[this.prevImagesIdx];
  if (needNew)
  {
    if (this.onThumbPointerChange && prevImagesObject != null)
      this.onThumbPointerChange(prevImagesObject,null);
    delete this.prevImagesIdx;
    prevImagesObject = null;
    this.newBatch(idx);
  }

  this.currentIdx = idx;
  if (this.onThumbPointerChange)
  {
    // Convert idx from an offset into the slides[] array
    // to an offset into the images[] array
    if (idx < this.dispFirst)
      idx += this.parentSlideShowManager.slides.length;
    idx -= this.dispFirst;
    if (this.images)
    {
      this.onThumbPointerChange(prevImagesObject,(idx >= 0) ? this.images[idx]:null);
    }
    this.prevImagesIdx = idx;
  }
}

//................................................................................................
// SlideShowSlide
//
//................................................................................................
function SlideShowSlide(listItem,imageDir)
{
  this.text = null;
  this.source = null;
  this.imgWidth = this.imgHeight = 0;
  this.errorLoading = false;
  this.listItemParent = listItem;
  this.thumbChild = null;

  if (listItem)
  {
    if (listItem.text)
    {
      this.text = (listItem.textIsHTML) ? listItem.text : htmlEncode(listItem.text);
    }
    if (listItem.source)
    {
      this.imgWidth  = (listItem.imgWidth || 0);
      this.imgHeight = (listItem.imgHeight || 0);
      this.source    = (imageDir || '')+listItem.source;
    }
  }
}

//................................................................................................
// SlideContainer
//
//................................................................................................
function SlideContainer(stage,container_id)
{
  this.div = Builder.node('div',
                          {
                            id: (stage.id||'stage')+'_container'+container_id,
                            className: 'slideShowImageContainerBaseStyle'
                          }
                         );
  this.img = Builder.node('img',
                          {
                            id: (stage.id||'stage')+'_slide'+container_id,
                            className: 'slideShowImageBaseStyle'
                          }
                          );
  this.slide = null;
  this.slideIdx = -1;
  stage.appendChild(this.div);
  this.div.appendChild(this.img);
  this.hide();
  this.img.style.position = 'absolute';
  this.img.style.left = '0px';
  this.img.style.top = '0px';
  this.imgRect = { left:0, top:0, width: 0, height: 0};
}
SlideContainer.prototype.hide = function()
{
  this.div.style.display = 'none';
}

SlideContainer.prototype.afterResize = function()
{
  // Warning: use of getDimensions() when the stage is hidden
  //          will result in 0 x 0
  var containerDim = this.div.getDimensions();
  var www = containerDim.width;
  var hhh = containerDim.height;

  if (www == 0 && hhh == 0)
  {
    this.needsResize = true;
    return;
  }
  delete this.needsResize;

  if (this.slide)
  {
    if (typeof(this.slide.imgWidth)=='number' && this.slide.imgWidth > 0)
      www = this.slide.imgWidth;
    if (typeof(this.slide.imgHeight)=='number' && this.slide.imgHeight > 0)
      hhh = this.slide.imgHeight;
    var ratio =
         ((containerDim.height * www) > (containerDim.width * hhh))
              ? (containerDim.width / www)
              : (containerDim.height / hhh);

    // Safari has a delay in rendering so that you cannot change the image width (for ex) then
    // reference the image width, as in:
    //        this.img.width = Math.floor(this.slide.imgWidth * ratio);
    //          ....
    //        this.img.style.left = Math.floor((containerDim.width - this.img.width)/2)+'px';
    // The latter does not work because this.img.width has not been updated yet

    this.imgRect.width  = Math.floor(www * ratio);
    this.imgRect.height = Math.floor(hhh * ratio);
    this.imgRect.left   = Math.floor((containerDim.width - this.imgRect.width)/2);
    this.imgRect.top    = Math.floor((containerDim.height - this.imgRect.height)/2);

    this.img.style.left = this.imgRect.left+'px';
    this.img.style.top  = this.imgRect.top+'px';
    this.img.width      = this.imgRect.width;
    this.img.height     = this.imgRect.height;
  }
  else
  {
    this.imgRect = { left:0, top:0, width:www, height:hhh};
    this.img.width = www;
    this.img.height = hhh;
  }
}
// Expects dims to have a .width and a .height
SlideContainer.prototype.setDimensions = function(dims)
{
  this.div.style.width = dims.width+'px';
  this.div.style.height = dims.height+'px';
  this.afterResize();
}

SlideContainer.prototype.setSlide = function(slide,onloadFunc,onerrorFunc)
{
  this.img.onload = null;
  this.img.onerror = null;
  this.img.onabort = null;
  this.img.src = null;
  this.img.alt = '';
  this.slide = (slide || null);
  this.afterResize();
  if (slide)
  {
    if (slide.source==null)
    {
      if (onerrorFunc)
        window.setTimeout(onerrorFunc,500); // So we can return from setSlide
      else
        this.img.alt = 'no image given';
    }
    else
    {
      if (onloadFunc)
        this.img.onload = onloadFunc;
      if (onerrorFunc)
      {
        this.img.onerror = onerrorFunc;
        this.img.onabort = onerrorFunc;
      }
      this.img.src = slide.source;
      if (this.isWebKitNasty)
      { // For Safari & Chrome (WebKit) we also need this:
        var thisimg = this.img;
        window.setTimeout(help_around_WebKit_nasty,500); // So we can return from setSlide
  //@@      if (onloadFunc && this.img.complete)
  //@@        window.setTimeout(onloadFunc,500); // So we can return from setSlide
      }
    }
  }
  function help_around_WebKit_nasty()
  {
    if (thisimg.complete)
    {
      onloadFunc();
    }
  }
}

//................................................................................................
// SlideShowManager
//
//................................................................................................

//
// SlideShowManager constructor
//     stage_id    the id of the element that will be the stage for the slide show
//     options     an object containing one or more of the following
//        list:      the slide show list (as described below)
//        autoPlay:  slide show beings in 'play' mode
//        loopShow:  slide show loops (ie next after last is the first; prev before 1st is the last)
//        fps:       frames per second
//        displayDuration:      how long to show a slide before moving to the next
//        transitionDuration:   duration of transition between slides
//      onSlidesCreated:    function to call when the slide definitions have been created
//      startImageLoad:     function to call when an image starts loading (e.g. to display a "loading")
//      endImageLoad:       function to call when an image loading completes (e.g. to hide a "loading")
//      onPlayModeChange:   function to call when the mode changes from play to pause
//                          called with parameter as either 'play' or 'pause'
//      onStartTransition:  function to call before a transition starts
//      onEndTransition:    function to call after a transition ends
//                          parameter (optional): true if transition was a reverse transition
//  Values given in the options override defaults and values given in the list
//  Default values
//        loopShow            false
//        autoPlay            false
//        fps                 100
//        displayDuration     5
//        transitionDuration  2
//  The list is an object containing one or more of the following
//        slides:    mandatory  an array of slide objects as described below
//        loopShow:  optional   slide show loops (ie next after last is the first; prev before 1st is the last)
//        fps:       optional   frames per second
//        displayDuration:      how long to show a slide before moving to the next
//        transitionDuration:   duration of transition between slides
//        withThumbnails:   optional    true if slide show with thumbnails
//  A slide object contains one or more of the following
//        source:    mandatory  path name for image
//        imgWidth:  mandatory  image width
//        imgHeight: mandatory  image height
//        text:      optional  text to associate with the image (also used for alt text)
//
function SlideShowManager(stage_id,options)
{
  try
  {
    if ( (this.stage = $(stage_id)) == null)
      throw "Invalid stage id "+stage_id+"]";
  }
  catch(ex)
  {
    alert("Failed creating slide show manager:\n"+ex);
    return;
  }

  this.isWebKitNasty = Prototype.Browser.WebKit;
  this.loopSlideShow = false;
  this.fps = 100;
  this.displayDuration = 5;
  this.transitionDuration = 2;
  this.playMode = 'pause';
  this.thumbManager = null;
  this.sortSlides = false;
  this.sortSlideCompare = null;
  this.sortRandom = false;

  this.removeAllSlides();

  this.containerA = new SlideContainer(this.stage,'A');
  this.containerB = new SlideContainer(this.stage,'B');
  this.containerA.isWebKitNasty = this.isWebKitNasty;
  this.containerB.isWebKitNasty = this.isWebKitNasty;
  this.onStageResize();
  this.containerSource = this.containerA;
  this.containerDest = this.containerB;

  // External hooks
  if (typeof(options.onSlidesCreated) != "undefined")
    this.onSlidesCreated = options.onSlidesCreated;
  if (typeof(options.startImageLoad) != "undefined")
    this.startImageLoad = options.startImageLoad;
  if (typeof(options.endImageLoad) != "undefined")
    this.endImageLoad = options.endImageLoad;
  if (typeof(options.onPlayModeChange) != "undefined")
    this.onPlayModeChange = options.onPlayModeChange;
  if (typeof(options.onStartTransition) != "undefined")
    this.onStartTransition = options.onStartTransition;
  if (typeof(options.onEndTransition) != "undefined")
    this.onEndTransition = options.onEndTransition;

  if (options.withThumbnails)
  {
    this.thumbManager = new SlideShowThumbManager(this,options.thumbOptions);
    this.thumbManager.isWebKitNasty = this.isWebKitNasty;
  }

  // Set these before adding items so we'll know what to do when added
  if (!options.sortRandom)
  {
    if (typeof(options.sortSlides) == "undefined")
    {
      if (typeof(options.sortSlideCompare) != "undefined")
      {
        this.sortSlides = true;
        this.sortSlideCompare = options.sortSlideCompare;
      }
    }
    else
    {
      this.sortSlides = options.sortSlides ? true : false;
      if (this.sortSlides && typeof(options.sortSlideCompare) != "undefined")
      {
        this.sortSlideCompare = options.sortSlideCompare;
      }
    }
  }
  else
  {
    if (typeof(options.sortSlides) == "undefined" || options.sortSlides)
    {
      this.sortSlides = true;
      this.sortRandom = true;
    }
  }
  this.addListItems(options.list);

  // Set these options after adding options.list so they will override
  // any settings found within the list
  //
  if (options.autoPlay)
    this.playMode = 'play';
  if (typeof(options.loopShow) != "undefined")
    this.loopSlideShow = options.loopShow;
  if (typeof(options.fps) != "undefined")
    this.fps = options.fps;
  if (typeof(options.displayDuration) != "undefined")
    this.displayDuration = options.displayDuration;
  if (typeof(options.transitionDuration) != "undefined")
    this.transitionDuration = options.transitionDuration;


  if (this.slides && this.slides.length > 0)
  {
    if (this.onSlidesCreated)
      this.onSlidesCreated();
    //... create thumbs??? already created...
  }
}
//.................................................................... Resize
SlideShowManager.prototype.onStageResize = function()
{
  var dims = this.stage.getDimensions();
  this.containerA.setDimensions(dims);
  this.containerB.setDimensions(dims);
}
SlideShowManager.prototype.needsResize = function()
{
  return(this.containerA.needsResize || this.containerB.needsResize);
}
//.................................................................... Content management
SlideShowManager.prototype.addListItems = function(listOrArrayOfLists)
{
  if (!listOrArrayOfLists)
    return;

  if (!Object.isArray(listOrArrayOfLists))
    listOrArrayOfLists = [ listOrArrayOfLists ];

  var listCount = listOrArrayOfLists.length;
  if (listCount == 0)
    return;

  var list;
  var slideFolder = null;
  var thumbFolder = null;

  for(var listIdx = 0; listIdx < listCount; listIdx++)
  {
    list = listOrArrayOfLists[listIdx];
    if (typeof(list) == "undefined" || !list)
      continue;
    if (typeof(list.slides) == "undefined" || !list.slides || list.slides.length==0)
      continue;

    if (typeof(list.loopShow) != "undefined")
      this.loopSlideShow = list.loopShow;
    if (typeof(list.durationSlide) != "undefined")
      this.displayDuration = list.durationSlide;
    if (typeof(list.durationTransition) != "undefined")
      this.transitionDuration = list.durationTransition;
    if (typeof(list.fps) != "undefined")
      this.fps = list.fps;

    slideFolder = null;
    thumbFolder = null;

    if (list.imagefolder)
    {
      slideFolder = list.imagefolder;
      if (!slideFolder.endsWith('/'))
          slideFolder += '/';
    }
    if (this.thumbManager && list.thumbfolder)
    {
      thumbFolder = list.thumbfolder;
      if (thumbFolder != null)
      {
        if (!thumbFolder.endsWith('/'))
            thumbFolder += '/';
      }
    }

    var tempArray;
    var newSlide;
    var c;

    tempArray = null;
    c = list.slides.length;
    for(var i=0; i < c; i++)
    {
      // Ignore invalid entries
      if (list.slides[i].source && typeof(list.slides[i].imgWidth)=='number' && typeof(list.slides[i].imgHeight)=='number')
      {
        if (tempArray == null)
          tempArray = new Array();
        newSlide = new SlideShowSlide(list.slides[i],slideFolder);
        tempArray.push(newSlide);
        if (this.thumbManager)
          newSlide.thumbChild = new SlideShowThumb(newSlide,thumbFolder,slideFolder);
      }
    }
    if (tempArray != null)
    {
      var sortIt = false;
      var cmpfunc = null;
      var randomSort = false;
      // Apply local sort options to tempArray
      // Set these before adding items so we'll know what to do when added
      if (!list.sortRandom)
      {
        if (typeof(list.sortSlides) == "undefined")
        {
          if (typeof(list.sortSlideCompare) != "undefined")
          {
            sortIt = true;
            cmpFunc = list.sortSlideCompare;
          }
        }
        else
        {
          sortIt = list.sortSlides ? true : false;
          if (sortIt && typeof(list.sortSlideCompare) != "undefined")
          {
            cmpFunc = list.sortSlideCompare;
          }
        }
      }
      else
      {
        if (typeof(list.sortSlides) == "undefined" || list.sortSlides)
        {
          sortIt = true;
          randomSort = true;
        }
      }
      if (sortIt)
      {
        if (randomSort)
          tempArray.randomOrder();
        else
          tempArray.sort((cmpFunc==null) ? this.DefaultSort : cmpFunc);
      }
      if (this.slides)
        this.slides = this.slides.concat(tempArray);
      else
        this.slides = tempArray;
    }
  }
  // Sort the result according to global sort options
  if (this.sortSlides)
  {
    if (this.sortRandom)
      this.slides.randomOrder();
    else
      this.slides.sort((this.sortSlideCompare==null) ? this.DefaultSort : this.sortSlideCompare);
  }

  // Now set each slide's "idx" property
  if (this.slides)
  {
    for(var i=this.slides.length-1; i >=0; i--)
      this.slides[i].idx = i;
  }
}
SlideShowManager.prototype.removeAllSlides = function()
{
  this.tooBusy = true;
  this.suspend();
  this.dropSuspendInfo();

  if (this.slides)
    delete this.slides;
  if (this.thumbManager)
    this.thumbManager.removeAllThumbs();
  this.slides = null;
  this.currentSlide = null;
  this.currentSlideIdx = -1;
  this.followingSlide = null;
  delete this.followingSlideIdx;
  delete this.slideLoading;
  this.tooBusy = false;
}
//.................................................................... Sorting
Array.prototype.randomOrder = function()
{
  var sifter = this.slice();
  var remain;
  var i = 0;
  while( (remain = sifter.length) > 1)
  {
    pick = Math.floor(Math.random()*remain);
    this[i] = (sifter.splice(pick,1))[0];
    remain = sifter.length;
    i++;
  }
  this[i] = sifter[0];
}
SlideShowManager.prototype.DefaultSort = function(a,b) { return(a.source.toLocaleLowerCase().localeCompare(b.source.toLocaleLowerCase())); };
//.................................................................... Slide loader
//
// Note: it is possible for the loader to be called again before its previous incarnation
// finishes loading its image (if the latter takes a while to load for ex). When this
// happens, the img tag's source will be changed from one name to another, and so:
//    the onload/onerror function may be called for a slide that has been replaced
//    the onload/onerror function of a slide may never be called which means the
//    startImageLoad user-hook will have been called but endImageLoad user-hook may
//    never be.
// To avoid these, we use this.slideLoading to identify the last slide stuck into the
// loader. The onload/onerror function verifies that its version on the slide is still
// valid. If it is not the same as this.slideLoading, it simply returns with no further
// ado: a subsequent (or previous) onload/onerror will match this.slideLoading and in
// effect complete.
// The completion may have happened before: image1 takes a long time to load and the
// loader is called with image2 which happens to be in cache. The latter will call onload
// before the former does.
//
SlideShowManager.prototype.moveToIdx = function(idx)
{
  if (this.tooBusy)
    return;

  this.clearAutoTimer();

  var wasTooBusy = this.tooBusy;

  if (idx < 0 || idx >= this.slides.length)
  {
    this.followingSlide = null;
    delete this.followingSlideIdx;
    this.containerDest.setSlide(null);
    this.tooBusy = wasTooBusy;
    if (this.slideLoading)
    {
      if (this.endImageLoad)
        this.endImageLoad(this.slideLoading);
      if (this.thumbManager)
        this.thumbManager.jumpToIdx(this.slideLoading.idx);
      delete this.slideLoading;
    }
    this.transition(true);
  }

  this.followingSlide = this.slides[idx];
  this.followingSlideIdx = idx;

  var slide = this.followingSlide;
  var handler = this;

  this.slideLoading = slide;
  if (this.startImageLoad)
    this.startImageLoad(this.slideLoading);

  this.containerDest.setSlide(slide,
                              function() { onSlideLoaded(false); },  // onload
                              function() { onSlideLoaded(true); });  // onerror
  this.tooBusy = wasTooBusy;

  function onSlideLoaded(isErr)
  {
    // Another instance, with the appropriate slide (i.e. the last one loaded)
    // may have been called earlier if the appropriate slide was quicker to load
    // This will have cleared handler.slideLoading.
    // Also, a subsequent call to move beyond the end or prior to the start of the
    // slide show would have cleared handler.slideLoading.

    // The following will be needed until Safari fixes its cached-image bug (in which
    // a cached image does not trigger an onload event). The following is needed
    // for other browsers because they are likely to call this handler twice.
    if (!handler.slideLoading)
      return;

    if (handler.slideLoading != slide)
      return;

    if (isErr)
    {
      with(handler.containerDest.img)
      {
        alt=(slide.source||'SLIDE SOURCE IS NULL')+' image not found';
        with(style)
        {
          color='red';
          fontWeight = 'bold';
          fontSize = '20pt';
        }
      }
    }
    slide.errorLoading = isErr;
    if (handler.endImageLoad)
      handler.endImageLoad(handler.slideLoading);
    if (handler.thumbManager)
      handler.thumbManager.jumpToIdx(handler.slideLoading.idx);
    delete handler.slideLoading;
    if (handler.thumbManager && handler.thumbManager.thumbLoadingInProgress)
      handler.thumbManager.onThumbLoadingDone_Internal = function() { handler.transition(true); };
    else
      handler.transition(true);
  }
}
SlideShowManager.prototype.moveToNext_Internal = function()
{
  if (this.tooBusy)
    return;
  var newIdx;
  if (typeof(this.followingSlideIdx) == "undefined")
    newIdx = this.currentSlideIdx + 1;
  else
    newIdx = this.followingSlideIdx;
  if (newIdx >= this.slides.length)
  {
    // -2 means end of slide show (as opposed to -1 which means slide show not started yet)
    newIdx = (this.loopSlideShow) ? 0 : -2;
  }
  if (this.transitionEffect)
  {
    this.suspend();
    this.resumeDuration = 0;
  }
  this.moveToIdx(newIdx);
}

//.................................................................... Suspend / Resume
SlideShowManager.prototype.dropSuspendInfo = function()
{
  delete this.suspended;
  delete this.saveOpacity;
  delete this.resumeDuration;
}
SlideShowManager.prototype.suspend = function()
{
  if (this.suspended)
    return;

  var wasTooBusy = this.tooBusy;
  this.tooBusy = true;

  this.clearAutoTimer();
  this.dropSuspendInfo();
  this.suspended = true;

  if (this.transitionEffect)
  {
    // If transitionEffect is not null, we are switching from one image
    // to the next.
    // The amount of time remaining in the transition can be deduced from
    // the current opacity:
    //      if Appear, remainTime = effectDuration * (1-opacity)
    //      if Fade,   remainTime = effectDuration * opacity
    Effect.Queue.remove(this.transitionEffect);
    if (this.transitionEffect.effects)
    {
      // 0 is Appear
      // 1 is Fade
      this.saveOpacity = [this.transitionEffect.effects[0].element.getOpacity(),
                         this.transitionEffect.effects[1].element.getOpacity()];
      this.resumeDuration = this.transitionDuration * this.saveOpacity[1];
    }
    else
    {
      // (this.currentSlide == null) ? Appear : Fade
      this.saveOpacity = [this.transitionEffect.element.getOpacity()];
      if (this.currentSlide==null)
      { // we have "Appear"
        this.resumeDuration = this.transitionDuration * (1-this.saveOpacity[0]);
      }
      else
      { // we have "Fade"
        this.resumeDuration = this.transitionDuration * this.saveOpacity[0];
      }
    }
    delete this.transitionEffect;
  }
  else
  {
    // If transitionEffect is null, we are displaying an image
    // and the autoTimer may be on
    // We have no idea how much time remains on the autoTimer.
  }
  this.tooBusy = wasTooBusy;
}
SlideShowManager.prototype.resume = function()
{
  if (!this.suspended)
    return;

  if (this.saveOpacity)
  {
    delete this.saveOpacity;
    this.transition();
  }
  else
  {
    this.enableMoves();
    if (this.Internal_onEndQuickTransition)
    {
      this.Internal_onEndQuickTransition();
      delete this.Internal_onEndQuickTransition;
      return;
    }
    if (this.playMode=='play' && this.displayDuration)
    {
      this.autoTimer = window.setTimeout(this.moveToNext_Internal.bind(this),this.displayDuration*1000);
    }
  }
  this.dropSuspendInfo();
}
//.................................................................... Transition
SlideShowManager.prototype.handlePlayModeAtEndOfTransition = function()
{
  if (this.playMode == 'play')
  {
    if (this.displayDuration)
    {
      this.autoTimer = window.setTimeout(this.moveToNext_Internal.bind(this),this.displayDuration*1000);
    }
    else
    {
      this.moveToNext_Internal();
    }
  }
}
// Warning:
//          if hookFunc is given, this does NOT resume auto-playing when the
//          appropriate transition ends. Although playMode is unchanged, the
//          move-to-next of playMode=='play' will NOT be triggered.
//          The hooked function should do this if needed
//
SlideShowManager.prototype.transitionQuicklyToMostVisible = function(hookFunc)
{
  if (!this.transitionEffect)
    return;

  if (hookFunc)
    this.Internal_onEndQuickTransition = hookFunc;
  else
    delete this.Internal_onEndQuickTransition;

  this.suspend();
  if (this.saveOpacity)
  {
    this.resumeDuration = 0;
    var revertIt = false;
    if (this.saveOpacity.length > 1)
    {
      // 0 is the one Appearing
      // 1 is the one Fading
      if (this.saveOpacity[0] < this.saveOpacity[1])
        revertIt = true;
    }
    else
    {
      if (this.currentSlide != null)
        revertIt = true;
    }
    if (revertIt)
    {
      this.reverseTransition();
    }
    else
    {
      this.resume();
    }
  }
  this.dropSuspendInfo();
}

//SlideShowManager.prototype.transitionQuicklyToNext = function()
//{
//  if ((!this.transitionEffect) || (this.tooBusy))
//    return;
//  this.suspend();
//  this.resumeDuration = 0;
//  this.resume();
//}
SlideShowManager.prototype.finishTransition = function(movingBack)
{
  var handler = this;
  if (!movingBack)
  {
    handler.currentSlide = handler.followingSlide;
    handler.currentSlideIdx = handler.followingSlideIdx;
    if (handler.containerSource == handler.containerA)
    {
      handler.containerSource = handler.containerB;
      handler.containerDest   = handler.containerA;
    }
    else
    {
      handler.containerSource = handler.containerA;
      handler.containerDest   = handler.containerB;
    }
  }
  handler.followingSlide = null;
  delete handler.followingSlideIdx;
  if (handler.transitionEffect)
    delete handler.transitionEffect;
  if (handler.onEndTransition)
    handler.onEndTransition((movingBack) ? true : false);
}
SlideShowManager.prototype.transition = function(isNormalTransition)
{
  if ((!this.currentSlide) && (!this.followingSlide))
     return;
  var handler = this;

  handler.clearAutoTimer();

  var effectDuration=0;
  if (this.suspended)
  { // Assume we are resuming
    effectDuration = handler.resumeDuration;
    delete handler.resumeDuration;
    if (handler.onResumeTransition)
      handler.onResumeTransition();
    this.dropSuspendInfo();
  }
  else
  {
    if (handler.playMode == 'play')
    {
      effectDuration = handler.transitionDuration;
      if (handler.onStartTransition)
        handler.onStartTransition();
    }
  }

  var setupFunc = (isNormalTransition) ? function() {handler.enableMoves(); }
                                       : null;
  if (handler.currentSlide == null)
  {
    handler.transitionEffect = new Effect.Appear(handler.containerDest.div,
                    {
                      fps: handler.fps,
                      duration: effectDuration,
                      afterSetup:  setupFunc,
                      afterFinish: function() { onTransitionEnded();}
                    });
  }
  else
  {
    if (handler.followingSlide == null)
    {
      handler.transitionEffect = new Effect.Fade(handler.containerSource.div,
                      {
                        fps: handler.fps,
                        duration: effectDuration,
                        afterSetup:  setupFunc,
                        afterFinish: function() { onTransitionEnded();}
                      });
    }
    else
    {
      handler.transitionEffect = new Effect.Parallel([
        new Effect.Appear(handler.containerDest.div,
                       {
                         sync: true,
                         fps: handler.fps,
                         duration: effectDuration
                       }),
        new Effect.Fade(handler.containerSource.div,
                        {
                          sync: true,
                          fps: handler.fps,
                          duration: effectDuration
                        })
        ],
      {
        fps: handler.fps,
        duration: effectDuration,
        afterSetup:  setupFunc,
        afterFinish: function()
        {
          onTransitionEnded();
        }
      }
      );
    }
  }
  function onTransitionEnded()
  {
    handler.finishTransition(false);
    if (handler.Internal_onEndQuickTransition)
    {
      handler.Internal_onEndQuickTransition();
      delete handler.Internal_onEndQuickTransition;
      return;
    }
    handler.handlePlayModeAtEndOfTransition();
  }
}
SlideShowManager.prototype.reverseTransition = function()
{
  if ((!this.currentSlide) && (!this.followingSlide))
     return;

  var handler = this;
  handler.clearAutoTimer();

  var effectDuration=0;
  if (handler.currentSlide == null)
  {
    handler.transitionEffect = new Effect.Fade(handler.containerDest.div,
                    {
                      fps: handler.fps,
                      duration: effectDuration,
                      afterFinish: function() { onTransitionEnded();}
                    });
  }
  else
  {
    if (handler.followingSlide == null)
    {
      handler.transitionEffect = new Effect.Appear(handler.containerSource.div,
                      {
                        fps: handler.fps,
                        duration: effectDuration,
                        afterFinish: function() { onTransitionEnded();}
                      });
    }
    else
    {
      handler.transitionEffect = new Effect.Parallel([
        new Effect.Fade(handler.containerDest.div,
                       {
                         sync: true,
                         fps: handler.fps,
                         duration: effectDuration
                       }),
        new Effect.Appear(handler.containerSource.div,
                        {
                          sync: true,
                          fps: handler.fps,
                          duration: effectDuration
                        })
        ],
      {
        fps: handler.fps,
        duration: effectDuration,
        afterFinish: function()
        {
          onTransitionEnded();
        }
      }
      );
    }
  }
  function onTransitionEnded()
  {
    handler.finishTransition(true);
    handler.dropSuspendInfo();
    handler.enableMoves();
    if (handler.Internal_onEndQuickTransition)
    {
      handler.Internal_onEndQuickTransition();
      delete handler.Internal_onEndQuickTransition;
      return;
    }
    handler.handlePlayModeAtEndOfTransition();
  }
}
//.................................................................... Next / Prev / Jump-to
SlideShowManager.prototype.enableMoves = function()
{
  delete this.busyMoving;
}
SlideShowManager.prototype.disableMoves = function()
{
  this.busyMoving = true;
}
SlideShowManager.prototype.jumpToSlideIdx = function(idx)
{
  if (this.tooBusy || this.busyMoving)
    return;

  if ((!this.slides) || (this.slides.length==0))
    throw "Slide show is empty";

  if (idx < 0 || idx >= this.slides.length)
    throw "Invalid jumpToSlideIdx: "+idx;

  this.disableMoves();
  if (this.transitionEffect)
  {
    var handler=this;
    this.transitionQuicklyToMostVisible(function(){ handler.moveToIdx(idx);});
    return;
  }
  this.moveToIdx(idx);
}

SlideShowManager.prototype.moveToNext = function()
{
  if (this.tooBusy || this.busyMoving)
    return;

  if ((!this.slides) || (this.slides.length==0))
    throw "Slide show is empty";

  this.disableMoves();
  this.moveToNext_Internal();
}
SlideShowManager.prototype.moveToPrev = function()
{
  if (this.tooBusy || this.busyMoving)
    return;

  if ((!this.slides) || (this.slides.length==0))
    throw "Slide show is empty";

  var newIdx;
  if (typeof(this.followingSlideIdx) == "undefined")
    newIdx = this.currentSlideIdx - 1;
  else
  {
    newIdx = this.followingSlideIdx - 1;
  }

  if (newIdx < 0)
  {
    if (this.loopSlideShow)
    {
      newIdx = this.slides.length-1;
    }
    else
    {
      newIdx = -1; // means slide show not started yet
    }
  }

  this.disableMoves();
  if (this.transitionEffect)
  {
    this.suspend();
    this.resumeDuration = 0;
  }
  this.moveToIdx(newIdx);
}
//.................................................................... Start / Pause / Play
SlideShowManager.prototype.clearAutoTimer = function()
{
  if (this.autoTimer)
  {
    window.clearTimeout(this.autoTimer);
    delete this.autoTimer;
  }
}
SlideShowManager.prototype.startSlideShow = function(startIdx)
{
  if (this.hasStarted())
    return;

  this.clearAutoTimer();
  this.dropSuspendInfo();
  this.containerA.hide();
  this.containerB.hide();
  this.currentSlide = null;
  this.currentSlideIdx = -1;
  this.followingSlide = null;
  delete this.followingSlideIdx;
  delete this.slideLoading;
  if (!startIdx)
    startIdx = 0;
  if (this.thumbManager)
    this.thumbManager.jumpToIdx(startIdx);
  this.jumpToSlideIdx(startIdx);
}
SlideShowManager.prototype.drop = function()
{
  this.clearAutoTimer();
  this.disableMoves();
  this.suspend();
  this.dropSuspendInfo();
  this.containerA.hide();
  this.containerB.hide();
  this.currentSlide = null;
  this.currentSlideIdx = -1;
  this.followingSlide = null;
  delete this.followingSlideIdx;
  delete this.slideLoading;
  this.removeAllSlides();
  this.containerA = null;
  this.containerB = null;
  this.containerSource = null;
  this.containerDest = null;
}
SlideShowManager.prototype.pause = function()
{
  if (this.playMode == 'play')
  {
    this.playMode = 'pause';
    if (this.transitionEffect)
      this.transitionQuicklyToMostVisible();
    else
      this.clearAutoTimer();
    if (this.onPlayModeChange)
      this.onPlayModeChange('pause');
  }
}
SlideShowManager.prototype.play = function(noMove)
{
  if (this.playMode == 'pause')
  {
    this.playMode = 'play';
    if (!noMove)
    {
      if (this.hasStarted())
        this.moveToNext();
      else
        this.startSlideShow();
    }
    if (this.onPlayModeChange)
      this.onPlayModeChange('play');
  }
}
SlideShowManager.prototype.hasStarted = function()
{
  return(  ((this.currentSlide != null) || (this.followingSlide != null)) ? true : false  );
}
SlideShowManager.prototype.isPaused = function()
{
  return((this.playMode == 'pause') ? true : false);
}
SlideShowManager.prototype.isPlaying = function()
{
  return((this.playMode == 'play') ? true : false);
}

