var LIB = {};

// Some array extras for the app developer.
// These are not used within the library
// to keep library interdependencies low.
// These extras don't use the optional 
// thisObject argument of native JavaScript 1.6
// Array.prototype.filter but that can easily
// be added here at the cost of some file size.
// Not using the native Array.prototype.filter
// because want to be able to send array-like
// objects to these functions.
LIB.filter = function(a, f) {
  var rs = [];
  for (var i=0, ilen=a.length; i<ilen; i++) {
      if (typeof a[i] != 'undefined' && f(a[i])) {
        rs[rs.length] = a[i];
      }
  }
  return rs;
}; 

LIB.forEach = function(a, f) {
  for (var i=0, ilen=a.length; i<ilen; i++) {
    if (typeof a[i] != 'undefined') {
      f(a[i]);
    }
  }
};

// ---------------------------------------------------

(function() {
  
  // Use for testing if a DOM property is present.
  // Use LIB.isHostCollection if the property is
  // a collection. Use LIB.isHostMethod if
  // you will be calling the property.
  //
  // Examples:
  //   // "this" is global/window object
  //   LIB.isHostObject(this, 'document');
  //   // "el" is some DOM element
  //   LIB.isHostObject(el, 'style');
  var isHostObject = function(o, p) {
    return !!(typeof(o[p]) == 'object' && o[p]);
  };
  LIB.isHostObject = isHostObject;

  // Use for testing if DOM collects are present
  // Some browsers make collections callable and
  // have a typeof 'function'.
  //
  // Examples:
  //   // since tested document as property of "this"
  //   // need to refer to it as such
  //   var doc = this.document;
  //   LIB.isHostCollection(doc, 'all');
  //   // "el" is some DOM element
  //   LIB.isHostCollection(el, 'childNodes');
  var isHostCollection = function(o, m) {
    var t = typeof(o[m]);
    return (!!(t=='object' && o[m])) ||
           t=='function';
  };
  LIB.isHostCollection = isHostCollection;

  // Use for testing a DOM property you intend on calling.
  // In Internet Explorer, some ActiveX callable properties
  // have a typeof "unknown". IE returns "object" for typeof
  // operations on callable host objects, but other browsers
  // (e.g. Safari, Opera) return "function."
  //
  // Examples:
  //   // since tested document as property of "this"
  //   // need to refer to it as such
  //   var doc = this.document;
  //   LIB.isHostMethod(doc, 'createElement');
  //   // "el" is some DOM element
  //   LIB.isHostMethod(el, 'appendChild');
  var isHostMethod = function(o, m) {
    var t = typeof(o[m]);  
    return t=='function' ||
           !!(t=='object' && o[m]) ||
           t=='unknown';
  };
  LIB.isHostMethod = isHostMethod;

  if (!(isHostObject(this, 'document'))) {
    return;
  }
  var doc = this.document;
  
  if (isHostObject(doc, 'documentElement')) {
    var getAnElement = function(d) {
      return (d || doc).documentElement;
    };
    LIB.getAnElement = getAnElement;
    LIB.getDocumentElement = getAnElement;
  }
  
  if (getAnElement &&
      typeof getAnElement().className == 'string') {
    // The RegExp support need here 
    // has been available since NN4 & IE4
    
    var classRegExpCache = {};
    
    // do not include the global flag on the regexp
    // as the regexp is being used repeatedly with 
    // RegExp.prototype.test(). See
    // http://groups.google.com/group/comp.lang.javascript/msg/a9523f0b06c4bdcc
    LIB.makeClassRegExp = function(className) {
      return classRegExpCache[className] = 
               new RegExp('(^|\\s+)' + className + '(\\s+|$)');
    };
    
    LIB.hasClass = function(el, className) {
      return (classRegExpCache[className] ||
              LIB.makeClassRegExp(className)).test(el.className);
    };

    LIB.addClass = function(el, className) {
      if (LIB.hasClass(el, className)) {
        return;
      }
      el.className = el.className + ' ' + className;
    };

    LIB.removeClass = function(el, className) {
      el.className = el.className.replace(
        (classRegExpCache[className] ||
         LIB.makeClassRegExp(className)), ' ');
      // in case of multiple
      if (LIB.hasClass(el, className)) {
        LIB.removeClass(el, className);
      }
    };
  }
  
  // Letting IE 5.x down the degradation path is not a tragedy
  // and could easily be justified. Support for IE 5 is
  // included here as an illustration of a more complex feature
  // detection problem.
  //
  // IE 5.0 & 5.5 don't support getElementsByTagName('*') but can
  // fallback to using document.all or element.all.
  // Safari 1.0 does not support
  // getElementsByTagName('*') and doesn't have document.all
  if (LIB.getDocumentElement &&
      (function() {
        var el = LIB.getDocumentElement();
               // Test both interfaces in the DOM spec
        return isHostMethod(doc, 'getElementsByTagName') &&
               ((doc.getElementsByTagName('*').length > 0) ||
                (isHostCollection(doc, 'all') &&
                 doc.all.length > 0)) &&
               isHostMethod(el, 'getElementsByTagName') &&
               (isHostMethod(el, 'getElementsByTagName') ||
                (isHostCollection(el, 'all') &&
                 el.all.length > 0));
      })()) {
    // One possible implementation for developers 
    // in a situation where it is not a problem that
    // IE5 thinks doctype and comments are elements.
    // Can workaround that if necessary.
    LIB.getEBTN = function(tag, root) {
      root = root || doc;
      var els = root.getElementsByTagName(tag);
      if (tag == '*' &&
          !els.length) {
        els = root.all;
      }
      return els;
    };
  }
  
  if (isHostMethod(doc, 'getElementById')) {
    // One possible implementation for developers
    // not troubled by the name and id attribute
    // conflict in IE
    LIB.getEBI = function(id, d) {
      return (d || doc).getElementById(id);
    };
  }
  
  if (LIB.getEBTN &&
      LIB.getEBI &&
      getAnElement &&
      (function() {
        var el = getAnElement();
        return typeof el.nodeType == 'number' &&
               typeof el.tagName == 'string' &&
               typeof el.className == 'string' &&
               typeof el.id == 'string' 
       })()) {

    // One possible selector compiler implementation
    // that can handle selectors with a tag name,
    // a class name and an id.
    //
    // use memoization for efficiency
    var selectorCache = {};
    var compile = function(s) {
      if (selectorCache[s]) {
        return selectorCache[s];
      }

      var m,    // regexp matches
          tn,   // tagName in s
          id,   // id in s
          cn,   // className in s
          f;    // the function body

      m = s.match(/^([^#\.]+)/);
      tn = m ? m[1] : null;

      m = s.match(/#([^\.]+)/);
      id = m ? m[1] : null;

      m = s.match(/\.([^#]+)/);
      cn = m ? m[1] : null;

      f = 'var i,els,el,m,ns=[];';
            
      if (id) {
        f += 'if (!d||(d.nodeType==9||(!d.nodeType&&!d.tagName))){'+
               'els=((el=LIB.getEBI("'+id+'",d))?[el]:[]);' +
               ((!cn&&!tn)?'return els;':'') +
             '}else{' +
               'els=LIB.getEBTN("'+(tn||'*')+'",d);' +
             '}';
      }
      else {
        f += 'els=LIB.getEBTN("'+(tn||'*')+'",d);';
      }

      if (cn) {
         // note that cn cannot contain unescaped "
        f += 'var re=LIB.makeClassRegExp("' + cn + '");';
      }

      if (id || cn) {
        f += 'i=els.length;' +
             'while(i--){' +
               'el=els[i];' +
               'if(';
            if (id) {
              f += 'el.id=="'+id+'"';
            }
            if ((cn||tn) && id) {
              f += '&&'; 
            }
            if (tn) {
              f += 'el.tagName.toLowerCase()=="' + tn + '"';
            }
            if (cn && tn) {
              f += '&&'; 
            }
            if (cn) {
              f += '((m=el.className)&&re.test(el.className))';
            }
          f += '){' +
                 'ns[ns.length]=el;' +
            '}' +
          '}';        
          
          f += 'return ns.reverse();';
      }
      else {
        f += 'return els;';
      }

      // http://elfz.laacz.lv/beautify/
      //console.log('function f(d) {' + f + '}');

      f = new Function('d', f);
      selectorCache[s] = f;
      return f;
    }

    LIB.querySelector = function(selector, rootEl) {
      return (compile(selector))(rootEl);
    };

  }
  
})();

// Events ----------------------------------------------------------

(function() {
  
  if (Function.prototype.apply && // IE 5 doesn't have apply()
      LIB.getAnElement &&
      Array.prototype.slice 
      // uncomment next line if using
      // the try-catch below 
      //&& this.setTimeout
      ) {
      
      var global = this,
          anElement = LIB.getAnElement(),
          listeners = [];

      var wrapHandler = function(element, handler, options) {
        options = options || {};
        var thisObj = options.thisObj || element;
        return function(e) {
          return handler.apply(thisObj, [e || global.event]);
        };
      };

      var createListener = function(element, eventType,
                                             handler, options) {
        return {
          element: element,
          eventType: eventType,
          handler: handler,
          wrappedHandler: wrapHandler(element, handler, options)
        };
      };

      var callListeners = function(listeners, e) {
        // Make a local copy incase one listener
        // adds more listeners to the array. 
        // Worst case is a listener adding itself
        // and creating an infinte loop.
        listeners = listeners.slice(0);
        
        for (var i=listeners.length; i--; ) {
          var listener = listeners[i];
          if (listener && listener.wrappedHandler) {
            // Uncomment try-catch if you do not need to support
            // old browsers or some modern cell phones.
            // The try-catch will ensure all handlers run.
            // Also uncomment test for global.setTimeout above
            //try {
            listener.wrappedHandler(e);
            //}
            //catch (err) {
            //  (function(err) {
            //    global.setTimeout(function() {throw err}, 10);
            //  })(err);
            //}
          }
        }
      };


    // BEGIN DOM2 event model
    if (LIB.isHostMethod(global, 'addEventListener') &&
        LIB.isHostMethod(anElement, 'addEventListener')) {
      // Feature test that the Safari workaround will work
      // This is *not* a browser sniff! Browsers other
      // than Safari will use the workaround if they
      // have the necessary features. This is not a problem.
      // Tested Safari 1.0, 1.2, 1.3, 2.0 and they all 
      // need the workaround and all return typeof 'object'
      // for unset global.onclick property.
      var useSafariWorkaround =
        !!(typeof global.onclick == 'object');
                                
      if (useSafariWorkaround) {
        var safariListeners = {
          click: [],
          dblclick: []
        }
      }

      LIB.addListener = function(element, eventType,
                                               handler, options) {
                                                 
        var listener = createListener(element, eventType,
                                                handler, options);

        if (useSafariWorkaround &&
            (eventType == 'click' || eventType =='dblclick') &&
            element == global) {
          var a = safariListeners[eventType];
          a[a.length] = listener;
        }
        else {
          element.addEventListener(
            listener.eventType,
            listener.wrappedHandler,
            false);
          listeners[listeners.length] = listener;
        }
      };

      var prevented = false;
      if (useSafariWorkaround) {
        // Clobbers existing DOM0 element. It is
        // trivial to build a workaround for that
        // but it shouldn't be an issue anyway.
        global.onclick = 
        global.ondblclick = 
          function(e) {
            // Safari 2.0 needs the +'' to access property
            // without a silent error
            callListeners(safariListeners[e.type + ''], e);
            var result = !prevented;
            prevented = false;
            return result;
          };
      }
      
      // event must bubble up to window
      // so no cancelBubble. It is a bad practice
      // anyway because canceling bubble foils
      // unrelated delegate listeners
      LIB.preventDefault = function(e) {
        prevented = true;
        if (LIB.isHostMethod(e, 'preventDefault')) {
         e.preventDefault();
        }
      };
    
    } // END DOM2 event model
    // BEGIN IE event model
    else if (LIB.isHostMethod(global, 'attachEvent') &&
             LIB.isHostMethod(anElement, 'attachEvent') &&
             LIB.isHostMethod(global, 'detachEvent') &&
             LIB.isHostMethod(anElement, 'detachEvent')) {
      
      LIB.addListener = function(element, eventType, 
                                               handler, options) {
        var listener = createListener(element, 'on'+eventType,
                                                handler, options);
      
        if (eventType == 'unload' && global == element) {
          unloadListeners[unloadListeners.length] = listener;
        }
        else {
          element.attachEvent(
            listener.eventType,
            listener.wrappedHandler);
          listeners[listeners.length] = listener;
        }
      };

      // IE leaks memory and want to cleanup
      // before when the document unloads
      var unloadListeners = [];

      global.attachEvent('onunload', 
        function(e) {
          // uncomment the next line to test
          // circular reference memory leak in IE
          //return;
          e = e || global.event;
          callListeners(unloadListeners, e);

          for (var i=listeners.length; i--; ) {
            listener = listeners[i];
            if (listener) {
              // break circular reference to
              // fix IE memory leak
              listener.element.detachEvent(
                listener.eventType,
                listener.wrappedHandler)
              // help garbage collection
              listeners[i] = null;
            }
          }
          global.detachEvent('onunload', arguments.callee);
          // help garbage collection
          global = null;
        });
      
      LIB.preventDefault = function(e) {
        e.returnValue = false;
      };
      
    } // END IE event model 

    // clean up for memory leaks
    anElement = null;
  }
  
})();

