/* A JavaScript module to handle the loading of images from mutliple
 * locations.
 *
 * Depends on: for basic usage, nothing. For advanced usage where it
 * is desired to remember which hosts are offline then gaia_ws.js or
 * suitable replacement.
 *
 *
 * Method: An object is created which stores the details of the URLs
 * to try. In advanced usage the server ("node") associated with a URL
 * is also stored. The node class must support the methods
 * "isOnline()", "setOffline()" and "getUrl()".
 *
 * Some care is required to avoid cyclic references (and the problems
 * they cause for garbage collection). We ensure cyclic references are
 * avoided by storing references to the gaiaMirror object only in the
 * image callback functions (closures). The gaiaMirror object is NOT
 * permitted to store a reference to the image (that would cause
 * cyclic references). However, the callback functions do pass the
 * image object via the this "pointer".
 *
 * It is necessary to store details of the timeouts, they are stored
 * inside the gaiaMirror object.
 *
 * Several callbacks are supported to allow notification of
 * events. Use the setOnLoad(), setOnError() and setOnAbort() member
 * functions to register the callback. The callback functions are
 * called with the gaiaMirror object as the sole parameter.
 *
 * User data can be stored within the gaiaMirror object with the
 * setAttribute() and setAttributes() member functions, and is
 * retrieved with getAttribute() and getAttributes() member
 * functions. WARNING: storing references to the the gaiaMirror object
 * (or or objects which reference the gaiaMirror object) will cause
 * cyclic references and problems with garbage collection.
 */

if (!window.gaiaMirror)
  gaiaMirror = new Object;

gaiaMirror.missingImageUrl = null;

// milliseconds
gaiaMirror.defaultTimeout = 10e3; 

gaiaMirror.gaiaMirror = function (urls, options) {
  if (!options)
    options = new Object;

  // Create the list of URLs and nodes. Insert dummy first elements.
  if (options.nodes)
    this.nodes = [ null ].concat(options.nodes);
  else
    this.nodes = new Array;

  if (options.timeouts) {
    if (typeof options.timeouts == "number") {
      // use same timeout for all nodes
      this.timeouts = new Array;
      for (var i = 0; i < this.nodes.length; ++i)
	this.timeouts[i] = options.timeouts;
    }
    else
      this.timeouts = [ null ].concat(options.timeouts);
  }
  else
    this.timeouts = new Array;
  
  this.setOfflinePermitted = true;
  
  if (urls.constructor == String) {
    // Assume that the URL is relative and append it to the node URLs
    if (options.nodes) {
      this.urls = [ null ];
      for (var i = 1; i < this.nodes.length; ++i)  // first elem is null
	this.urls[i] = this.nodes[i].getUrl() + urls;
      
    }
    else
      // Cannot do much mirroring with only one place to try, but at
      // least the missing image can be inserted if necessary
      this.urls = [ null, urls ];
  }
  else
    // Must be an array of URLs
    this.urls = [ null ].concat(urls);

  // Copy other options
  if (options.missingImageUrl)
    this.missingImageUrl = options.missingImageUrl;
  else
    this.missingImageUrl = gaiaMirror.missingImageUrl;
  
  if (options.setOfflinePermitted !== null 
      && options.setOfflinePermitted !== void 0)
    this.setOfflinePermitted = options.setOfflinePermitted;
  else
    this.setOfflinePermitted = true;
    

  // Copy attributes
  this.setAttributes(options.attributes);
  
  // Copy handlers (use Handler in name to avoid confusion with member
  // functions)
  this.onLoadHandler = options.onLoad;
  this.onErrorHandler = options.onError;
  this.onAbortHandler = options.onAbort;

  // The ID of currently pending timeout
  this.timeoutId = null;
  


  // ### DEBUG
  this.title = null;

  // 0: never; 
  // 1: onload
  // 2: onload, and onerror when missing image URL set
  // 3: always
  if (options.makeVisible !== null && options.makeVisible !== void 0)
    this.makeVisible = options.makeVisible;
  else
    this.makeVisible = 3;
  
};


gaiaMirror.gaiaMirror.prototype.toString = function () {
  return "gaiaMirror: " + this.nodes.length + " node" 
  + (this.nodes.length != 1 ? "" : "s");
};


gaiaMirror.gaiaMirror.prototype.setMakeVisible = function (vis) {
  this.makeVisible = vis;
  return this;
};


// Try the next URL
gaiaMirror.gaiaMirror.prototype.init = function (img, retry) {
  if (img == null || img.tagName != "IMG")
    return false;
  
  if (!retry) {
    if (img.gaiaMirror) {
      // console.warn("Image already configured to use gaiaMirror, not proceeding");
      // return false;
      if (this.timeoutId !== null)
	clearTimeout(this.timeoutId);
      gaiaMirror.finished(img);
      img.gaiaMirror = true;
    }
    else
      // remember that this image is already associated with a
      // gaiaMirror object
      img.gaiaMirror = true;
  }

  // ### DEBUG
  if (this.title === null)
    this.title = img.title;
  
  // Remove the details associated with the first location
  this.urls.shift();
  this.nodes.shift();
  this.timeouts.shift();

  while (this.urls.length) {
    var node = this.nodes[0];
    var timeout = this.timeouts[0];
    
    if (node) {
      // this URL has a node associated with it
      if (!node.isOnline()) {
	// node offline, try next URL in the list
	this.urls.shift();
	this.nodes.shift();
	this.timeouts.shift();
	break;
      }
      
      if (!timeout)
	// fall back to the nodes own timeout value
	timeout = node.getTimeout();
    }
    
    if (!timeout)
      // fallb back to class timeout value
      timeout = gaiaMirror.defaultTimeout;
  
    var t = this;
    // Create a closure for the setTimeout function. Here it is not
    // possible to use the "this" pointer to refer to the image so we
    // must include a reference to the image. This doesn't create a
    // cyclic reference because the image doesn't contain a reference
    // to the timeout function.
    if (this.timeoutId !== null) {
      console.warn("timoutId not null: " + this.timeoutId);
    }
    this.timeoutId = setTimeout(function () { t.onTimeout(img) }, timeout);

    // Create similar closures for the image handlers. However, we
    // cnanot use a reference to the image as that would create cyclic
    // references. However, we can use the "this" pointer to refer to
    // the image.
    img.onload = function () { return t.onLoad(this) }; 
    img.onabort = function () { return t.onAbort(this) }; 
    img.onerror = function () { return t.onError(this) }; 
    img.src = this.urls[0];
    // ### DEBUG
    // img.title = this.title + ' ' + this.urls[0];
    return true;
  }

    
  // // clean up, remove handlers etc
  // gaiaMirror.finished();
  // No more to try. Clear all handlers
  img.onload = null; 
  img.onabort = null; 
  img.onerror = null; 
  
  // Set default missing image and modify title
  if (this.missingImageUrl) {
    img.src = this.missingImageUrl;
    if (this.makeVisible >= 2)
      img.style.visibility = "visible";
  }
  else
    if (this.makeVisible >= 3)
      img.style.visibility = "visible";

  if (this.title != '')
    img.title = this.title + " (Image not found)";
  else
    img.title = "Image not found";

  // Call the onError handler (if set)
  if (typeof this.onErrorHandler == "function")
    this.onErrorHandler(this, img);
  
  // Clear user-defined handler now that we are finished with
  // them. Any cyclic references they might contain are broken.
  this.onLoadHandler = this.onErrorHandler = this.onAbortHandler = null;

  return false;
};

gaiaMirror.gaiaMirror.prototype.onLoad = function (img) {
  if (this.timeoutId !== null) {
    clearTimeout(this.timeoutId);
    this.timeoutId = null;
  }

  // Clean up by removing handlers etc
  img.onload = null; 
  img.onabort = null; 
  img.onerror = null; 

  // Remove the flag marking it as controlled by gaiaMirror
  // delete img.gaiaMirror; // cannot do this because IE complains
  img.gaiaMirror = false;
 

  if (this.makeVisible >= 1)
    img.style.visibility = "visible";

  // Call the onLoad handler (if set)
  if (typeof this.onLoadHandler == "function")
    this.onLoadHandler(this, img);

  // Clear user-defined handler now that we are finished with
  // them. Any cyclic references they might contain are broken.
  this.onLoadHandler = this.onErrorHandler = this.onAbortHandler = null;

  return true;
};


gaiaMirror.gaiaMirror.prototype.onError = function (img) {
  if (this.timeoutId !== null) {
    clearTimeout(this.timeoutId);
    this.timeoutId = null;
  }
  this.init(img, true);
  return true;
};
  

gaiaMirror.gaiaMirror.prototype.onAbort = function (img) {
  if (this.timeoutId !== null) {
    clearTimeout(this.timeoutId);
    this.timeoutId = null;
  }

  // Image loading aborted so no need for the handlers
  img.onload = null; 
  img.onabort = null; 
  img.onerror = null; 

  // Cal the onAbort handler (if set)
  if (typeof this.onAbortHandler == "function")
    this.onAbortHandler(this, img);

  // Clear user-defined handler now that we are finished with
  // them. Any cyclic references they might contain are broken.
  this.onLoadHandler = this.onErrorHandler = this.onAbortHandler = null;

  // remove the flag marking it as controlled by gaiaMirror
  delete img.gaiaMirror;
  return true;
};
  
gaiaMirror.gaiaMirror.prototype.onTimeout = function (img) {
  if (this.timeoutId !== null) {
    console.warn("Clearing timeout ID " + this.timeoutId);
    clearTimeout(this.timeoutId);
    this.timeoutId = null;
  }
  
  if (this.setOfflinePermitted && this.nodes && this.nodes[0])
    this.nodes[0].setOffline();
  

  // try next location
  this.init(img, true);
  return true;
};


gaiaMirror.gaiaMirror.prototype.getAttribute = function (name) {
  return this.attributes[name];
};

gaiaMirror.gaiaMirror.prototype.getAttributes = function () {
  return this.attributes;
};

gaiaMirror.gaiaMirror.prototype.setAttribute = function (name, value) {
  var oldValue = this.attributes[name];
  this.attributes[name] = value;
  return oldValue;
};

gaiaMirror.gaiaMirror.prototype.setAttributes = function (obj) {
  var oldValue = this.attributes;
  this.attributes = new Object;
  
  for (var name in obj)
    this.attributes[name] = obj[name];
  return oldValue;
};


// To avoid confusion with member functions use "Handler" in the name
gaiaMirror.gaiaMirror.prototype.setOnLoad = function (f) {
  return this.onLoadHandler = f;
};

// To avoid confusion with member functions use "Handler" in the name
gaiaMirror.gaiaMirror.prototype.setOnError = function (f) {
  return this.onErrorHandler = f;
};

// To avoid confusion with member functions use "Handler" in the name
gaiaMirror.gaiaMirror.prototype.setOnAbort = function (f) {
  return this.onAbortHandler = f;
};


// Finished with this image. Have onabort cleanup
gaiaMirror.finished = function (img) {
  if (!img.gaiaMirror || img.onabort != "function")
    // not 'controlled' by gaiaMirror or onabort callback missing/incorrect
    return false;
  
  return img.onabort(img);
};


