// preloader.js
// Copyright (c) 2010 'Ili Butterfield. No permission is granted to copy,
// modify, or distribute this code in full or in part without prior written
// permission from the author.
//
// Object for easy preloading of images, with event hooks for actions to take
// place after loading.

// This class preloads the list of images whose URLs are given in the array
// urls. The images are loaded in sequential order, starting with the first
// element in urls and moving on when the image is loaded successfully or fails
// to load. The second optional argument is an object with any or all of the
// following properties set:
//
//   - keys: A list of keys to be used to access the images in the preloader.
//           The first string in keys will key to the first image in urls, and
//           so on. If this property is not set, then the URLs themselves will
//           be used as the keys.
//
//   - loadCallback: A function to call after any image is loaded. The function
//                   signature should look like this:
//                       function loadCallback(preloader, index)
//                   preloader is the preloader object, and index is the index
//                   of the image in urls that was loaded. This function does
//                   not need a return value, but if it returns true, then no
//                   further images will be loaded.
//
//   - errorCallback: A function to call if there's an error in loading an
//                    image. The function signature should look like this:
//                        function errorCallback(preloader, index)
//                    preloader is the preloader object, and index is the index
//                    of the image in urls that failed to load. This function
//                    does not need a return value, but if it returns true, then
//                    no further images will be loaded.
//
//   - allCallback: A function to call after all images have either loaded or
//                  errored out. The function signature should look like this:
//                      function allCallback(preloader, imagesHandled, errors)
//                  preloader is the preloader object, imagesHandled is the
//                  total number of images that were attempted to be loaded, and
//                  errors is an array containing the keys of all the images
//                  that were unable to be loaded. This callback will be called
//                  AFTER loadCallback or errorCallback is called for the final
//                  image to be loaded. If loadCallback or errorCallback returns
//                  true, this callback will be called with imagesHandled and
//                  errors counting only the images that were loaded.
function ImagePreloader(urls, args) {    
    this.preloadImage = function(index) {
        if (index == this.urls.length) {
            this.allCallback(this, index, this.errors);
            return;
        }
        
        var image = new Image();
        var preloader = this;
        image.onload = function() { preloader.imageLoaded(index); };
        image.onerror = function() { preloader.imageErrored(index); };
        image.src = this.urls[index];
        this.statuses[this.keys[index]] = PreloadStatus.loading;
        this.imageStore[this.keys[index]] = image;
    };
    
    this.imageLoaded = function(index) {
        var key = this.keys[index]
        this.statuses[key] = PreloadStatus.loaded;
        if (key in this.callbacks) {
            this.callbacks[key](this, key, this.imageStore[key]);
        }
                
        var stopLoading = this.loadCallback(this, index);
        if (stopLoading) {
            this.allCallback(this, index + 1, this.errors);
            return;
        } else {
           this.preloadImage(index + 1);
        }
    };
    
    this.imageErrored = function(index) {
        var key = this.keys[index];
        this.statuses[key] = PreloadStatus.errored;
        this.errors[this.errors.length] = key;
        if (key in this.callbacks) {
            this.callbacks[key](this, key, null);
        }
                
        var stopLoading = this.errorCallback(this, index);
        if (stopLoading) {
            this.allCallback(this, index + 1, this.errors);
            return;
        } else {
            this.preloadImage(index + 1);
        }
    };

    this.urls = urls;
    args = args || new Object();
    this.keys = args.keys || urls;
    this.loadCallback = args.loadCallback || function(preloader, index) {};
    this.errorCallback = args.errorCallback || function(preloader, index) {};
    this.allCallback = args.allCallback || function(preloader, imagesHandled, errors) {};
    
    this.imageStore = new Object();
    this.statuses = new Object();
    this.callbacks = new Object();
    this.errors = [];
    
    this.preloadImage(0);
}
var PreloadStatus = {unloaded: 0, loading: 1, loaded: 2, errored: 3};

// Returns the Image object corresponding to the key provided in the argument
// key, or null if the image hasn't loaded yet or there was an error in loading
// it. If the image may not be loaded yet and you want to wait until it is, use
// doAfterLoaded().
ImagePreloader.prototype.getImage = function(key) {
    if (!(key in this.statuses) || (this.statuses[key] != PreloadStatus.loaded)) {
        return null;
    } else {
        return this.imageStore[key];
    }
}

// Calls the function callback after the Image object corresponding to the key
// provided in the argument key has loaded or failed to load. The function
// signature should look like this:
//    function callback(preloader, key, image)
// preloader is the preloader object, key is the key of the image, and image is
// either the Image object if it loaded successfully or null if it didn't. If
// this method was previously called and the callback has not been called yet
// (so the image is still loading), then the callback will be replaced with the
// current one. This callback will be called BEFORE loadCallback or
// errorCallback, if either was provided when the preloader was created.
ImagePreloader.prototype.doAfterLoaded = function(callback, key) {
    if (key in this.statuses && (this.statuses[key] == PreloadStatus.loaded)) {
        callback(this, key, this.imageStore[key]);
    } else {
        this.callbacks[key] = callback;
    }
}

