/**
 * A central manager where JavaScripts can register for and dispatch non-DOM events. */
var EventManager = {
  evaluatedScripts: {}, 
  
  /**
   * Simple way to add several observers at once.
   * Expects a naming convention of "on[event name]" for handlers.
   * 
   * For example, if the event name is "UserUpdated", the handler
   * name must be "onUserUpdated".
   * 
   * @param {Array} eventNames The names of the events to add observers for.
   * @param {Object} context The object that owns the event handlers
   */
  addObservers: function(eventNames, context) {
    var len = eventNames.length;
    for (var i = 0; i < len; i++) {
      var eventName = eventNames[i];
      this.addObserver(eventName, context['on' + eventName], context);
    }
  },

  /**
   * Simple way to remove several observers at once.
   * Expects a naming convention of "on[event name]" for handlers.
   * 
   * For example, if the event name is "UserUpdated", the handler
   * name must be "onUserUpdated".
   * 
   * @param {Array} eventNames The names of the events to add observers for.
   * @param {Object} context The object that owns the event handlers
   */
  removeObservers: function(eventNames, context) {
    var len = eventNames.length;
    for (var i = 0; i < len; i++) {
      var eventName = eventNames[i];
      this.removeObserver(eventName, context['on' + eventName], context);
    }
  },

  /**
   * Adds an observer or event handler.
   * 
   * @param {String} eventName The name of the event to observe.
   * @param {Function} handler The JavaScript function called when the event fires.
   *   Note the handler method signature should accept a single array of arguments.
   *   Example: function myHandler(args) {...}
   * @param {Object} [context] The context to use when invoking the handler.
   */
  addObserver: function(eventName, handler, context) {
    if (typeof(eventName) == "string" && handler) {
      if (typeof(handler) == "string") {
        handler = new Function(handler);
      }
      
      if (typeof(handler) != "function") {
        return;
      }
      
      var observers = this._events[eventName];
      
      // intialize observer list for first time
      if (!observers) {
        this._events[eventName] = [];
        observers = this._events[eventName];
      }
      
      context = context || window;
      var len = observers.length;
      
      for (var i = 0; i < len; i++) {
        var observer = observers[i];
        if (observer.handler == handler && observer.context == context){
          return; // observer already added
        }
      }

      observers.push({
        handler: handler,
        context: context
      });
    }
  },

  /**
   * Removes an observer. 
   * 
   * @param {String} eventName The name of the event.
   * @param {Function} handler The handler to remove from the observer list.
   * @param {Object} [context] The context assigned to the handler.
   */
  removeObserver: function(eventName, handler, context) {
    var observers = this._events[eventName];
    
    if (observers) {
      context = context || window;
      var index = -1;
      var len = observers.length;
      
      for (var i = 0; i < len; i++){
        var observer = observers[i];
        if (observer.handler == handler && observer.context == context){
          index = i;
          break;
        }
      }
      
      if (index >= 0) {
        observers.splice(index, 1);              
      }
    }
  },
  
  /**
   * Fires or dispatches an event.
   * 
   * @param {Function} eventName The name of the event to fire.
   * @param {Object, Array} arguments The argument or arguments to be passed to the event handlers.
   */
  fireEvent: function(eventName, arguments) {
    var errors = [];
    var observers = this._events[eventName];
    
    try {
      if (observers) {
        var len = observers.length;
        var args = [];

        if (arguments) {
          if (arguments instanceof Array || (arguments.length && !arguments.toUpperCase)) {
            var argLen = arguments.length;
            for (var i = 0; i < argLen; i++) {
              args.push(this._getArgumentValue(arguments[i]));
            }
          }
          else {
            args.push(this._getArgumentValue(arguments));
          }
        }

        for (var i = 0; i < len; i++) {
          var observer = observers[i];
          if (observer) {
            try {
              observer.handler.apply(observer.context, args);
            } 
            catch (ex) {
              errors.push({
                ex: ex,
                observer: observer
              });
            }
          }
        }
      }
      
      if (top != window && top.EventManager) {
        top.EventManager.fireEvent(eventName, arguments);
      }
    }
    catch (ex) {
      errors.push({ex: ex});
    }
    
    // alert any errors that may have occured in any of the handlers
    try {
      var errorLen = errors.length;
      if (errorLen > 0) {
        var msg = [];
        msg.push("EventManager: An error occurred while executing observers for: " + eventName);

        for (var i = 0; i < errorLen; i++) {
          var error = errors[i];
          if (error.ex) {
            msg.push("\n\n" + (i + 1) + ": " + error.ex.name + ", " + error.ex.message);
            if (error.observer) msg.push("\n\nHandler:\n" + error.observer.handler);
          }
        }

        alert(msg.join(""));
      }
    } catch (ex) {}
  },
  
  /**
   * Parses an XHR response and fires any events contained in the responseText.
   * Looks for elements with an attribute of "eval='true'".
   * The innerHTML of these elements should be executable JavaScript.
   * 
   * @param {Object} request
   */
  fireServerGeneratedEvents: function(request) {
    var meta = EventManager._parseResponseText(request.responseText);
    meta.externalStylesheets.invoke("inject");
    meta.externalScripts.invoke("inject");
    //meta.text.evalScripts();
    EventManager._evalScripts(meta.text);
    return meta.text.stripScripts();
  },
  
  /** 
   * Custom eval method.  Takes a unique key as an argument to ensure that subsequent calls 
   * with the same key are not executed more than once. */
  eval: function(key, evalString) {
    if (!this.evaluatedScripts[key]) {
      try {
        this.evaluatedScripts[key] = true;
        eval(evalString);
      } catch (ex) {
        alert("EventManager: Error occured when evaluating the script:\n\n" + evalString + "\n\n" + ex.name + "\n" + ex.message);
      }
    }
  },
  
  /**
   * Parses the html string and evaluates any scripts contained in it. 
   * Reports in detail which scripts failed and why. */
  _evalScripts: function(html) {
    var scripts = html.extractScripts();
    var len = scripts.length; 
    
    for (var i = 0; i < len; i++) {
      var script = scripts[i];
      
      try {
        eval(script);
      } catch (ex) {
        alert("EventManager: Evaluation of script failed!\n\n" + script + "\n\n" + ex.name + "\n" + ex.message);
      }
    }
    
    /*var scripts = [];
    var startMatch = html.match(/<script/i);
    
    while (startMatch) {
      var stopMatch = html.match(/<\/script>/i);
      
      if (stopMatch) {
        var stopIndex = stopMatch.index + 9;
        var script = html.substring(startMatch.index, stopIndex);
        html = html.substring(stopIndex);
        startMatch = html.match(/<script/i);
                
        start = script.indexOf(">") + 1;
        stop = script.lastIndexOf("<");
        var evalScript = script.substring(start, stop).replace(/\/\/\s*<!\[CDATA\[|\/\/\s*\]\]>/ig, "");
        
        try {
          eval(evalScript);
        } catch (ex) {
          alert("EventManager: Evaluation of script failed!\n\n" + evalScript + "\n\n" + ex.name + "\n" + ex.message);
        }

      } else {
        break;
      }
    }*/
  },
  
  _events: [],
  
  /** Used to identify JSON in a string value. */
  _jsonPattern: /^\{.*\}$/,
  
  /** Gets the correct argument value. Converts JSON strings to actual JSON objects. */
  _getArgumentValue: function(argument) {
    if (typeof(argument) == "string"){
      if (argument.match(this._jsonPattern)){
        return argument.evalJSON();
      }
    }
    
    return argument;  
  },
  
  /**
   * Parses an HTML string and extracts the external scripts and external stylesheets.
   * String parsing is necessary because Internet Explorer simply discards these elements
   * if they are injected into the DOM. Thank you IE.
   *
   * Returns a hash containing the "text" external scripts and css omitted, and array of 
   * external scripts and an array of external stylesheets.
   *
   * Requires that markup be well formed.  Primarily the script & link elements for external resources.
   * Examples: 
   *  <script type="text/javascript" src="script.js"></script>
   *  <link type="text/css" href="style.css" />
   */
  _parseResponseText: function(text) {
    try {
      var result = {text: text, externalScripts: [], externalStylesheets: []};

      var externalScripts = text.match(/<script.+src.+<\/script>/ig);
      var externalStylesheets = text.match(/<link.+\/>/ig);

      if (externalScripts) {
        var len = externalScripts.length;
        for (var i = 0; i < len; i++) {
          var script = externalScripts[i];
          result.externalScripts.push({html: script, inject: EventManager._injectExternalResource});
          text = text.replace(script, "");
        }
      }

      if (externalStylesheets) {
        var len = externalStylesheets.length;
        for (var i = 0; i < len; i++) {
          var stylesheet = externalStylesheets[i];
          result.externalStylesheets.push({html: stylesheet, inject: EventManager._injectExternalResource});
          text = text.replace(stylesheet, "");
        }
      }

      result.text = text;
      return result;
    } catch (ex) {
      alert("EventManager: Failed while parsing responseText!\n\n" + ex.name + "\n" + ex.message);
    }
  },
  
  /**
   * Extension method used to inject HTML strings that represent a single element 
   * into the HEAD of the document as an element. */
  _injectExternalResource: function() {
    try {
      var tagName = this.html.substring(0, this.html.match(/\s|>/).index)
      tagName = tagName.replace(/<|>/, "").strip().toLowerCase();
      var pattern = new RegExp("<" + tagName + "|" + tagName + ">|<\\/|\\/>|<|>", "ig");
      var attrs = this.html.replace(pattern, "").strip().split(" ");
      var attributes = {keys: []};
      var count = attrs.length;
      var uriAttr = null;

      for (var x = 0; x < count; x++) {
        var attr = attrs[x];
        var pair = attr.split("=");
        var key = pair[0];
        var value = null;
        if (pair.length > 1) value = pair[1].replace(/"/ig, "");
        
        if (value) {
          if (key.match(/src|href/i)) uriAttr = key;

          attributes.keys.push(key);
          attributes[key] = value;
        }
      }

      var head = $$("head")[0];

      if (!uriAttr || head.innerHTML.indexOf(attributes[uriAttr]) == -1) {
        var tag = document.createElement(tagName);    
        var len = attributes.keys.length;

        for (var i = 0; i < len; i++) {
          var key = attributes.keys[i];
          var value = attributes[key];
          tag.setAttribute(key, value);
        }

        head.appendChild(tag);
      }
    } catch (ex) {
      alert("EventManager: Failed to inject external resource into HEAD!\n\n" + this.html + "\n\n" + ex.name + "\n" + ex.message);
    }
  }
  
}