function ImageScroller( parentElement, imageUrls, options )
{
  options = options || {};

  this.parentElement    = parentElement;
  this.imageUrls        = imageUrls;
  this.images           = null;
  this.lastUpdateTime   = null;
  this.nextImageIndex   = 0;
  this.cells            = new ArrayList();
  
  this.context          = options.context || document;
  this.timer            = options.timer || new Timer();
  this.width            = options.width || 800;
  this.height           = options.height || 600;
  this.loaderImageUrl   = options.loaderImageUrl || "loader.gif";
  this.imageLoader      = options.imageLoader || new ImageLoader();
  this.speed            = options.speed || 20; // pixels per second
  this.vertical         = options.vertical || false;
  this.reverse          = options.reverse || false;

  this.initializeFrame();
  this.initializeLoader();
  this.loadImages();
}

ImageScroller.prototype =
{
  loadImages: function()
  {
    var operation = this.imageLoader.loadMultiple( this.imageUrls );
    operation.completed.attach( this.onImageLoaderLoaded.bind( this ) );
    operation.start();
  },
  
  onImageLoaderLoaded: function()
  {
    this.images = this.imageLoader.getImages();
    this.initializeViewport();
    this.fill();
    this.frame.removeChild( this.loaderDiv );
    this.timer.tick.attach( this.onTimerTick.bind( this ) );
    this.timer.start();
  },
  
  initializeFrame: function()
  {
    this.frame = this.context.createElement( "div" );
    this.parentElement.appendChild( this.frame );
  },

  initializeLoader: function()
  {
    var image;

    this.loaderDiv = this.context.createElement( "div" );
    this.loaderDiv.style.position = "absolute";

    image = this.context.createElement( "img" );
    image.src = this.loaderImageUrl;

    this.loaderDiv.appendChild( image );
    this.frame.appendChild( this.loaderDiv );
  },
  
  initializeViewport: function()
  {
    var viewport;
    
    viewport = this.context.createElement( "div" );
    viewport.style.position = "relative";
    viewport.style.width = new Length( this.width ).toString();
    viewport.style.height = new Length( this.height ).toString();
    viewport.style.overflow = "hidden";
    
    this.frame.appendChild( viewport );  
    this.viewport = viewport;
  },
  
  shift: function( delta )
  {
    var i;
    var cell;
    
    for ( i = 0; i < this.cells.getLength(); ++i )
    {
      cell = this.cells.get( i );
      cell.setNear( cell.getNear() - delta );
    }
  },
  
  fill: function()
  {
    if ( this.images.length > 0 )
    {
      while ( !this.isFull() )
      {
        this.addCell( new Cell( this.getNextImage(), this ) );
      }
    }
  },  
  
  prune: function()
  {
    var i;
    var cell;
    
    for ( i = 0; i < this.cells.getLength(); ++i )
    {
      cell = this.cells.get( i );
      
      if ( cell.hasExited() )
      {
        this.removeCell( cell );
      }
      else
      {
        break;
      }
    }
  },
  
  getLong: function()
  {
    return this[ this.vertical ? "height" : "width" ];
  },
  
  getShort: function()
  {
    return this[ this.vertical ? "width" : "height" ];
  },

  isFull: function()
  {
    return ( this.cells.getLength() > 0 ) &&
      ( !this.getLastCell().hasEntered() );
  },
  
  addCell: function( cell )
  {
    var near = ( this.cells.getLength() == 0 ) ? 0 : this.getLastCell().getFar();
    this.cells.add( cell );
    cell.setNear( near );
    this.viewport.appendChild( cell.getElement() );
  },
  
  removeCell: function( cell )
  {
    this.cells.remove( cell );
    this.viewport.removeChild( cell.getElement() );
  },
  
  getNextImage: function()
  {
    if ( this.images.length == 0 )
    {
      throw "Can't get next image because there are no images";
    }
    
    var image = this.images[ this.nextImageIndex ];
    this.nextImageIndex = ( this.nextImageIndex + 1 ) % this.images.length;
    return image;
  },

  getLastCell: function()
  {
    return this.cells.get( this.cells.getLength() - 1 );
  },
 
  onTimerTick: function()
  {
    this.update();
  },

  update: function()
  {
    var delta;
    
    if ( this.lastUpdateTime !== null )
    {
      delta = Math.round( ( new Date().getTime() - this.lastUpdateTime ) / 1000 * this.speed )
      this.shift( delta );
      this.prune();
      this.fill();
    }

    this.lastUpdateTime = new Date().getTime();
  }
};

function Cell( image, scroller )
{
  var img;
  
  img = scroller.context.createElement( "img" );
  img.src = image.src;
  img.width = image.width;
  img.height = image.height;
  img.style.position = "absolute";

  this.img = img;
  this.scroller = scroller;
  this.nearCssProperty = this.getNearCssProperty();
  this.setNear( 0 );
}

// "near" is position of boundary in the direction of the scroll
//   i.e. in a right to left scroll, "near" is left; bottom to top "near" is top

// the "long" axis is parellel to direction of movement

Cell.prototype =
{
  getElement: function()
  {
    return this.img;
  },
  
  getNear: function()
  {
    return this.near;
  },
  
  setNear: function( value )
  {
    this.near = value;
    this.img.style[ this.nearCssProperty ] = value.toString() + "px";
  },
  
  getNearCssProperty: function()
  {
    if ( this.scroller.vertical )
    {
      return this.scroller.reverse ? "bottom" : "top";
    }
    else
    {
      return this.scroller.reverse ? "right" : "left";
    }
  },
  
  getFar: function()
  {
    return this.getNear() + this.getLong();
  },
  
  getLong: function()
  {
    return this.img[ this.scroller.vertical ? "height" : "width" ];
  },
  
  getShort: function()
  {
    return this.img[ this.scroller.vertical ? "width" : "height" ];
  },
  
  // _whole_ cell has exited viewport
  hasExited: function()
  {
    return ( this.getFar() < 0 );
  },
  
  // _whole_ cell has entered viewport
  hasEntered: function()
  {
    return ( this.getFar() < this.scroller.getLong() );
  }
};

