Bolo = {
  Version: '0.1',
  path: function () {
    var scripts = document.getElementsByTagName('script');
    for (var i = 0, l = scripts.length; i < l; i++) {
      var src = scripts[i].getAttribute('src');
      if (src) {
        var index = src.indexOf('js/bolo.js');
        if (index > -1) {
          return src.substring(0,index);
        }
      }
    }
  }(),
  loaded: [],
  load: function (pkg) {
    if (!Bolo.loaded[pkg]) {
      var url = this.path + 'js/' + pkg.replace(/\./g,'/') + '.js';
      var script = document.createElement('script');
      script.setAttribute('src',url);
      document.getElementsByTagName('head')[0].appendChild(script);
    }
    Bolo.loaded[pkg] = true;
  },
  styleSheets: [],
  loadStyleSheet: function (pkg) {
    if (!Bolo.styleSheets[pkg]) {
      var url = this.path + 'stylesheets/' + pkg.replace(/\./g,'/') + '.css';
      var link = document.createElement('link');
      link.setAttribute('rel','stylesheet');
      link.setAttribute('type','text/css');
      link.setAttribute('href',url);
      document.getElementsByTagName('head')[0].appendChild(link);
    }
    Bolo.styleSheets[pkg] = true;
  }
}

Bolo.Tree = Class.create();

Bolo.Tree.prototype = {
  setOptions: function (options) {
    this.options = Object.extend({
      toggle: Bolo.Tree.SimpleToggle,
      select: Bolo.Tree.NoSelect,
      effect: Effect.Appear,
      duration: 0.4,
      xmlbuilder: Bolo.Tree.XMLBuilder, 
      attributePlugins: {}
    }, options || {})
  },
  initialize: function (container, options) {
    this.setOptions(options);
    this.container = $(container);
    Element.addClassName(this.container, 'Bolo');
    Element.addClassName(this.container, 'Tree');
    this.childNodes = [];
    if (this.options.xmldoc) {
      this.options.xmlbuilder(this, this.options.xmldoc, this.options);
      var node = this;
      var walker = new Bolo.Walker(node);
      while (node = walker.next()) { 
        if (!node.expanded) node.collapse();
        else node.expand();
      }    
    }
    else if (this.options.xmlsrc) { 
      Bolo.Tree.loadXML(this, this.options);
      this.onload = function () {this.childNodes.each(function (c) { c.collapse(true) });}.bindAsEventListener(this);
    }
    else if (this.container.childNodes.length != 0) {
      Element.cleanWhitespace(this.container);
      Bolo.Tree.ULBuilder(this, this.container, this.options);
      var node = this;
      var walker = new Bolo.Walker(this);
      while (node = walker.next()) { 
        if (!Element.hasClassName(node.container, "Expanded")) node.collapse();
        else node.expand();
      }
    }
    Event.observe(window, 'unload', function () { this.dispose(); }.bindAsEventListener(this), false);
  },
  dispose: function () {
    this.childNodes.each(function (c) {c.dispose()});
    this.container = null;
  },
  addChildNode: function (childNode) {
    childNode.parentNode = this;
    this.childNodes.push(childNode);
  },
  addChildNodes: function (childNodes) {
    childNodes.each(function(c) {this.addChildNode(c)}.bind(this));
  },
  getSelected: function () {
    var walker = new Bolo.Walker(this);
    var selected = [];
    var node;
    while (node = walker.next()) {
      if (node.selected) selected.push(node);
    }
    return selected;
  }
}

/*------------------------------------------------------------------------*/

Bolo.Tree.Node = Class.create();

Bolo.Tree.Node.prototype = {
  setOptions: function (options) {
    this.options = Object.extend({
      preventClick: false,
      onclick: function () {}
    }, options || {})
  },
  initialize: function (container, options) {
    this.container = $(container);
    this.label = this.container.firstChild;
    Element.addClassName(this.container, 'TreeNode');
    this.setOptions(options);
    this.childNodes = [];
    this.clickObserver = this.click.bindAsEventListener(this);
    Event.observe(this.container.firstChild,'click', this.clickObserver);
  },
  click: function (e) {
    var root = this.getTree();
    root.options.toggle(this);
    root.options.select(this);
    this.options.onclick.call(this, e);
    //if (this.options.preventClick) Event.stop(e);
  },
  dispose: function () {
    this.childNodes.each(function (c) {c.dispose()});
    this.container = null;
    this.label = null;
  },
  addChildNode: function (childNode) {
    childNode.parentNode = this;
    this.childNodes.push(childNode);
  },
  addChildNodes: function (childNodes) {
    childNodes.each(function(c) {this.addChildNode(c)}.bind(this));
  },
  expand: function (recursive) {
    this.childNodes.each(function (c) {
      c.show();
      if (recursive) c.expand(true);
    });
    if (this.childNodes.length > 0) {
      this.expanded = true;
      Element.addClassName(this.container, 'Expanded');
      Element.removeClassName(this.container, 'Collapsed');    
    }
  },
  collapse: function (recursive) {
    this.childNodes.each(function (c) {
      c.hide();
      if (recursive) c.collapse(true);
    })
    if (this.childNodes.length > 0) {
      this.expanded = false;
      Element.removeClassName(this.container, 'Expanded');
      Element.addClassName(this.container, 'Collapsed');
    }
  },
  select: function () {
    Element.addClassName(this.container, 'Selected');
    this.selected = true;
  },
  deselect: function () {
    Element.removeClassName(this.container, 'Selected');
    this.selected = false;
  },
  show: function () {
    var root = this.getTree();
    var options = root.options;
    if (options.effect) {
      options.effect(this.parentNode.container.lastChild, {duration: options.duration});
    }
    else this.parentNode.container.lastChild.style.display = '';
  },
  hide: function () {
    this.parentNode.container.lastChild.style.display = 'none';
  },
  getTree: function () {
    var node = this;
    while (node.parentNode) node = node.parentNode;
    return node;
  },
  getParents: function () {
    var nodes = [];
    var node = this;
    while (node = node.parentNode) nodes.push(node);
    nodes.pop(); // remove the Tree.
    return nodes;
  },
  getPath: function () {
    var path = [];
    var currentNode = this;
    while (currentNode.parentNode) {
      var childNode = currentNode;
      currentNode = currentNode.parentNode;
      for (var i = 0, l = (currentNode.childNodes.length - 1); i < l; i++) {
        if (childNode == currentNode.treeNodes[i]) {
          path.push(i);
          break;
        }
      }
    }
    return path.reverse();
  },
  getLevel: function () {
    var level = 0;
    var currentNode = this;
    while (currentNode = currentNode.parentNode)  level++;
    return level;
  }
}

/*------------------------------------------------------------------------*/

Bolo.Tree.Separator = Class.create();

Bolo.Tree.Separator.prototype = {
  setOptions: function (options) {
    this.options = Object.extend({
    }, options || {});
  },
  initialize: function (container, options) {
    this.container = $(container);
    this.label = this.container.firstChild;
    Element.addClassName(this.container, 'Separator');
    this.setOptions(options);
    this.childNodes = [];
  },
  dispose: function () {
    this.container = null;
    this.label = null;
  },
  collapse: function () {},
  expand: function () {}
}

/*------------------------------------------------------------------------*/

Bolo.Tree.Banner = Class.create();

Bolo.Tree.Banner.prototype = {
  setOptions: function (options) {
    this.options = Object.extend({
    }, options || {});
  },
  initialize: function (container, options) {
    this.container = $(container);
    this.banner = this.container.firstChild;
    Element.addClassName(this.container, 'Banner');
    this.setOptions(options);
    this.childNodes = [];
    this.clickObserver = this.click.bindAsEventListener(this);
    Event.observe(this.container.firstChild,'click', this.clickObserver);
  },
  click: function (e) {
    var root = this.getTree();
    root.options.select(this);  
    Event.stop(e);
  },
  dispose: function () {
    this.container = null;
    this.label = null;
  },
  collapse: function () {},
  expand: function () {},
  select: function () {}
}

Bolo.Tree.Banner.prototype.getTree = Bolo.Tree.Node.prototype.getTree

/*------------------------------------------------------------------------*/

Bolo.Tree.SimpleToggle = function (node) {
  if (node.childNodes.length > 0) {
    if (node.expanded) node.collapse();
    else node.expand();
  }
}

Bolo.Tree.SameBranchToggle = function (node) {
  var pn = node.parentNode;
  if (pn) {
    for (var i = 0; i < pn.childNodes.length; i++) {
      if (node != pn.childNodes[i]) pn.childNodes[i].collapse();
    }
  }
  if (node.expanded) node.collapse();
  else node.expand();
}

/*------------------------------------------------------------------------*/

Bolo.Tree.NoSelect = function (node) {}

Bolo.Tree.SingleSelect = function (node) {
  var tree = node.getTree();
  var selected = tree.getSelected();
  if (node.selected) node.deselect();
  else node.select();
  selected.each(function (s) {
    if (node != s) s.deselect();
  });
}

Bolo.Tree.ParentSelect = function (node) {
  var tree = node.getTree();
  var parents = node.getParents();
  var selected = tree.getSelected();
  if (node.selected) node.deselect();
  else node.select();
  selected.each(function (s) {
    if (node != s) s.deselect();
  });
  parents.each(function (p) {
    p.select();
  });
}

/*------------------------------------------------------------------------*/

Bolo.Tree.ULBuilder = function (treeNode, ulNode, options) {
  Element.cleanWhitespace(ulNode);
  var liNodes = $A(ulNode.childNodes);
  for (var i = 0, l = liNodes.length; i < l; i++) {
    var li = liNodes[i];
    Element.cleanWhitespace(li);
    var tn = new Bolo.Tree.Node(li,(options)?options.nodeOptions:{});
    if (options.plugins) options.plugins.each(function (p) { p(tn) });
    treeNode.addChildNode(tn);
    var tag = li.lastChild.tagName;
    if (tag && tag.toLowerCase() == 'ul') { Bolo.Tree.ULBuilder(tn, li.lastChild, options)};
  }
};

Bolo.Tree.loadXML = function (treeNode, options) {
  var xhr = new XMLHttpRequest();
  xhr.onreadystatechange = function () {
    if ((xhr.readystate == 4 || xhr.readyState == 4) && xhr.responseXML.documentElement) {
      options.xmlbuilder(treeNode, xhr.responseXML.documentElement, options);
      if (typeof treeNode.onload == 'function') treeNode.onload();
    }
  }
  xhr.open("GET", options.xmlsrc, true);
  xhr.send(null);
}

Bolo.Tree.XMLBuilder = function (treeNode, xmlNode, options) {
  var c = $A(xmlNode.childNodes).filter(function (c) { return c.nodeType == 1});
  var treenodes = [];
  for (var i = 0, l = c.length; i < l; i++) {
    treenodes.push(Bolo.Tree.XMLBuilder.XMLToNode(c[i], options));
    if (c[i].childNodes.length > 0) {
      options.xmlbuilder(treenodes[i], c[i], options);
    }
  }
  treenodes.each(function (t){
    var ul = treeNode.container.getElementsByTagName('ul')[0];
    if (!ul) { 
      ul = Builder.node('ul');
      treeNode.container.appendChild(ul);
    }
    ul.appendChild(t.container);
  }.bind(this));
  treeNode.addChildNodes(treenodes);
}

Bolo.Tree.XMLBuilder.XMLToNode = function (xmlNode, options) {
  var tag = xmlNode.tagName;
  var attributes = {};
  $A(xmlNode.attributes).collect(function(a) {
    attributes[a.name] = xmlNode.getAttribute(a.name);
  });
  var node;
  switch (tag) {
    case 'treenode':
      var li = Builder.node('li', {}, 
        [Builder.node('a',{href: '#'}, xmlNode.getAttribute('label').unescapeHTMLEntities())]
      );
      node = new Bolo.Tree.Node(li, (options)?options.nodeOptions:{});
      break;
    case 'treenodebanner':
      var li = Builder.node('li', {}, 
        [Builder.node('img',{src: xmlNode.getAttribute('src')})]
      );
      var node = new Bolo.Tree.Banner(li, (options)?options.nodeOptions:{});
      break;
    case 'treenodeseparator':
      var li = Builder.node('li', {}, 
        [Builder.node('span',{}, xmlNode.getAttribute('label').unescapeHTMLEntities())]
      );
      var node = new Bolo.Tree.Separator(li, (options)?options.nodeOptions:{});
      break;
  }
  if (options.plugins) options.plugins.each(function (p) { p(attributes, node) });
  return node;
}

/*------------------------------------------------------------------------*/

Bolo.Tree.LimitedLevelXMLBuilder = function (treeNode, xmlNode, options, level) {
  var level = level || 0;
  var c = $A(xmlNode.childNodes).filter(function (c) { return c.nodeType == 1 && (c.getAttribute('visible') != 'false') });
  var treenodes = [];
  for (var i = 0, l = c.length; i < l; i++) {
    treenodes.push(Bolo.Tree.XMLBuilder.XMLToNode(c[i], options));
    if (c[i].childNodes.length > 0 && level < options.level) {
      Bolo.Tree.LimitedLevelXMLBuilder(treenodes[i], c[i], options, level+1);
    }
  }
  treenodes.each(function (t){
    var ul = treeNode.container.getElementsByTagName('ul')[0];
    if (!ul) { 
      ul = Builder.node('ul');
      treeNode.container.appendChild(ul);
    }
    ul.appendChild(t.container);
  }.bind(this));
  treeNode.addChildNodes(treenodes);
}
Bolo.Walker = Class.create();

Bolo.Walker.prototype = {
  setOptions: function (options) {
    this.options = Object.extend({
      childNodes: 'childNodes',
      parentNode: 'parentNode'
    }, options || {});    
  },
  initialize: function (start, options) {
    this.setOptions(options);
    this.startNode = start;
    this.currentNode = start;
    this.childNodesProperty = this.options.childNodes;
    this.parentNodeProperty = this.options.parentNode;
    this.walkingNodes = start[this.childNodesProperty];
    this.index = 0;
  },
  _nextNode: function () {
    this.currentNode = this.walkingNodes[++this.index];
  },
  _moveDown: function () {
    this.walkingNodes = this.currentNode[this.childNodesProperty];
    this.index = 0;
    this.currentNode = this.walkingNodes[this.index];
  },
  _moveUp: function () {
    this.index = $A(this.currentNode[this.parentNodeProperty][this.childNodesProperty]).indexOf(this.currentNode);
    this.walkingNodes = this.currentNode[this.parentNodeProperty][this.childNodesProperty];
    this.currentNode = this.currentNode[this.parentNodeProperty];
  },
  _atLastNode: function () {
    return this.index == this.walkingNodes.length-1;
  },
  _atStartNode: function () {
    return this.currentNode == this.startNode;
  },  
  _hasChildNodes: function () {
    return this.currentNode[this.childNodesProperty] && this.currentNode[this.childNodesProperty].length > 0;
  },
  next: function () {
		if (this._hasChildNodes()) { // move down the tree	
      this._moveDown();
      return this.currentNode;
    }
    else if (!this._atLastNode()) { // move to next node
      this._nextNode();
      return this.currentNode;
    }
    else if (!this._atStartNode()) { // move up the tree
      this._moveUp();
      while (!this._atStartNode() && this._atLastNode()) { // move up some more
        this._moveUp();
      }
      this._nextNode();
      return this.currentNode;
    }
    return null;
  }
}