/*
 * outlet_core.js
 *
 * Provide the core outlet functionality to javascript extensions -- act as
 * a liason between javascript and flash extensions.
 *
 */


function OutletEvent(type, data){
  this.transferClass = "OutletEvent";
  this.type = type;
  this.data = data || {};
}



function OutletExtension(extensionDescription){
  this.extensionDescription = extensionDescription;
  this.typeToListenerDict = {};

  this.init = function(){};

  this.getDescription = function(){
    return this.extensionDescription;
  };

  this.dispatchEvent = function(event){
    event.extensionId = this.extensionDescription.id;
    var listeners = (this.typeToListenerDict[event.type]) || [];
    $.each(listeners, function(i, ea){
	     ea(event);
	   });
  };

  this.addEventListener = function(type, listener){
    this.typeToListenerDict[type] = this.typeToListenerDict[type] || [];
    (this.typeToListenerDict[type]).push(listener);
  };

  this.removeEventListener = function(type, listener){
    var listeners = this.typeToListenerDict[type];
    if(listeners && (listeners.indexOf(listener) > -1)){
      listeners.splice(listeners.indexOf(listener), 1);
    }
  };

}



function OutletImpl(){
  var _this = this;
  this.pendingExtensionsQ = [];
  this.jsExtensions = {};
  this.keyToFuncDict = {};
  this.typeToListenerDict = {};
  this.scriptNodeListeners = [];
  this.flashEmbedId = window.outletFlashEmbedId;

  this.OUTLET_INITED = "outletInited";
  this.OUTLET_INIT_FAILED = "outletInitFailed";
  this.OUTLET_JS_EXTENSION_INSTANTIATED = "outletJSExtensionInstantiated";
  this.OUTLET_JS_LOADED = "outletJavascriptLoaded";

  /* Triggers download of extension by inserting a script element into the document head.
   * Save the extension description for later, so that when the extension class is loaded, we
   * can create an instance with the properer extension description.
   */
  this.insertJavascriptOutletExtension = function(extensionDescription){
    extensionDescription.outlet = this;
    this.pendingExtensionsQ.push(extensionDescription);
    this.insertJavascriptForExtension(extensionDescription.url);
  };



  /* Triggers download of a javascript source file by inserting a script element into the document head.
   */
  this.insertJavascriptForExtension = function(url){
    var callback = function(){
      _this.dispatchEvent(new OutletEvent("outletJavascriptLoaded", { url: url }));
    };
    var node = document.createElement("script");
    if (node.addEventListener){
      node.addEventListener("load", callback, false);
    }
    else{
      node.onreadystatechange = function() {
	if (this.readyState == "complete" || this.readyState == "loaded") {
	  callback();
	}
      };
    }
    this.scriptNodeListeners.push({node: node, listener: callback});
    node.type ="text/javascript";
    var noCache = Math.floor(Math.random() * 10000000);
    node.src = url + "?" + noCache;
    $first("head").appendChild(node);
    node = null;
  };



  /* Get an extension instance by name. Return null if
   * extension doesn't exist
   */
  this.getOutletExtension = function(extensionId){
    if(this.jsExtensions[extensionId]){
      return this.jsExtensions[extensionId];
    }
    else {
      return this.getFlashOutletExtension(extensionId);
    }
  };


  /* Get an array of all outlet extensions that implement the provided interface/class name.
   */
  this.getOutletExtensionsByType = function(typeName){
    var found = [];
    for(var extensionId in this.jsExtensions){
      var ext = this.jsExtensions[extensionId];
      if(ext.getDescription().classDef.match(typeName + "$")){
	found.push(ext);
      }
    }
    var foundFlashExtensions = this.getFlashOutletExtensionsByType(typeName);
    return found.concat(foundFlashExtensions);
  };


  /* Add an event listener to Outlet
   */
  this.addEventListener = function(type, listener){
    if(this.flashOutletIsAvailable()){
      this.applyFlashOutletMethod("addEventListener", [type, listener]);
    }
    else{
      this.typeToListenerDict[type] = this.typeToListenerDict[type] || [];
      (this.typeToListenerDict[type]).push(listener);
    }
  };


  /* Dispatch an Outlet event
   */
  this.dispatchEvent = function(event){
    this.applyFlashOutletMethod("dispatchEvent", [event]);
  };


  /* Invoked by flash to send an event to pure js listeners */
  this.dispatchEventToPureJSListeners = function(event){
    var listeners = (this.typeToListenerDict[event.type]) || [];
    $.each(listeners, function(i, ea){
	     ea(event);
	   });
  };


  /* A helper for making an http request, facilitated by flash.
   * options is of the form {url: "", headers: [], method: "", data: {} }
   * onSuccess will be aplied to the retrieved data
   * onFailure will be aplied to any error descriptor received
   */
  this.httpRequest = function(options, onSuccess, onFailure){
    if(this.flashOutletIsAvailable()){
      (_this.getFlashEmbed()).applyUtilityMethod("httpRequest", this.prepArgsForAS3([options, onSuccess, onFailure]));
    }
  };


  /* Must be invoked by each extension script to signal that the provided extension class is available
   * for instantiation.
   */
  this.provideExtension = function(classDef, extensionClass){
    var pending = $.grep(this.pendingExtensionsQ, function(desc){ return desc.classDef == classDef; });
    $.each(pending, function(i, desc){
	     extensionClass.prototype = new OutletExtension(desc);
	     extensionClass.prototype.constructor = extensionClass;
	     extensionClass.prototype.parent = OutletExtension.prototype;
	     var newExtension = new (extensionClass)(desc);
	     _this.jsExtensions[desc.id] = newExtension;
	     _this.dispatchEvent(new OutletEvent("outletJSExtensionInstantiated", { extensionId: desc.id }));
	   });
    this.pendingExtensionsQ = $.grep(this.pendingExtensionsQ, function(desc){ return desc.classDef == classDef; }, true);
  };



  /* Given an array of arguments, freshly received from flash,
   * process them into arguments suitable for javascript.
   */
  this.processArgsFromAS3 = function(args){
    var processedArgs = [];
    $.each(args, function(i, arg){
	     processedArgs.push(_this.processValueFromAS3(arg));
	   });
    return processedArgs;
  };


  /* Process a value from AS3 into a value suitable for javascript*/
  this.processValueFromAS3 = function(arg){
    if(arg && arg.constructor == String){
      return unescape(arg);
    }
    else if(arg instanceof Object && arg.transferClass == "Function"){
      var proxyFunc = function(){
	var argsAsArray = [];
	for(var i = 0; i < arguments.length; i++){
	  argsAsArray.push(arguments[i]);
	}
	if(_this.flashOutletIsAvailable()){
	  return _this.processValueFromAS3((_this.getFlashEmbed()).applyWrappedFunction(arg.key, _this.prepArgsForAS3(argsAsArray)));
	}
      };
      if(this.keyToFuncDict[arg.key]){
	return this.keyToFuncDict[arg.key];
      }
      else{
	this.keyToFuncDict[arg.key] = proxyFunc;
	return proxyFunc;
      }
    }
    else{
      return arg;
    }
  };



  /* Given an array of arguments, prepare them to be passed to flash.
   * Specifically, this involves special handling for function objects.
   */
  this.prepArgsForAS3 = function(args){
    var processedArgs = [];
    $.each(args, function(i, arg){
	     processedArgs.push(_this.prepValueForAS3(arg));
	   });
    return processedArgs;
  };



  /* Process a value from javascript into a value suitable for AS3 */
  this.prepValueForAS3 = function(arg){
    if(arg instanceof Function){
      // Should use existing key,func pair if it exists
      // TODO No need for this to be linear
      var found = false;
      for(var keyStr in _this.keyToFuncDict){
	if(_this.keyToFuncDict[keyStr] == arg){
	  return { transferClass: "Function",  key: keyStr };
	  found = true;
	}
      }
      if(!found){
	var key = String(Math.random()) + "wrappedFuncKey" + String((new Date()).getTime());
	_this.keyToFuncDict[key] = arg;
	return { transferClass: "Function", key: key };
      }
    }
    else{
      return arg;
    }
  };



  /* Invoke a method on an outlet extension.
   * This function is called by flash to invoke methods on js extensions.
   */
  this.applyOutletExtensionMethod = function(extensionId, methodName, args){
    var ext = this.getOutletExtension(extensionId);
    if(ext && (ext[methodName] instanceof Function) && args){
      return this.prepValueForAS3(ext[methodName].apply(ext, this.processArgsFromAS3(args)));
    }
    else{
      return null;
    }
  };


  /* Read a property of an outlet extension.
   * This function is called by flash to read properties of js extensions.
   */
  this.readOutletExtensionProperty = function(extensionId, name){
    var ext = this.getOutletExtension(extensionId);
    if(ext && (name in ext)){
      return ext[name];
    }
    else{
      return null;
    }
  };


  /* Write a property of an outlet extension.
   * This function is called by flash to write properties of js extensions.
   */
  this.writeOutletExtensionProperty = function(extensionId, name, value){
    var ext = this.getOutletExtension(extensionId);
    if(ext && (name in ext)){
      return ext[name] = (this.processArgsFromAS3([value]))[0];
    }
    else{
      return null;
    }
  };


  /* Invoke a javascript function by functionKey.
   * This function is called by flash to invoke functions that are proxied on flash side.
   */
  this.applyWrappedFunction = function(functionKey, args){
    var func = this.keyToFuncDict[functionKey];
    if(func instanceof Function){
      return func.apply(this, this.processArgsFromAS3(args));
    }
    else{
      return null;
    }
  };



  /* Retreive a proxy representation of a flash outlet extension.
   * Returns null if extension does not exist.
   */
  this.getFlashOutletExtension = function(extensionId){
    if(this.flashOutletIsAvailable()){
      var desc = (_this.getFlashEmbed()).getFlashOutletExtension(extensionId);
      if(desc){
	return _this.proxyForFlashExtensionDescription(desc);
      }
    }
    return null;
  };



  /* Retreive an array of proxy representations of flash outlet extensions, by type.
   */
  this.getFlashOutletExtensionsByType = function(typeName){
    var found = [];
    if(this.flashOutletIsAvailable()){
      var descs = (_this.getFlashEmbed()).getFlashOutletExtensionsByType(typeName);
      $.each(descs, function(i, desc){
	       found.push(_this.proxyForFlashExtensionDescription(desc));
	     });
    }
    return found;
  };



  /* Given the raw descriptive representation of a flash extension, create and return a proxy
   * for interacting with the flash-side extension.
   */
  this.proxyForFlashExtensionDescription = function(desc){
    var proxy = {};
    var extensionId = desc.extensionId;
    $.each(desc.methods, function(i, methName){
	     proxy[methName] = function(){
	       var argsAsArray = [];
	       for(var i = 0; i < arguments.length; i++){
		 argsAsArray.push(arguments[i]);
	       }
	       return _this.applyFlashOutletExtensionMethod(extensionId, methName, argsAsArray);
	     };
	   });
    return proxy;
  };

  /* Helper for invoking a method on the flash outlet Outlet
   */
  this.applyFlashOutletMethod = function(methName, args){
    return (this.getFlashEmbed()).applyOutletMethod(methName, this.prepArgsForAS3(args));
  };


  /* Helper for invoking a method on a flash-side outlet extension
   */
  this.applyFlashOutletExtensionMethod = function(extensionId, methName, args){
    return (this.getFlashEmbed()).applyOutletExtensionMethod(extensionId, methName, this.prepArgsForAS3(args));
  };
  
  /* Return the data in from the <application> element in the XML configuration file
  */
  this.getApplicationConfiguration = function(){
	return this.applyFlashOutletMethod("getApplicationConfiguration", []);
  }


  /* Return true if the flash Outlet is available in the current page.
   */
  this.flashOutletIsAvailable = function(){
    return (this.flashEmbedId && ($("#" + this.flashEmbedId).length == 1));
  };

  /* Return true is this outlet is available, useful for testing.
   */
  this.isAvailable = function(){
    return true;
  };

  /* Return a reference to the flash embed element
   */
  this.getFlashEmbed = function(){
    return $("#" + this.flashEmbedId).get(0);
  };


  /* Sets the html id of the Outlet flash movie.
   * This function should be invoked *by* the flash movie, automatically.
   */
  this.setFlashEmbedId = function(objectId){
    this.flashEmbedId = objectId || this.flashEmbedId;
  };


  /* Reset all state in this js outlet, useful for unit testing.
   */
  this.reset = function(){
    this.pendingExtensionsQ = [];
    this.flashEmbedId = null;
    this.keyToFuncDict = {};
    $.each(this.scriptNodeListeners, function(i, item){
	     if(item.node.removeEventListener){
	       item.node.removeEventListener("load", item.listener, false);
	     }
	     else{
	       item.node.onreadystatechange = null;
	     }
	   });
    this.scriptNodeListeners = [];
    $.each(this.jsExtensions, function(i, ext){
	     if("destroy" in ext){
	       ext.destroy();
	     }
	   });
    this.jsExtensions = {};
  };


  /* Log a string to the master outlet log
   */
  this.log = function(string){
    this.applyFlashOutletMethod("log", [string]);
  };


  /* Set the logger function
   */
  this.setLogger = function(logger){
    this.applyFlashOutletMethod("setLogger", [logger]);
  };


  /* Return true if an active internet connection is available.
   */
  this.isOnline = function(){
    return this.applyFlashOutletMethod("isOnline", []);
  };


}

var Outlet = null;
function instantiateJavascriptOutlet(){
  Outlet = new OutletImpl();
}



/***** General Utilities *******/

function assert(desc, pred){
  var result = false;
  try{
    result = pred();
  }
  catch(e){
    result = false;
  }
  if(!result){
    alert("FAILED ASSERTION: " + desc);
  }
}

function $first(aString){
  return $(aString).get(0);
}

function $last(aString){
  var result = $(aString);
  return result.get(result.length - 1);
}

if(!Array.indexOf){
  Array.prototype.indexOf = function(obj){
    for(var i=0; i<this.length; i++){
      if(this[i]==obj){
	return i;
      }
    }
    return -1;
  };
}


