var SortableTree = Class.create({
  initialize: function(element, options) {
    this.element = $(element);
    this.root = new SortableTree.Node(this, null, element, options);
		this.isSortable = false;
  },
  
  toggleSortable: function() {
    this.isSortable ? this.setUnsortable() : this.setSortable();
  },
  
  setSortable: function() {
    Element.addClassName(this.root.element, 'sortable');
		this.root.setSortable();
		this.isSortable = true;
  },

	setUnsortable: function() {
    Element.removeClassName(this.root.element, 'sortable');
		this.root.setUnsortable();
		this.isSortable = false;
	},
  
  find: function(element) {
    return this.root.find($(element));
  },

	unmark_all: function() {
    this.root.unmark();
	}
});

SortableTree.Node = Class.create({
  initialize: function(tree, parent, element, options) {
    this.tree = tree;
    this.parent = parent;
    this.element = $(element);

    this.options = Object.extend({
      tagName: 'LI',
      containerTagName: 'UL',
			droppable: {},
			draggable: {}
    }, options || {});

    this.droppable_options = Object.extend({
      onHover:      function(drag, drop, overlap){ this.onHover(drag, drop, overlap) }.bind(this),
      onDrop:       function(drag, drop, event){ this.onDrop(drag, drop, event) }.bind(this), 
      overlap:      'vertical',
      hoverclass:   'drop_hover'
    }, options.droppable);

    this.draggable_options = Object.extend({
      ghosting: true,
      revert: true,
      constraint:  'vertical',
      reverteffect: function(element, top_offset, left_offset) {
		    element.setStyle({left: '0px', top:  '0px'});
				// would be so cool to be able to use this. but it leaves a backgroundColor
				// style property on the element which overwrites the class' value
				// (i.e. the drop marker) and apperently can't be removed anymore (?)
				// new Effect.Highlight(element, { startcolor: '#FFFF99' })
      },
    }, options.draggable);

    this.initChildren()
  },
  
  id: function() {
    if (!this._id) {
      var match = this.element.id.match(/^[\w]+_([\d]*)$/);
      this._id = encodeURIComponent(match ? match[1] : null);
    }
    return this._id;
  },
  
  previousSibling: function() {
    var pos = this.parent.children.indexOf(this);
    return pos > 0 ? this.parent.children[pos - 1] : null;
  },
  
  initChildren: function() {
    this.children = [];  
    var container = this.findContainer(this.element)
    if(container){
      $A(container.childNodes).each(function(child) {
        if(this.acceptTagName(child)) {
          this.children.push(new SortableTree.Node(this.tree, this, child, this.options));
        }
      }.bind(this));
    }
  },

  acceptTagName: function(element) {
    return element.tagName && element.tagName.toUpperCase() == this.options.tagName
  },

  setSortable: function() {
    Droppables.add(this.element, this.droppable_options);
    this.draggable = new Draggable(this.element, this.draggable_options);
    this.children.each(function(child) { child.setSortable() });
  },

  setUnsortable: function() {
		Droppables.remove(this.element)
		this.draggable.destroy();
    this.children.each(function(child) { child.setUnsortable() });
  },
  
  find: function(element) {
    if(element == this.element) return this;
    for(var i = 0; i < this.children.length; i++) {
      var node = this.children[i].find(element);
      if(node) return node; 
    }
  },

  findContainer: function(element) {
    if(element.tagName != this.options.containerTagName) {
      element = $A(element.childNodes).detect(function(node) { 
        return node.tagName == this.options.containerTagName
      }.bind(this));
    }
    return element;
  },

  findOrCreateContainer: function(element) {
    var container = this.findContainer(element);
    if(!container) {
      container = document.createElement(this.options.containerTagName);
      element.appendChild(container);
    }
    return container;
  },

  onHover: function(drag, drop, overlap) {		
		if(this.canContainChildren(drop)) {
		  this.dropPosition = overlap < 0.33 ? 'bottom' : overlap > 0.77 ? 'top' : 'insert';
		} else {
			this.dropPosition = overlap < 0.5 ? 'bottom' : 'top';
		}
    this.mark(drop);
		// $('log').update('hovering: ' + drop.tagName + ': ' + drop.id + "<br />" + 
		//                 'classes: ' + drop.className + "<br />" + 
		// 							  'dropPosition: ' + this.dropPosition)
  },	

	canContainChildren: function(element) {
		if(this.options.droppable.container) {
			return element.match(this.options.droppable.container);
		}
		return true;
	},

  onDrop: function(drag, drop, event) {
    drag = this.tree.find(drag);
    drop = this.tree.find(drop);

		// i.e. don't do anything if it's a toplevel node and has been dropped on "itself"
		// another way around this could be to change scriptaculous to affect() a node
		// when it has been dropped on itself
		if(drop.parent || this.dropPosition == 'insert') { 
	    switch(this.dropPosition) {
	      case 'top':    drop.parent.insertBefore(drag, drop); break;
	      case 'bottom': drop.parent.insertBefore(drag, drop.nextSibling()); break;
	      case 'insert': this.insertBefore(drag, this.firstChild()); break;
	    }
		}

    if(this.options.onDrop) this.options.onDrop(drag, drop, event);
  },

  mark: function(element, position) {
		this.tree.unmark_all();
    Element.addClassName(element, 'drop_' + this.dropPosition);
  },

  unmark: function() {
    ['drop_top', 'drop_bottom', 'drop_insert'].each(function(classname){
      Element.removeClassName(this.element, classname);
    }.bind(this));
		this.children.each(function(child) { child.unmark(); }) 
  },
  
  to_params: function(name) {
		name = name || this.tree.element.id;
    var leftNode = this.previousSibling();
    return name + '[' + this.id() + '][parent_id]=' + this.parent.id() + '&' + 
           name + '[' + this.id() + '][left_id]=' + (leftNode ? leftNode.id() : ''); // null
  },
  
  firstChild: function() {
    return this.children.length > 0 ? this.children[0] : null;
  },
  
  previousSibling: function() {
    var pos = this.parent.children.indexOf(this);
    return pos > 0 ? this.parent.children[pos - 1] : null;
  },
  
  nextSibling: function() {
    var pos = this.parent.children.indexOf(this);
    return pos + 1 < this.parent.children.length ? this.parent.children[pos + 1] : null;    
  },
  
  removeChild: function(node) {
    this.children.splice(this.children.indexOf(node), 1);
    node.element.parentNode.removeChild(node.element)
  },
  
  insertBefore: function(node, sibling) {
		if(node == sibling) return;
		
		node.parent.removeChild(node);
    node.parent = this;
    var pos = sibling ? this.children.indexOf(sibling) : this.children.length;
    this.children.splice(pos, 0, node);

    this.findOrCreateContainer(this.element).insertBefore(node.element, sibling ? sibling.element : null)
  }
});