/**
 * node module - pulp javascript framework
 * @link {http://pulpjs.org}
 * @license MIT-style license http://pulpjs.org/license
 */
/** var $ = pulp.node.getInstance; */
pulp.Modules.node = '$';

(function() {

  var $p = pulp.base;
  
  /**
   * HTMLElement Wrapper
   * @name pulp.node
   * @namespace
   * @example
   *   var $ = pulp.node.getInstance;
   *   var bgColor = $('myId').getStyle('background-color');
   *   var myNode = document.getElementById('myId');
   *   $(myNode).hide;
   *   var myWrapper = $(myNode);
   *   var myWrapper2 = $(myWrapper);
   *   alert(myWrapper === myWrapper2); // true
   *
   * @requires base
   * @requires cls
   */
  var self = pulp.node = pulp.cls.create(/** @lends pulp.node# */{
    /**
     * Return an HTMLElement Wrapper given an HTMLElement
     * @param {HTMLElement} node
     * @constructor
     */
    initialize: function(node) {
      this.raw = node;
      this._setCache();
    },
    /**
     * Save a reference to this object on the node itself
     * see below for circular reference decupling for IE6
     * @return undefined
     */    
    _setCache: function() {
      this.raw.__pulpNode = this;
    },       
    /**
     * Set the css style of a node. Attribute names can be hyphenated (e.g. "z-index")
     *   or camelized (e.g. "zIndex"). Optionally an Object with attribute-value pairs may be passed.
     * @param {String} attr  The attribute name
     * @param {String} value  The value to set
     * @return {pulp.node}
     * @chainable
     * 
     * @notify dom:attrmodified  When style is changed contains keys style, prevValue, newValue
     * @example
     *   var $ = pulp.node.getInstance;
     *   $('fruit').setStyle('backgroundColor', 'red');
     *   $('fruit').setStyle({
     *     color: 'white',
     *     'font-weight': 'bold'
     *   });
     */
    setStyle: function(attr, value) {
      if (arguments.length == 2) {
        var prevValue = this.getStyle(attr);
        if (attr == 'float') {
          attr = 'cssFloat';
        }
        else if (attr == 'for') {
          attr = 'htmlFor';
        }
        else if (attr.indexOf('-') > -1) {
          attr = self._camelize(attr);
        }
        else if (attr == 'overflow' && !self.support.setStyleOverflow && this.raw.style.overflow) {
          this.setAttribute('style', this.getAttribute('style').replace((/overflow\s*\:\s*[^;]+\s*;/g), ''));
        }
        this.notify('dom:attrmodified', function() {        
          if (attr == 'opacity') {
            return setOpacity.call(this, value);
          }     
          this.raw.style[attr] = value;
        }, {method: 'node.setStyle', attrName: 'style', attrChange: 1/*event.MODIFICATION*/, style: attr, prevValue: prevValue, newValue: value, relatedNode: this.raw.getAttributeNode('style')});
      }
      else {
        if (typeof attr == 'string') {
          attr = self.styleStringToObject(attr);
        }
        for (var name in attr) {
          this.setStyle(name, attr[name]);
        }
      }
      return this;
    },
    /**
     * Set the css style of a node. Attribute names can be hyphenated (e.g. "z-index")
     *   or camelized (e.g. "zIndex"). Optionally an Object with attribute-value pairs may be passed.
     * @param {String} attr  The attribute name
     * @return {String}
     */
    getStyle: function(attr) {
      // TODO: option to get computed style??
      if (attr == 'opacity') {
        return this.getOpacity();
      }
      else if (attr.indexOf('-') > -1) {
        attr = self._camelize(attr);
      }
      else if (attr == 'overflow' && !self.support.setStyleOverflow && this.raw.style.overflow) {
        return this.raw.cssText.match((/overflow\:\s*([^;]+)\s*;/))[0];
      }
      return this.raw.style[attr];
    },   
    /**
     * Set the opacity style
     * @param {String|Number}  A number between 0 and 1. 1 for fully visible, 0.5 for 50% opacity, 0 for invisible
     * @return {pulp.node}
     * @chainable
     * @triggers dom:attrmodified
     */
    setOpacity: function(opacity) {
      // run through setStyle function to trigger dom:attrmodified event
      return this.setStyle('opacity', opacity);
    },
    /**
     * Get the opacity style
     * @return {Number}  A number between 0 and 1. 1 for fully visible, 0.5 for 50% opacity, 0 for invisible
     * @chainable
     * @triggers dom:attrmodified
     */    
    getOpacity: function() {
      return parseFloat(this.raw.style.opacity || '0');
    },
    /**
     * Get the raw value of any given property (e.g. innerHTML, style, title, value, parentNode, etc.)
     * @param {String} property
     * @return {Mixed}
     */
    get: function(property) {
      return this.raw[property];
    },
    /**
     * Set the raw value of any given property (e.g. innerHTML, style, title, value, parentNode, etc.)
     * @param {String} property
     * @param {Mixed} value
     * @return {pulp.node}
     * @chainable
     */    
    set: function(property, value) {
      this.raw[property] = value;
      return this;
    },    
    /**
     * Set the innerHTML
     * @param {String|HTMLElement|HTMLElement[]} html  An html string, a dom node, or a collection of dom nodes
     * @return {pulp.node}
     * @chainable
     * @triggers update
     */
    update: (function() { 
    	var shells = {
    		'sele' : ['<select multiple="multiple">', '</select>', 1],
    		'tr'   : ['<table><tbody>', '</tbody></table>', 2],
    		'td'   : ['<table><tbody><tr>', '</tr></tbody></table>', 3],
    		'thea' : ['<table>', '</table>', 1],
    		'col'  : ['<table><tbody></tbody><colgroup>', '</colgroup></table>', 2]
    	};
    	shells.tbod = shells.tfoo = shells.colg = shells.capt = shells.thea;
      
      return function(html) {
        html = html || '';
        
  			this.notify('update', function() {      
  				if (html[0] && html[0].nodeType == 1) {
            this.raw.innerHTML = '';
  					$p.each(html[0], function(el) {
  						this.raw.appendChild(el);
  					}, this);				
  				}
  				else if (html.nodeType == 1) {
            this.raw.innerHTML = '';
  					this.appendChild(html);
  					
  				} else {
  					// based on Prototype 1.6.1rc2 and jQuery 1.3.2
  					var tag = this.raw.tagName.toLowerCase().substring(0,4);
  					
  					if (tag === 'scri') {
  						if (self.support.scriptTextNode) {
  							this.raw.appendChild(document.createTextNode(html));	
  						}
  						else {
  							this.raw.text = content;
  						}
  					}
  					else if (
              (!self.support.selectInnerHTML && tag == 'sele') || // TODO: handle optgroup
              (!self.support.tableInnerHTML && (tag in shells))
            ) {
  						var wrap = shells[tag];
  						var div = document.createElement('div');
  						div.innerHTML = wrap[0] . html . wrap[1];
  						var level = wrap[2];
  						
  						while (level--) {
  							div = div.lastChild;
  						}
  						this.raw.replaceNode(div);
              // TODO: copy attributes and events from wrapped items?
  					}
  					else {
  						this.raw.innerHTML = html;
  					}
          }
  			}, {method: 'update', prevValue: this.raw.innerHTML, newValue: html});
        return this;
      };
    })(),    
    /**
     * Get or set the innerHTML (sets when argument is present, gets when argument is absent)
     * 
     * @param {String|HTMLElement} html  An html string or an html node
     * @return {pulp.node}
     * @chainable
     * @triggers dom:modified
     */
    html: function(html) {
      if (arguments.length == 0) {
        return this.raw.innerHTML;
      }
      return this.update(html);
    },
    /**
     * Return true if the node has the given css class name (aliased as hasClassName)
     * 
     * @param {String} className
     * @return {Boolean}
     */
    hasClass: function(className) {
      if (className && this.raw.className) {
        // indexOf is much faster than creating a dynamic RegExp        
        return this.raw.className === className || 
          (' ' + this.raw.className + ' ').indexOf(' ' + className + ' ') > -1
        ;  
      }
      return false;
    },
    /**
     * Add a css class name to the node (aliased as addClassName)
     * 
     * @param {String} className 
     * @return {pulp.node}
     * @chainable
     * @triggers dom:attrmodified
     */
    addClass: function(className) {
      if (className && this.hasClass(className) == false) {
        this.set('className', this.raw.className + (this.raw.className.length ? ' ': '') + className);
      }
      return this;
    },
    /**
     * Remove a css class name from the node (aliased as removeClassName)
     * 
     * @param {String} className
     * @return {this}
     * @chainable
     * @triggers dom:attrmodified
     */
    removeClass: function(className) {
      if (className !== '' && typeof className === 'string') {
        this.set('className', 
          // TODO: benchmark to see if this approach is actually faster than a new regex
          (' ' + this.raw.className + ' ')
          .replace(' ' + className + ' ', ' ')
          .replace((/^[ ]?(.*?)[ ]?$/), '$1')
        );
      }
      return this;
    },
    /**
     * Remove a css class if present or add if not pressent
     * @param {String} className  The name of the css class
     * @param {Object} onOff  If undefined, do a regular toggle. If true add class, if false remove it
     */
    toggleClass: function(className, onOff) {
      var add = (arguments.length == 2 ? arguments[1] : !this.hasClass(className));
      return this[add ? 'addClass' : 'removeClass'](className);
    },
    /**
     * set display style to none to visually hide the node
     * 
     * @return {this}
     * @chainable
     * @triggers dom:attrmodified
     */
    hide: function() {
      // use setStyle to trigger the set:style event 
      return this.setStyle('display', 'none');
    },
    /**
     * Set display style to an empty string
     * 
     * @triggers dom:attrmodified
     * @return {this}
     * @chainable
     */
    show: function() {
      return this.setStyle('display', '');
    },
    /**
     * Remove the node from the document tree
     * 
     * @triggers dom:noderemoved
     * @return {this}
     * @chainable 
     */
    remove: function() {
      if (this.raw.parentNode) {
        this.notify('dom:noderemoved', function() {
          this.raw.parentNode.removeChild(this.raw);        
        }, {method: 'node.remove', relatedNode: this.raw});
      }
      return this;
    },
    /**
     * Replace a DOM node
     * @param {HTMLElement|pulp.node} node  Node to add instead of this one
     * @return {this}  The replaced node
     * @chainable
     * @triggers dom:noderemoved
     */
    replace: function(node) {
      this.insertAfter(node);
      return this.remove();
    },
    /**
     * Add a new child to this node as the first child
     * 
     * @triggers dom:nodeinserted
     * @return {pulp.node}
     * @chainable  
     */
    prependChild: function(child) {
      child = $(child);
      if (this.raw.firstChild) {
        child.notify('dom:nodeinserted', function() {
          if (this.raw.firstChild) {
          	$(this.raw.firstChild).insertBefore(child.raw);
          }
          else {
            this.raw.appendChild(child.raw);
          }        
        }, {method: 'node.prependChild', relatedNode: this.raw});
        return this;
        
      } else {
        return this.appendChild(child);
      }
    },
    prependChildren: function(nodeList) {
      $p.each(nodeList, function(node) {
        this.prependChild(node);
      }, this);
      return this;
    },		
    /**
     * Add a new child to this node as the last child
     * 
     * @return {pulp.node}
     * @chainable  
     * @triggers dom:nodeinserted
     */
    appendChild: function(child) {
      child = $(child);
      this.notify('dom:modified', function() {
        this.raw.appendChild(child.raw);        
      }, {method: 'node.appendChild', relatedNode: this.raw});
      return this;
    },
    appendChildren: function(nodeList) {
      $p.each(nodeList, function(node) {
        this.appendChild(node);
      }, this);
      return this;
    },
    /**
     * Replace child with another element
     * @param {HTMLElement|pulp.node} newEl
     * @param {HTMLElement|pulp.node} oldEl
     * @return {pulp.node} The newly inserted node
     */
    replaceChild: function(newEl, oldEl) {
      oldEl = $(oldEl);
      newEl = $(newEl);
      oldEl.replace(newEl);
      return oldEl;
    },
    /**
     * Add a node immediately before this one
     * 
     * @triggers dom:nodeinserted
     * @return {pulp.node}
     * @chainable  
     */
    insertBefore: function(child) {
      var parent = this.raw.parentNode;
      child = $(child);
      child.notify('dom:nodeinserted', function() {
        parent.insertBefore(this.raw, child.raw);
      },{method: 'node.insertBefore', relatedNode: parent});
      return this;
    },
    /**
     * Add a node immediately after this one
     * 
     * @triggers dom:nodeinserted
     * @return {pulp.node}
     * @chainable
     */
    insertAfter: function(child) {
      child = $(child);
      var parent = this.raw.parentNode;
      child.notify('dom:nodeinserted', function() {
        var nextSibling = this.raw.nextSibling;
        if (nextSibling) {
          parent.insertBefore(child.raw, nextSibling);
        } else {
          parent.appendChild(child.raw);
        }
      }, {method: 'node.appendChild', child: child, relatedNode: parent});
      return this;
    },    
    /**
     * Wrap a node in HTML or another node
     * 
     * @triggers dom:nodeinserted
     * @param {String|HTMLElement|pulp.node} wrapper  The HTML or node to wrap around this node
     * @param {Boolean} [deepest=true]  If true, append to the deepest child of the wrapper
     * @return {pulp.node}  The new wrapper
     */    
    wrap: function(wrapper, deepest) {
      wrapper = $(wrapper);
      if (deepest !== false) {
        wrapper = wrapper.getDeepestChild();
      }
      this.replace(wrapper);
      wrapper.appendChild(this.raw);
      return wrapper;
    },
    /**
     * Move all child nodes into a new wrapper
     * 
     * @triggers dom:nodeinserted
     * @param {String|HTMLElement|pulp.node} wrapper  The HTML or node to wrap around this node's children
     * @param {Boolean} [deepest=true]  If true, append to the deepest child of the wrapper
     * @return {pulp.node}  The new wrapper
     */    
    wrapInner: function(wrapper, deepest) {
      wrapper = $(wrapper);
      if (deepest !== false) {
        wrapper = wrapper.getDeepestChild();
      }
      $p.each(this.raw.childNodes, function(node) {
        wrapper.appendChild(node);
      });
      this.appendChild(wrapper.raw);
      return this;
    },
    /**
     * Return the first node at the deepest level
     * @return {pulp.node}
     * @example
     *   var p = $('<p><a href="go.php"><span></span></a></p>');
     *   var span = p.getDeepestChild();
     */
    getDeepestChild: function() {
      var deepest = node.firstChild, deepestIdx = 0;
      $p.each(node.getElementsByTagName('*'), function(el, idx) {
      	if (el.nodeType == 1) {
      	  var i = 0, oldEl = el;
      	  do { i++; } while ((el = el.parentNode))
      	  if (i > deepestIdx) {
        		deepestIdx = i;
        		deepest = oldEl;
      	  } 
      	}
      });
      return $(deepest);
    },
    /**
     * Get an array of child nodes as pulp.node objects
     * 
     * @return {pulp.node[]}
     */
    getChildren: function() {
      var results = [], ch = this.raw.childNodes;
      for (var i = 0, len = ch.length; i < len; i++) {
        if (ch[i].nodeType == 1) {
          results.push($(ch[i]));
        }
      }
      return results;      
    },   
    /**
     * Get the sibling that comes immediately before this one
     * 
     * @return {Boolean|pulp.node}
     */
    getPrevSibling: function() {
      var sib = this.raw;
      while ((sib = sib.previousSibling)) {
        if (sib.nodeType == 1) {
          break;
        }
      }
      return sib === this.raw ? false : $(sib);
    },
    /**
     * Get the sibling that comes immediately after this one
     * 
     * @return {Boolean|pulp.node}
     */    
    getNextSibling: function() {
      var sib = this.raw;
      while ((sib = sib.nextSibling)) {
        if (sib.nodeType == 1) {
          break;
        }
      }
      return sib === this.raw ? false : $(sib);    
    },
    /**
     * Get an array of all siblings as pulp.node objects (don't include this node)
     * 
     * @return {pulp.node[]}
     */    
    getSiblings: function() {
      var children = this.raw.parentNode.childNodes, sibs = [], sib, i = 0;
      while ((sib = children[i++])) {
        if (sib.nodeType == 1 && sib !== this.raw) {
          sibs.push($(sib));
        }
      }
      return sibs;
    },
    /**
     * Get an array of siblings as pulp.node objects that come after this node
     * 
     * @return {pulp.node[]}
     */    
    getNextSiblings: function() {
      var sibs = [], sib = this.raw;
      while ((sib = sib.getNextSibling())) {
        if (sib.nodeType == 1) {
          sibs.push($(sib));
        }
      }
      return sibs;
    },
    /**
     * Get an array of siblings as pulp.node objects that come before this node
     * 
     * @return {pulp.node[]}
     */    
    getPreviousSiblings: function() {
      var sibs = [], sib = this.raw;
      while ((sib = sib.getPreviousSibling())) {
        if (sib.nodeType == 1) {
          sibs.push(sib);
        }
      }
      return sibs;
    },
    /**
     * Get an array of ancestors of this node as pulp.node objects
     * 
     * @return {pulp.node[]}
     */     
    getAncestors: function() {
      var anc = [], node = this.raw;
      while ((node = node.parentNode)) {
        anc.push($(node));
      }
      return anc;
    },
    /**
     * Get the first ancestor matching the given tag name, index, or css selector
     * 
     * @param {String|Number} [selector=""]  A tag name, index or a css selector (pulp.cssQuery is required to use css selectors)
     *   - When a tag name is given, it returns the first ancestor with that tag name
     *   - When a number is given, it returns the ancestor with that index (1 for parent, 2 for grandparent, etc.)
     *   - When a selector is given, the first ancestor matching the selector is given (assuming pulp.cssQuery is present)
     * @return {pulp.node}
     */     
    up: function(selector) {
      var parent = this.raw;
      if (typeof selector == 'string') {
        if ((/^[a-z]+$/i).test(selector)) {
          // regular tag name
          var tag = selector.toUpperCase();
          while ((parent = parent.parentNode) && parent.tagName) {
            if (parent.tagName.toUpperCase() == tag) {
              return $(parent);
            }
          }
        } else if ('cssQuery' in pulp) {
          // css selector
          while ((parent = parent.parentNode)) {
            if (pulp.cssQuery.match(parent, selector)) {
              return $(parent);
            }
          } 
          return null;
        } else {
          throw 'pulp.cssQuery is required to pass css selectors to pulp.node#up()';
        }
      }
      // index where 1 = parent, 2 = grandparent, etc.
      var idx = selector > 0 ? parseFloat(selector) : 1;
      while (idx--) {
        parent = parent.parentNode;
      }
      return $(parent);
    },
    /**
     * Get the first descendant matching the given tag name, index, or css selector
     * 
     * @param {String|Number} [idx=""]  A tag name, index or a css selector (pulp.cssQuery is required to use css selectors)
     *   - When a tag name is given, it returns the first descendant with that tag name
     *   - When a number is given, it returns the descendant with that index (1 for first child, 2 for first child's first child, etc.)
     *   - When a selector is given, the first descendant matching the selector is given (assuming pulp.cssQuery is present)
     * @return {pulp.node}
     */     
    down: function(idx) {
      if (typeof idx == 'string') {
        if ((/^[a-z]+$/i).test(arguments[0])) {
          // regular tag name
          idx = (arguments.length == 2 ? idx : 0);
          var nodes = this.raw.getElementsByTagName(arguments[0]);
          return nodes.length && nodes[idx] ? $(nodes[idx]) : undefined;
          
        } else if ('cssQuery' in pulp) {
          // css selector
          return $(pulp.cssQuery(idx, this.raw)[0]);
          
        } else {
          throw 'pulp.cssQuery is required to pass css selectors to pulp.node#down()';
        }
      }
      var idx = selector ? parseFloat(selector) : 1;
      return this.raw.childNodes && this.raw.childNodes[idx] ? $(this.raw.childNodes[idx]) : undefined;
    },
    /**
     * Return true if the node is a descendent of the given node
     * 
     * @param {pulp.node|HTMLElement} parent  The parent to test against
     * @return {Boolean}
     */
    isDescendantOf: function(parent) {
      var node = this.raw;
      parent = $(parent).raw;
      while ((node = node.parentNode)) {
        if (node === parent) {
          return true;
        }
      }
      return false;
    },
    /**
     * Get the id attribute of a node.  If id is not set, set it.
     * Account for situations where id property is not a string such as a form with an input named "id"
     * 
     * @return {String}
     */    
    identify: function() { $p.identifyNode(this.raw); },
    /**
     * Get the height and width of the node in px
     * 
     * @return {Object}  Object with properties "width" and "height"
     */
    getDimensions: function() {
      // adapted from prototype 1.6.1_rc2
      var display = this.getStyle('display');
      if (display != 'none' && display != null) { // Safari bug
        return {width: this.raw.offsetWidth, height: this.raw.offsetHeight};
      }
      var els = this.raw.style;
      var originalVisibility = els.visibility;
      var originalPosition = els.position;
      var originalDisplay = els.display;
      els.visibility = 'hidden';
      if (originalPosition != 'fixed') { // Switching fixed to absolute causes issues in Safari
        els.position = 'absolute';
      }
      els.display = 'block';
      var originalWidth = this.raw.clientWidth;
      var originalHeight = this.raw.clientHeight;
      els.display = originalDisplay;
      els.position = originalPosition;
      els.visibility = originalVisibility;
      return {width: originalWidth, height: originalHeight};
    },
    /**
     * Get the height of the node in px
     * 
     * @return {Number}
     */
    getHeight: function() {
      return this.getDimensions().height;
    },
    /**
     * Get the width of the node in px
     * 
     * @return {Number}
     */
    getWidth: function() {
      return this.getDimensions().width;
    },
    /**
     * Return a copy of the pulp.node being sure to remove the clone's id attribute.
     * If a deep copy, remove the id attribute from children as well.
     * Remember that the copy is not attached to the DOM tree.
     * TODO: delete the event handlers that IE erroneously adds to the copied node
     * 
     * @param {Boolean} withChildren  If true, perform a deep copy
     * @return {pulp.node}
     */
    cloneNode: function(withChildren) {
      var copy = this.raw.cloneNode(withChildren);
      if (withChildren) {
        var descendents = copy.getElementsByTagName('*');
        for (var i = 0, len = descendents.length; i < len; i++) {
          if (descendents[i].nodeType == 1) {
            descendents[i].removeAttribute('id');
          }
        }
      }
      copy.removeAttribute('id');
      // TODO clear events from copy if support.cloneClearsEvents is false
      return $(copy);
    },
    /**
     * Set an attribute on a node
     * 
     * @param {String} attr  The attribute to set
     * @param {String|undefined|false|null}  The value of the attribute.  
     *   If not given, the attribute will be set to the atribute name.
     *   e.g. $(el).setAttribute('checked')  will set checked="checked"
     *   If false or null, the attribute is removed
     * @notify dom:attrmodified
     * @return {undefined}
     */
    setAttribute: function(attr, value) {
      var el = this.raw;
      if (attr in attrTransform) {
        attr = attrTransform[attr];
      }      
      var prevValue = this.getAttribute(attr), changeType = 1; // event.MODIFICATION
      if (value === null || (value === false && attr != 'checked')) {
        changeType = 3; // event.REMOVAL
      }
      else if (prevValue === undefined) {
        changeType = 2; // event.ADDITION
      }
      if (typeof attr === 'string') {
        this.notify('dom:attrmodified', function() {
          if (attr == 'checked') {
            // we want setAttribute('checked') to set to true
            el.checked = (value === undefined ? true : !!value);
            
          } else if (attr == 'style' && !self.support.setAttrStyle) {
            el.style.cssText = (value ? value : '');
            
          } else if (attr == 'htmlFor' || attr == 'className') {
            el[attr] = value;
            if (value === false || value === null) {
              delete el[attr];
            }
            
          } else if (value === false || value === null) {
            // e.g. we want setAttribute(attr, false) to remove attribute
            el.removeAttribute(attr);
            
          } else if (value === true || value === undefined) {
            // e.g. we want setAttribute('disabled', true) and setAttribute('disabled') to set disabled=disabled
            el.setAttribute(attr, attr);
            
          } else {
            el.setAttribute(attr, value);
          }
        }, {method: 'node.setAttribute', attrName: attr, attrChange: changeType, prevValue: prevValue, newValue: value, relatedNode: attrNode = el.getAttributeNode(attr)});
      } else {
        for (var name in attr) {
          this.setAttribute(name, attr[name]);
        }
      }
      return this;
    },
    /**
     * Return the value of the specified attribute (special treatment for IE)
     * 
     * @param {String} attr  The attribute to get
     * @return {String|Number}
     */
    getAttribute: function(attr) {
      var value, el = this.raw;
      if (attr in attrTransform) {
        attr = attrTransform[attr];
      }
      if (attr == 'htmlFor' || attr == 'className') {
        value = this.raw[attr];
      }
      else if (attr == 'opacity') {
        value = this.getOpacity();
      }
// TODO: figure out the proper usage of these bad boys
//      else if ((' title name rel value ').indexOf(' ' + attr + ' ') > -1) {
//        value = el[attr];
//      } 
      else if ((' disabled checked readOnly multiple ').indexOf(' ' + attr + ' ') > -1) {
        value = (this.hasAttribute(attr) ? attr.toLowerCase() : null);
      }
      else if (attr == 'style') {
        value = this.raw.style.cssText;
        if (!self.support.styleLowerCase) {
          value = value.toLowerCase();
        }
      }
      else if (attr == 'action' && !self.support.getAttrAction) {
        attr = el.getAttributeNode(attr);
        value = attr ? attr.value : "";        
      }
      else if (!self.support.getAttrEvents && (' onload onunload ondblclick onclick onmousedown onmouseup onmouseover onmousemove onmouseout onmouseenter onmouseleave onfocus onblur onkeypress onkeyenter onkeydown onkeyup onsubmit onreset onselect onchange ').indexOf(' ' + attr + ' ') > -1) {
        // get only the internals of the function
        var fn = el.getAttribute(attr);
        value = fn ? String(fn).replace((/^[^{]+\{\s*([\s\S]*?)\s*\}\s*$/), '$1') : null;
      }      
      else {
        if (
          (typeof el.tagName !== 'string' || el.tagName.toUpperCase() == 'FORM') && 
          (' cloneNode childNode parentNode nextSibling previousSibling ').indexOf(' ' + attr + ' ') == -1
        ) {
          el = el.cloneNode(false);
        }
        else if (!self.support.getAttrIframeType && el.tagName.toUpperCase() == 'IFRAME' && attr == 'type') {
          value = el.getAttribute('type', 1); 
        }
        else if (!self.support.rawHref && (' href src type ').indexOf(' ' + attr + ' ') > -1) {          
          value = el.getAttribute(attr, 2);
//alert(self.support.rawHref + ' ' + attr + ' ' + value);          
        }
        return el.getAttribute(attr);
      }
      return value;
    },
    hasAttribute: function(attr) {
      if (attr in attrTransform) {
        attr = attrTransform[attr];
      }
      if (attr == 'htmlFor') {
        return !!this.raw.htmlFor;
      }      
      var a = this.raw.getAttributeNode(attr);
      return !!(a && a.specified);
    },
    /**
     * Remove the given attribute
     * 
     * @param {String} attribute  The name of the attribute to remove
     * @return {pulp.node}
     * @chainable
     */
    removeAttribute: function(a) {
      // run through setAttribute to handle special cases and trigger dom:attrmodified
      return this.setAttribute(a, undefined);
    },
    getElementById: function(id) {
      return $(this.raw.getElementById(id));
    },
    // based on jQuery 1.3.2
    /**
     * Return the first ancestor with offset of static (or body if none ar found)
     *
     * @return {pulp.node}
     */
    offsetParent: function() {
      var offsetParent = this.raw.offsetParent || document.body;
      while (offsetParent && (!(/^body|html$/i).test(offsetParent.tagName) && offsetParent.style.position == 'static')) {
        offsetParent = offsetParent.offsetParent;
      }
      return $(offsetParent);
    }
  });
  
  self.extend(/** @scope pulp.node */{
    /**
     * Return a new pulp.node by node id or node handle
     * 
     * @param {String|HTMLElement|pulp.node}  The id or node handle to get
     * @return {pulp.node|undefined}  Returns undefined if the node cannot be found
     */
    getInstance: function(node) {
      var el;
      if (node && node.__pulpNode) {
        // return wrapper that was already created
        return node.__pulpNode;
      }
      else if (node && node.nodeType == 1) {
        // create a new wrapper from HTMLElement
        return new self(node);          
      }
      else if (typeof node == 'string') {
        if (node.charAt(0) == '<') {
          // parse html string
          el = self.htmlToNodeList(node)[0];
        }
        else {
          // lookup node by id          
          el = document.getElementById(node);
        }
        // ensure it is not already wrapped
        return el && el.nodeType == 1 ? $(el) : undefined;
      }
      else if (node instanceof self) {
        // return itself if already a pulp.node instance
        return node;
      }
      return undefined;
    },
    /**
     * Convert an html string to an HTMLNodelist object
     *
     * @param {String} html
     * @return {HTMLNodeList}
     */
    htmlToNodeList: function(html, doc) {
      var el = (doc || pulp.document).createElement('div');
      el.innerHTML = html;
      var list = el.children;
      el = null;
      return list;
    },
    /**
     * perform capability testing to be used in other functions
     */
    support: (function() {
      // modeled after jQuery 1.3.2 and prototype 1.6.1_rc3
      // create some nodes to test stuff
      var div = document.createElement('div');
      div.setAttribute('style', 'color: red');
      div.style.width = '50px';
      div.innerHTML = '<input onclick="a" style="float: left;"><a href="/a" style="opacity: 0.42;">a</a>';
      var input = div.firstChild;
      var a = div.childNodes[1];
      // does the browser clone events on cloned nodes? (it shouldn't but IE does)
      var cloneClearsEvents = true;
    	if (div.attachEvent && div.fireEvent) {
        var fn = function() {
    			// Cloning a node shouldn't copy over any
    			// bound event handlers (IE does this)
    			cloneClearsEvents = false;
    			div.detachEvent("onclick", fn);
    		}
    		div.attachEvent("onclick", fn);
    		div.cloneNode(true).fireEvent("onclick");
    	}
      // can script node be set with innerHTML or do we need to append a text node
      var script = document.createElement("script");
      var scriptTextNode = false;
      try {
        script.appendChild(document.createTextNode(""));
        scriptTextNode = true;
      } catch (e) {}
      // can innerHTML be set on table elements (IE elements are read only)
      var tableInnerHTML = false;
      try {
        var table = document.createElement("table");
        table.innerHTML = "<tbody><tr><td>test</td></tr></tbody>";
        tableInnerHTML = true;
      } catch (e) {}
      // can innerHTML be set on select elements or is appending nodes required (IE)
      var select = document.createElement("select");
      select.innerHTML = "<option value=\"test\">test</option>";
      var selectInnerHTML = (select.options && !!select.options[0]);
      // do we get an exception trying to get the type attribute on an iframe sometimes
      var iframe = document.createElement('iframe'),
        iframeTypeAttr = false,
        doc = document.documentElement;
      doc.appendChild(iframe);
      try {
        iframe.getAttribute('type', 2);
        iframeTypeAttr = true;
      } catch(e) {}
      doc.removeChild(iframe);
      // check if the overflow property can be duplicated in cssText (konqueror)
      var overflowed = document.createElement('div');
      overflowed.innerHTML = '<p style="overflow: visible;">x</p>';
      if (overflowed.firstChild && overflowed.firstChild.style) {
        overflowed.firstChild.style.overflow = 'hidden';
      }
      // save the tests results
      var s = {
        opacity: parseFloat(a.style.opacity) == 0.42,
        setAttrStyle: div.style.color.length > 0,
        getAttrEvents: input.getAttribute('onclick') == 'a',
        styleLowerCase: div.style.cssText.indexOf('WIDTH') == -1,
        noAutoTbodies: div.getElementsByTagName('tbody').length == 0,
        rawHref: a.getAttribute("href") == "/a",
        styleCssFloat: !!input.style.cssFloat,
        cloneClearsEvents: cloneClearsEvents,
        scriptTextNode: scriptTextNode,
        tableInnerHTML: tableInnerHTML,
        selectInnerHTML: selectInnerHTML,
        getAttrIframeType: iframeTypeAttr,
        setStyleOverflow: overflowed.firstChild.style.overflow !== 'hidden'
      };
      // clean out memory of un-needed values
    	div = input = a = fn = script = table = select = iframe = doc = overflowed = null;
      return s;      
    })(),
    /**
     * Convert a hyphen-delimited string to a camelCase string
     * 
     * @param {String}  Thes string to convert
     * @return {String}
     */
    _camelize: function(str) {
      var parts = str.split('-'), result = parts[0];
      parts = Array.prototype.slice.call(parts, 1);
      for (var i = 0, len = parts.length; i < len; i++) {
        result += parts[i].substring(0,1).toUpperCase() + parts[i].substring(1);
      }
      return result;
    },
    /**
     * Convert a CSS style string to an object
     * 
     * @param {String}  CSS stylesheet style string
     * @return {Object}
     */
    styleStringToObject: function(styles) {
      var styleList = styles.split(';'), match, obj = {};
      for (var i = 0, len = styleList.length; i < len; i++) {
        if ((match = styleList[i].match(/^\s*([a-z0-9-]+)\s*:\s*(\S.*)\s*$/))) {
          obj[match[1]] = match[2];
        }
      }
      return obj;
    },
    styleObjectToString: function(obj) {
      var str = '';
      for (var prop in obj) {
        str += prop + ':' + obj[prop] + ';';
      }
      return str;
    }
  });
  
  var $ = self.getInstance;  
  
  /**
   * Return true if the node has the given attribute (special treatment for IE)
   * 
   * @param {String} attr  The attribute to test against
   * @return {Boolean}
   * @name pulp.node#hasAttribute
   */
  // TODO: add docs for getElementsByTagName getElementsByClassName
  $p.each('getElementsByTagName getElementsByClassName', function(m) {
    /** @ignore */
    self.prototype[m] = function(arg) {
      var list = this.raw[m](arg);
      var i = list.length;
      var inst = Array(i);      
      while (i--) {
        inst[i] = $(list[i]);
      }
      return inst;
    };
  });
  if (!$p.isNative(document.getElementsByClassName)) {
    /**
     * Emulate getElementsByClassName for browsers without it
     * @ignore
     */
    self.prototype.getElementsByClassName = function(className) {
      var desc = this.raw.getElementsByTagName('*');
      var results = [];
      for (var i = 0, len = desc.length; i < len; i++) {
        if (desc[i].className && (' ' + desc[i].className + ' ').indexOf(' ' + className + ' ') > -1) {
          results.push($(desc[i]));
        }
      }
      return results;
    };
  }

  if (self.support.opacity) {
    // standards compliant
    /** @ignore */
    var setOpacity = function(value) {
      var opacity = (value == 1 || value === '') ? 1.0 : (value < 0.00001 ? 0 : value);
      this.raw.style.opacity = opacity;
      return opacity;
    };
  }
  else {    
    // alpha filter; e.g. IE 6,7
    /** @ignore */
    self.prototype.getOpacity = function() {      
      if ((value = (this.raw.style.filter || '').match(/alpha\(opacity=(.*)\)/))) {
        if (value[1]) {
          return parseFloat(value[1]) / 100;
        }
      }
      return 1.0;      
    };
    /** @ignore */
    var setOpacity = function(value) {
      var currentStyle = this.raw.currentStyle;
      if ((currentStyle && !currentStyle.hasLayout) || (!currentStyle && this.raw.style.zoom == 'normal')) {
        this.raw.style.zoom = 1;
      }
      var s = this.raw.style, f = s.filter;
      if (value === '') {
        value = 1;
      } else if (value < 0.00001) {
        value = 0;
      }
      s.filter = f.replace(/alpha\([^\)]*\)/gi,'') + 'alpha(opacity=' + (value * 100) + ')';
    };  
  }
  self.aliasMethods(/** @lends pulp.node# */{
    /** @function  alias of {@link pulp.node#getAttribute} */
    readAttribute: 'getAttribute',
    
    /** @function  alias of {@link pulp.node#setAttribute} */
    writeAttribute: 'setAttribute',
    
    /** @function  alias of {@link pulp.node#addClass} */
    addClassName: 'addClass',
    
    /** @function  alias of {@link pulp.node#removeClass} */
    removeClassName: 'removeClass',
    
    /** @function  alias of {@link pulp.node#hasClass} */
    hasClassName: 'hasClass',
    
    /** @function  alias of {@link pulp.node#wrapInner} */
    wrapChildren: 'wrapInner',
    
    /** @function  alias of {@link pulp.node#replace} */
    replaceNode: 'replace'
  });
  
  // add dom event methods
  if ('event' in pulp) {
    var obs = pulp.event.observable;
    $p.each('observe stopObserving fire', function(m) {
      self.prototype[m] = function(a,b) {
        obs[m](this.raw, a, b);
        return this;
      };     
    });
    self.prototype.listHandlers = function(type) {
      return obs.listHandlers(this.raw, type);
    };
  }
  /*
  // add generics
  $p.each(pulp.node.prototype, function(fn, name) {	
    self[name] = function() {
      var el = $(arguments[0]);
      if (el) {
        return el[name].apply(el, Array.prototype.slice.call(arguments, 1));
      }
      return undefined;
    };
  });
  */
  
  // attr that should be mixed case
  var attrTransform = {'for': 'htmlFor', 'class': 'className'};
  $p.each('cellPadding cellSpacing colSpan rowSpan vAlign dateTime accessKey tabIndex encType maxLength readOnly longDesc frameBorder', function(camel) {
    attrTransform[camel.toLowerCase()] = camel;
  });
  
  // define dom event options
  var ev = pulp.cls.event;
  ev.defineEvent('dom:attrmodified', {
    //bubbles: yes,
    cancelable: false,
    MODIFICATION: 1,
    ADDITION: 2,
    REMOVAL: 3
  });
  ev.defineEvent('dom:nodeinserted', {
    //bubbles: yes,
    cancelable: false
  });
  ev.defineEvent('dom:noderemoved', {
    //bubbles: true,
    cancelable: false
  });  
  
  if (pulp.isIE6) {
    // the only known browser that cannot garbage collect circular references
    var els = [];
    self.prototype._setCache = /** @ignore */ function(node) {
      this.raw.__pulpNode = this;
      els.push(this.raw);
    };
    // decouple circular references to avoid IE6 memory leak
    window.attachEvent('onunload', function() {
      var i = els.length;
      while (i--) {
        els[i].__pulpNode = null;
      }
    });
  } 
    
})();