/*
 * Copyright 2006 Matthew Eernisse (mde@fleegix.org)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
*/
if (typeof fleegix == 'undefined') { var fleegix = {}; }
fleegix.event = new function () {
  // List of handlers for event listeners
  var listenerCache = [];
  // List of channels being published to
  var channels = {};

  // If set to true the listener registry is set on
  // the obj itself instead of being hidden on the
  // handler function. This makes it visible/ennumerable
  // but other toolkits' event systems will be less likely
  // to break stuff if it's turned on
  this.compatibilityMode = false;

  this.listen = function () {
    var obj = arguments[0]; // Target object for the new listener
    var meth = arguments[1]; // Method to listen for
    var compatMode = this.compatibilityMode;

    // Simple function
    var r = {}; // package of info about what to execute
    var o = {}; // options -- stopPropagation or preventDefault
    if (typeof arguments[2] == 'function') {
      r.method = arguments[2];
      o = arguments[3] || {};
    }
    // Object and method
    else {
      r.context = arguments[2];
      r.method = arguments[3];
      o = arguments[4] || {};
    }

    if (!obj) { 
      throw new Error('fleegix.listen called on an object (' +
        obj + ') that does not exist.'); }

    // Add dummy onmousewheel that allows us to fake
    // old-school event registration with Firefox's
    // XUL mousewheel event
    if (meth == 'onmousewheel') {
      if (window.addEventListener &&
        typeof obj.onmousewheel == 'undefined') {
        obj.onmousewheel = null;
      }
    }

    // Look to see if there's already a handler and
    // registry of listeners
    var listenReg;
    if (this.compatibilityMode) {
      if (obj[meth] && obj._fleegixEventListenReg) {
        listenReg = obj._fleegixEventListenReg[meth];
      }
      else {
        listenReg = null;
      }
    }
    else {
      listenReg = obj[meth] ? obj[meth].listenReg : null;
    }
    // Create the registry of handlers if it does not exist
    // It will contain all the info needed to run all the attached
    // handlers -- hanging this property on the actual handler
    // (e.g. onclick, onmousedown, onload) to avoid adding visible
    // properties on the object.
    // -----------------
    if (!listenReg) {
      listenReg = {};
      // The original obj and method name
      listenReg.orig = {};
      listenReg.orig.obj = obj;
      listenReg.orig.methName = meth;
      // Preserve any existing listener
      if (obj[meth]) {
        listenReg.orig.methCode = obj[meth];
      }
      // Array of handlers to execute if the method fires
      listenReg.after = [];
      // Replace the original method with the executor proxy
      obj[meth] = function () {
        var reg = compatMode ? obj._fleegixEventListenReg[meth] : obj[meth].listenReg;
        if (!reg) {
          if (obj['_' + meth + '_suppressErrors']) {
            return false;
          }
          else {
            throw new Error('Cannot execute handlers for ' + obj + '  "' +
              meth + '". Something' +
              ' (likely another JavaScript library) has' +
              ' removed the fleegix.event.listen handler registry.');
          }
        }
        var args = [];
        for (var i = 0; i < arguments.length; i++) {
          args.push(arguments[i]);
        }

        // Try to be a good citizen -- preserve existing listeners
        // Execute with arguments passed, in the right execution context
        if (reg.orig.methCode) {
          reg.orig.methCode.apply(reg.orig.obj, args);
        }
        // DOM events
        // Normalize the different event models
        var ev = null;
        if (obj.attachEvent || obj.nodeType ||
          obj.addEventListener) {
          // Try to find an event if we're not handed one
          if (!args.length) {
            try {
              switch (true) {
                case !!(obj.ownerDocument):
                  ev = obj.ownerDocument.parentWindow.event;
                  break;
                case !!(obj.documentElement):
                  ev = obj.documentElement.ownerDocument.parentWindow.event;
                  break;
                case !!(obj.event):
                  ev = obj.event;
                  break;
                default:
                  ev = window.event;
                  break;
              }
            }
            catch(e) {
              ev = window.event;
            }
          }
          else {
            ev = args[0];
          }
          if (ev) {
            // Set both target and srcElement
            if (typeof ev.target == 'undefined') {
              ev.target = ev.srcElement;
            }
            if (typeof ev.srcElement == 'undefined') {
              ev.srcElement = ev.target;
            }
            // Handle delta differences for mousewheel
            if (ev.type == 'DOMMouseScroll' || ev.type == 'mousewheel') {
              if (ev.wheelDelta) {
                ev.delta = ev.wheelDelta / 120;
              }
              else if (ev.detail) {
                ev.delta = -ev.detail / 3;
              }
            }
            args[0] = ev;
          }
        }
        // Execute all the handler functions registered
        for (var i = 0; i < reg.after.length; i++) {
          var ex = reg.after[i];
          var f = null; // Func to execute
          var c = null; // Execution context
          // Single functions
          if (!ex.context) {
            f = ex.method;
            c = window;
          }
          // Methods of objects
          else {
            f = ex.context[ex.method];
            c = ex.context;
          }
          // Make sure there's something to execute
          if (typeof f != 'function') {
            throw(f + ' is not an executable function.');
          }
          // Pass args and exec in correct scope
          else {
            f.apply(c, args);
          }
          ev = args[0];
          // Stop propagation if needed
          if (ex.stopPropagation) {
            this.stopPropagation(ev);
          }
          // Prevent the default action if needed
          if (ex.preventDefault) {
            this.preventDefault(ev);
          }
        }

      }
      if (this.compatibilityMode) {
        if (!obj._fleegixEventListenReg) { obj._fleegixEventListenReg = {}; }
        obj._fleegixEventListenReg[meth] = listenReg;
      }
      else {
        obj[meth].listenReg = listenReg;
      }
      // Add to global cache -- so we can remove listeners on unload
      listenerCache.push(listenReg);
      // Add XUL event for Firefox mousewheel
      if (meth == 'onmousewheel') {
        if (window.addEventListener) {
          obj.addEventListener('DOMMouseScroll', obj.onmousewheel, false);
        }
      }
    }
    
    // Add the new handler to the listener registry
    listenReg.after.push(r);
    if (this.compatibilityMode) {
      if (!obj._fleegixEventListenReg) { obj._fleegixEventListenReg = {}; }
      obj._fleegixEventListenReg[meth] = listenReg;
    }
    else {
      obj[meth].listenReg = listenReg;
    }
  };
  this.unlisten = function () {
    var obj = arguments[0]; // Obj from which to remove
    var meth = arguments[1]; // Trigger method
    var listenReg;
    if (this.compatibilityMode) {
      if (obj[meth] && obj._fleegixEventListenReg) {
        listenReg = obj._fleegixEventListenReg[meth];
      }
      else {
        listenReg = null;
      }
    }
    else {
      listenReg = obj[meth] ? obj[meth].listenReg : null;
    }
    var remove = null;

    // Bail out if no handlers set
    if (!listenReg) {
      return false;
    }
    // Remove the handler if it's in the list
    for (var i = 0; i < listenReg.after.length; i++) {
      var r = listenReg.after[i];
      // Simple function
      if (typeof arguments[2] == 'function') {
        if (r.method == arguments[2]) {
          listenReg.after.splice(i, 1);
          break; // Only remove one instance per unlisten call
        }
      }
      // Object and method
      else {
        if (r.context == arguments[2] && r.method ==
          arguments[3]) {
          listenReg.after.splice(i, 1);
          break; // Only remove one instance per unlisten call
        }
      }
    }
    if (this.compatibilityMode) {
      obj._fleegixEventListenReg[meth] = listenReg;
    }
    else {
      obj[meth].listenReg = listenReg;
    }
  };
  this.flush = function () {
    // Remove all the registered listeners
    for (var i = 0; i < listenerCache.length; i++) {
      var reg = listenerCache[i];
      removeObj = reg.orig.obj;
      removeMethod = reg.orig.methName;
      removeObj[removeMethod] = null;
    }
  };
  this.subscribe = function(subscr, obj, method) {
    // Make sure there's an obj param
    if (!obj) { return; }
    // Create the channel if it doesn't exist
    if (!channels[subscr]) {
      channels[subscr] = {};
      channels[subscr].audience = [];
    }
    else {
      // Remove any previous listener method for the obj
      this.unsubscribe(subscr, obj);
    }
    // Add the object and its handler to the array
    // for the channel
    channels[subscr].audience.push([obj, method]);
  };
  this.unsubscribe = function(unsubscr, obj) {
    // If not listener obj specified, kill the
    // entire channel
    if (!obj) {
      channels[unsubscr] = null;
    }
    // Otherwise remove the object and its handler
    // from the array for the channel
    else {
      if (channels[unsubscr]) {
        var aud = channels[unsubscr].audience;
        for (var i = 0; i < aud.length; i++) {
          if (aud[i][0] == obj) {
             aud.splice(i, 1);
          }
        }
      }
    }
  };
  this.publish = function(pub, data) {
    // Make sure the channel exists
    if (channels[pub]) {
      var aud = channels[pub].audience;
      // Pass the published data to all the
      // obj/methods listening to the channel
      for (var i = 0; i < aud.length; i++) {
        var listenerObject = aud[i][0];
        var handlerMethod = aud[i][1];
        listenerObject[handlerMethod](data);
      }
    }
  };
  // Convenience method for getting a the source
  // element of an event or its parent based on
  // a particular property
  this.getSrcElementByAttribute = function(e, prop) {
    var node;
    if (e.srcElement) { node = e.srcElement; }
    else if (e.target) { node = e.target; }
    // Avoid trying to use fake obj from IE on disabled
    // form elements
    if (!node || typeof node[prop] == 'undefined') {
      return null;
    }
    // Look up the id of the elem or its parent
    else {
      // Look for something with an id -- not a text node
      while (!node[prop] || node.nodeType == 3) {
        // Bail if we run out of parents
        if (node.parentNode) {
          node = node.parentNode;
        }
        else {
          return null;
        }
      }
    }
    return node;
  };
  this.getSrcElementId = function (e) {
    var node = this.getSrcElementByAttribute(e, 'id') || null;
    return node.id || null;
  };
  this.annihilate = function (e) {
    this.stopPropagation(e);
    this.preventDefault(e);
  };
  this.stopPropagation = function (e) {
    if (e.stopPropagation) {
      e.stopPropagation();
    }
    else {
      e.cancelBubble = true;
    }
    return e;
  };
  this.preventDefault = function (e) {
    if (e.preventDefault) {
      e.preventDefault();
    }
    else {
      e.returnValue = false;
    }
    return e;
  };
  // If there are known problems looking up the listener registry
  // for a particular handler, this will allow the execution to 
  // fail silently instead of throwing errors alerting the user
  // that listening functions are not being triggered correctly.
  // Used in cases where listeners are being addded to windows
  // where document.domain is changed on the fly, which causes
  // lookup of .listenReg to fail
  this.suppressHandlerErrors = function (obj, meth) {
    obj['_' + meth + '_suppressErrors'] = true;
  };
};
// Clean up listeners
fleegix.event.listen(window, 'onunload', fleegix.event, 'flush');

