dojo.provide("bl.tree");

dojo.require("dojo.lang");
dojo.require("dojo.event");
dojo.require("dojo.event.browser");
dojo.require("dojo.io.*");
dojo.require("dojo.profiler");
dojo.require("dojo.string.Builder");
dojo.require("dojo.html.extras");

dojo.require("bl.alg");
dojo.require("bl.console");
dojo.require("bl.dnd");
dojo.require("bl.dom-extensions");
dojo.require("bl.event");
dojo.require("bl.html-extensions");
dojo.require("bl.lang");
dojo.require("bl.translate");

function $(id) { return dojo.byId(id); }
function $A(n,a) { return dojo.html.getAttribute(n, a); }
function $E(n) { return document.createElement(n); }
function $T(txt) { return document.createTextNode(txt); }

dojo.declare("bl.tree.DragSource", bl.dnd.Source, 
  function(node, type, options, view) {
    this.view = view;
  },
  {
    onDragStart: function(){
      //console.log("drag starting for %o", this);
      var s = this.view.model.selected;
      if(s.length == 0) return null;
      var type = -1
      dojo.lang.forEach(s, function(n) {
        if(n.isSub() && type < 0) type = 0;
        else if(n.isFolder() && type < 1) type = 1;  
      });
      if(type == 0) type = "sub";
      else if(type == 1) type = "folder";
      return new bl.tree.TreeDragObject(this.dragNode, type, null, this.view);
    }
  });

dojo.declare("bl.tree.TreeDragObject", bl.dnd.DragObject,
  function(node, type, options, view) { 
    this.view = view;
  },
  {
  createDragNode: function() {
    var s = this.view.model.selected;
    if(s.length <= 0) return null;
    var cntFolder = 0;
    var cntSub = 0;
    for(var i=0; i < s.length; i++) {
      if(s[i].isRoot()) throw new Error("cannot drag using root");
      if(s[i].isFolder()) cntFolder++;
      if(s[i].isSub()) cntSub++;
    }
    var cls = "dg_";
    if(cntFolder > 0) cls += "folder";
    if(cntFolder > 1) cls += "_many";
    if(cntSub > 0 && cntFolder == 0) cls += "sub";
    if(cntSub > 1 && cntFolder == 0) cls += "_many";
    var dragIco = $E("div");
    dragIco.id = "dragIco";
    dragIco.style.zIndex = 999;
    dojo.html.setClass(dragIco, cls);
    return dragIco;
  }
  });

dojo.declare("bl.tree.TreeDropTarget", bl.dnd.Target,
  function(node, types, priority, view) {
    this.view = view;
  },
  {
  onDrop: function(e) {
    this.onDragOut(e);
    var i = this._getNodeUnderMouse(e);
    var moving = e.dragObject.domNode;
    var ref = null
    var loc = "";      

    if(i < 0) {
      if (this.childBoxes.length) {
	      if (dojo.html.gravity(this.childBoxes[0].node, e) & dojo.html.gravity.NORTH) {
          ref = this.childBoxes[0].node;
          loc = "before";
        } else {
          ref = this.childBoxes[this.childBoxes.length - 1].node;
          loc = "after";
        }
      } else {
        ref = this.domNode;
          loc = "append";
      }
    } else {
      ref = this.childBoxes[i].node;
      loc = (dojo.html.gravity(ref, e) & dojo.html.gravity.NORTH) ? "before" : "after";
    }
    return this.view.onDrop(moving, ref, loc, e);

  },
  repr: function() {
    return "TreeDropTarget: { node: " + this.domNode + ", types: [" + this.acceptedTypes.join(",") + "] }"; 
  }
  });

dojo.declare("bl.tree.FolderDropTarget", bl.tree.TreeDropTarget, null,
{
  onDragOver: function(e) {
    return this.accepts(e.dragObject);
  },
  onDragMove: function(e) {
    dojo.html.addClass(this._getNode(e), "drop");
  },
  
  onDragOut: function(e) {
    dojo.html.removeClass(this._getNode(e), "drop");
  },

  _getNode: function(e) {
    if(dojo.html.hasClass(this.domNode, "bl_root_header")) {
      return this.domNode;
    } else {
      return this.domNode.parentNode;
    }
  },
  
  onDrop: function(e) {
    this.onDragOut(e);
    return this.view.onDrop(e.dragObject.domNode, this.domNode, "append", e);
  },

  repr: function() {
    return "FolderDropTarget: { node: " + this.domNode + ", types: [" + this.acceptedTypes.join(",") + "] }";
  }
});

dojo.declare("bl.tree.TrashDropTarget", bl.tree.TreeDropTarget, null,
  {  
  onDelete: function() {},

  onDragMove: function(e) {
    dojo.html.addClass(this.domNode, "drop");
  },
  
  onDragOut: function(e) {
    dojo.html.removeClass(this.domNode, "drop");
  },
  
  onDrop: function(e) {
    this.onDragOut(e); 
    if(this.view.model.selected.length > 0) { this.onDelete(); return true; }
    return false;
  },

  repr: function() {
    return "TrashDropTarget: { node: " + this.domNode + ", types: [" + this.acceptedTypes.join(",") + "] }";
  }
});



dojo.declare("bl.tree.TreeModelSimpleLoader", null, 
  function(obj) {
    this.obj = obj;
  },
  {  
  // load gathers tree data from a source.
  // by default, this source is a url that returns JSON
  // after loading the data, onSuccess is called with the data
  // onFailure is called if the call failed with the error event
  load: function(onSuccess, onFailure) {
    onSuccess(this.obj);
  }
});

// The TreeModelLoader is responsible for going off and finding
// data to fill the tree up with.  This class does so using
// an appropriate transport and a url.  If you subclass this
// class, be sure to extend the load method.
dojo.declare("bl.tree.TreeModelLoader", null, 
  function(url) {
    this.url = url;
  },
  { 
  // load gathers tree data from a source.
  // by default, this source is a url that returns JSON
  // after loading the data, onSuccess is called with the data
  // onFailure is called if the call failed with the error event
  load: function(onSuccess, onFailure) {
    var config = {
      url: this.url,
      mimetype: "text/json",
      load: function(type, js, evt) {
        if(js) onSuccess(js);
        else onFailure(evt);        
      },
      error: function(type, errObj) {
        onFailure(errObj);
      },
      sendTransport: false
    };
    dojo.io.bind(config);
  }
});

dojo.declare("bl.tree.Node", null, 
  function(data) {
    dojo.profiler.start("TreeNode::Initialize");
    this.parentNode = null;  
    this._unread = 0;
    this._kept = 0;
    if(data != null) {
      this._name = dojo.string.trimEnd(data.n);
      this._hidden = (typeof(data.p) == "undefined" ? false : data.p);
      this._notify_ignore = (typeof(data.ni) == "undefined" ? false : data.ni);
      this._display_mobile = (typeof(data.dm) == "undefined" ? false : data.dm);
    }
    dojo.profiler.end("TreeNode::Initialize");
  },
  {
  // ENUMS
  TYPES: { ROOT: 0, SUB: 1, FOLDER: 2 },

  isFolder: function() { return this._type == this.TYPES.FOLDER; },
  isSub: function() { return this._type == this.TYPES.SUB; },
  isRoot: function() { return this._type == this.TYPES.ROOT; },

  // METHODS
  id: function() { return "node"; },
  ss_id: function() { return ""; },
  nextSibling: function() {
    if(!this.parentNode) return null;
    var i = this.parentNode.indexOf(this.id());
    if(i < 0 || i == this.parentNode.kids.length - 1) return null;
    i++;
    return this.parentNode.kids[i];
  },
  prevSibling: function() {
    if(!this.parentNode) return null;
    var i = this.parentNode.indexOf(this.id()) - 1;
    if(i < 0) return null
    return this.parentNode.kids[i];
  },
    // PROPERTIES
  name: function() {
    return this.__prop(arguments, "name", dojo.lang.isString);
  },
  unread: function() {
    return this.__prop(arguments, "unread", dojo.lang.isNumber);
  },
  hasUnread: function() {
    return this._unread > 0;
  },
  readAll: function() {
    if(this.hasUnread()) this.unread(0);
  },
  kept: function() {
    return this.__prop(arguments, "kept", dojo.lang.isNumber);
  },
  hasKept: function() {
    return this._kept > 0;
  },
  type: function() {
    return this.__prop(arguments, "type", function(v) { return v == 0 || v == 1 || v == 2; });
  },
  hidden: function() {
    return this.__prop(arguments, "hidden");
  },
  notify_ignore: function() {
    return this.__prop(arguments, "notify_ignore");
  },
  display_mobile: function() {
    return this.__prop(arguments, "display_mobile");
  },  

  // HELPERS
  __changed: function(what) {
    if(this.parentNode) this.parentNode.kidChanged([ this ], what);
  },
  __prop: function(args, field, validator) {
    var name = "_" + field;
    switch(args.length) {
      case 0:
        return this[name];
      case 1:
       if(dojo.lang.isFunction(validator)) {
         if(!validator(args[0])) { throw new Error("Invalid value for " + field + ": {"+args[0]+"}"); }
       }
       if(this[name] != args[0]) {
         this[name] = args[0];
         this.__changed(field);
       }
       return args[0];
       
    default:
      throw new Error("Invalid number of args for prop.  Trying to get or set " + field + ".");
    }
  },
  repr: function() {
    return this.id() + "-" + this.type();
  }
});

dojo.declare("bl.tree.TreeSubNode", bl.tree.Node, 
  function(data) {
    dojo.profiler.start("TreeSubNode::Initializer");
    this._type = this.TYPES.SUB;  
    if(data != null) {
      this._error = data.e || 0;
      this._favicon = data.ico;
      this._unread = data.un;
      this._kept = data.kn;
      this._site_id = data.sid;
      this._sub_id = data.id;
      this.isSearch = (typeof(data.se) == "undefined" ? false : data.se);
      this.isWeather = (typeof(data.we) == "undefined" ? false : data.we);
      if(this.isWeather) {
        this._name = this._name.replace(/\s*\(.+\)/gm, "");
      }
    }
    dojo.profiler.end("TreeSubNode::Initializer");
  },
  {
  id: function() {
    return bl.tree.TreeSubNode.IdFor(this.sub_id(), this.site_id());
  },
  ss_id: function() {
    return this.sub_id();
  },
  error: function() {
    return this.__prop(arguments, "error");
  },
  isError: function() {
    return this._error != 0;
  },
  favicon: function() {
    return this.__prop(arguments, "favicon");
  },
  sub_id: function() {
    return this.__prop(arguments, "sub_id");
  },
  site_id: function() {
    return this.__prop(arguments, "site_id");
  }
});

bl.tree.TreeSubNode.IdFor = function(sub_id, site_id) {
  return "sub_" + sub_id + "_" + site_id;
}

dojo.declare("bl.tree.TreeFolderNode", bl.tree.Node, 
  function(data) {
    dojo.profiler.start("TreeFolderNode::FromData");

    this._type = this.TYPES.FOLDER;
    this.kids = [];
    this.kidMap = {};
    if(dojo.lang.isObject(data)) {
      this.kids = []; 
      this._folder_id = data.id;
      this._sort_order = data.sm;
      this._sort_order_text = data.sms;
      for(var i = 0; i < data.kids.length; i++) {
        this.replaceAtQuiet(new bl.tree.TreeSubNode(data.kids[i]), i);
      }
    }
    dojo.profiler.end("TreeFolderNode::FromData");
 
  },

  {
    SORT: { DEFAULT: 0, UNSORTED: 1, AZ: 2, ZA: 3, UNREAD: 4, OLDEST_FIRST: 5, NEWEST_FIRST: 6 },
    id: function() { 
      return bl.tree.TreeFolderNode.IdFor(this.folder_id()) 
    },
    ss_id: function() {
      return this.folder_id();
    },
    readAll: function() {
      var howmany = this.unread();
      if(howmany > 0) {
        dojo.lang.forEach(this.kids, 
            function(n) {
              n.readAll();
            });
      }
    },
    folder_id: function() {
      return this.__prop(arguments, "folder_id");
    },
    sort_order: function() {
      return this.__prop(arguments, "sort_order");
    },
    sort_order_text: function() {
      return this.__prop(arguments, "sort_order_text");
    },
    get: function(id) {
      return this.kidMap[id];
    },
    getOrder: function() {
      var order = [];
      for(var i = 0; i < this.kids.length; i++) {
        order.push(this.kids[i].ss_id());
      }
      return order.join(",");
    },
    indexOfRef: function(n) {
      return bl.lang.indexOf(this.kids, function(s) { return s == n; });
    },
    indexOf: function(id) {
      return bl.lang.indexOf(this.kids,
                             function(s) {
                               return s.id() == id;
                             });
    },
    add: function(node) {
      switch(this.sort_order()) {
        case this.SORT.UNSORTED:
          this.insertAt(node, this.kids.length);
          break;
        case this.SORT.AZ:
        case this.SORT.DEFAULT:
          this.insert(node, function(node, other) {
            return node._name.toLowerCase() > other._name.toLowerCase();
          });
          break;
        case this.SORT.ZA:
          this.insert(node, function(node, other) {
            return node._name.toLowerCase() < other._name.toLowerCase();
          });
          break;
        case this.SORT.UNREAD:
          this.insert(node, function(node, other) {
            return node._unread < other._unread;
          });
          break;
        case this.SORT.OLDEST_FIRST:
          this.insert(node, function(node, other) {
            return node._updated_on > other._updated_on;
          });
          break;
        case this.SORT.NEWEST_FIRST:
          this.insert(node, function(node, other) {
            return node._updated_on < other._updated_on;
          });
          break;

        default:
          throw new Error("Unknown sort type:" + this.sort_order());
          break;
      }   
    },
    append: function(node) {
      this.insertAt(node, this.kids.length);    
    },
    insertBefore: function(node, ref) {
      var index = this.indexOf(ref.id());
      if(index < 0) index = 0;
      this.insertAt(node, index);
    },
    insertAfter: function(node, ref) {
      var index = this.indexOf(ref.id());
      if(index < 0) index = this.kids.length;
      else index++;    
      
      this.insertAt(node, index);
    },
    insert: function(node, furtherOn) {
      var i = 0;
      for(i; i < this.kids.length; i++) {
        if(!furtherOn(node, this.kids[i])) break;
      }
      this.insertAt(node, i);
      return i;
    },
    insertAt: function(node, index) {
      node.parentNode = this;
      this.kids.splice(index, 0, node);
      this.kidMap[node.id()] = node;
      this._incCounts(node);
      if(this.parentNode) this.parentNode.notifyAdd(this, node);
    },
    notifyAdd: function(who, what) {
      if(this.parentNode) this.parentNode.notifyAdd(who, what);
    },
    replaceAtQuiet: function(node, index) {
      node.parentNode = this;
      this.kids[index] = node;
      this.kidMap[node.id()] = node;
      this._incQuiet(node);
    },
    remove: function(node) {
      var i = dojo.lang.find(this.kids, node, true);
      if(i >= 0) { this.removeAt(i); }
    },
    removeId: function(id) {
      var i = bl.lang.indexOf(this.kids,
                              function(s) {
                                return s.id() == id;
                              });
      if(i >= 0) this.removeAt(i);
    },
    removeAt: function(index) {
      var n = this.kids[index];
      if(n == null) return;
      this.kids.splice(index,1);
      delete this.kidMap[n.id()];
      this._decCounts(n);
      if(this.parentNode) this.parentNode.notifyRemove(this);
    },
    notifyRemove: function(who) {
      if(this.parentNode) this.parentNode.notifyRemove(who);
    },
    kidChanged: function(who, what) {
      switch(what) {
        case "unread":
        case "kept":
          this._updateCounts();
          break;
      }
      who.push(this);
      if(this.parentNode) this.parentNode.kidChanged(who, what);
    },
    _updateCounts: function() {
      var un = 0, kept = 0;
      dojo.lang.forEach(this.kids, function(k) {
        un += k.unread();
        kept += k.kept();
      });

      this._unread = un;
      this._kept = kept;
    },
    _incQuiet: function(node) {
      this._unread += node._unread;
      this._kept += node._kept;
    },
    _incCounts: function(node) {
      if(node.unread() != 0) this.unread(this.unread() + node.unread());
      if(node.kept() != 0) this.kept(this.kept() + node.kept());
    },
    _decCounts: function(node) {
      if(node.unread() != 0) this.unread(this.unread() - node.unread());
      if(node.kept() != 0) this.kept(this.kept() - node.kept());
    } 
  });

bl.tree.TreeFolderNode.IdFor = function(folder_id) {
  return "folder_" + folder_id;
}

// A root node for the tree
dojo.declare("bl.tree.TreeRootNode", bl.tree.TreeFolderNode, 
  function() {
    this.type(this.TYPES.ROOT);
  },
  {
  id: function() { return "treeRoot"; },
  ss_id: function() { return 0; },
  get: function(id) {
    if(id == this.id()) return this;

    var n = this.kidMap[id];
    if(n != null) return n;

    for(var i = 0; i < this.kids.length; i++) {
      var kid = this.kids[i];
      if(kid.isFolder()) {
        var sub = kid.get(id);
        if(sub != null) return sub;
      }
    }
    return null;
  },
  sub_count: function(onlyThoseUnread) {
    var acc = 0;
    if(onlyThoseUnread == null) onlyThoseUnread = false;
    this.visit(function(n) {
      if(n.isSub() && (!onlyThoseUnread || (n.hasUnread() || n.hasKept())) == n.TYPES.SUB ) acc++;
    });
    return acc;
    
  },
  visit: function(visitor) {
    visitor(this);
    dojo.lang.forEach(this.kids, 
                      function(f) { 
                        visitor(f);
                        if(f._type == f.TYPES.FOLDER) dojo.lang.forEach(f.kids, visitor);
                      });
  },
  __changed: function(what) {
    this.kidChanged([this], what);
  },
  iterator: function(startAt) {
    if(startAt == null) 
      startAt = this;
    var self = this;

    return new function() {
      this.startAt = startAt
      this.cur = startAt;
      
      this.next = function() {
        if(this.cur.type() != this.cur.TYPES.SUB && this.cur.kids.length > 0) {
          this.cur = this.cur.kids[0];
          return this.cur != this.startAt;
        }
        var n = this.cur.nextSibling();
        if(n == null) {
          if(this.cur.parentNode.isRoot() || (this.cur.parentNode.isFolder() && this.cur.parentNode.nextSibling() == null)) 
            n = self;
          if(this.cur.parentNode.isFolder()) 
            n = this.cur.parentNode.nextSibling();
            if(n == null) {
              n = self;              
            }
        }
        if(n == null || n == this.startAt) { this.cur = null; return false; }
        this.cur = n;
        return true;
        
      }

      this.get = function() { return this.cur; }
    }
  }
});


dojo.declare("bl.tree.TreeModel", null, 
  function(loader) {
    this.loader = loader;
    this.root = new bl.tree.TreeRootNode();
    dojo.event.connect(this.root, "kidChanged", this, "kidChanged");
    dojo.event.connect(this.root, "notifyAdd", this, "onKidAdd");
    dojo.event.connect(this.root, "notifyRemove", this, "onKidRemove");
    this.root._name = "Empty tree";
    this.selected = [];
    this.multiselect = false;
    this._sendUpdates = true;
  },
  {
  suspendSelectionUpdates: function() {
    this._sendUpdates = false;
    this._sawUpdates = false;
  },
  resumeSelectionUpdates: function() {
    this._sendUpdates = true;
    if(this._sawUpdates) this.onSelectionChanged();
  },

  changeLoader: function(l) {
    this.loader = l;
  },
  
  visit: function(visitor) {
    // walk all the kids, visiting nodes as you pass.
    // walk should be: root, then all things under root, walking into folders as we find them.
    this.root.visit(visitor);    
  },
  
  iterator: function(startAt) {
    return this.root.iterator(startAt);
  },
         
  select: function(id) {
    var node = this.get(id);
    if(!node) return;

    if(this.isSelected(node) && this.selected.length == 1) return;

    this.selected = [ node ];
    this._notifySelectionChanged();
  },

  _fixupSelected: function() {
    var newSel = [];
    for(var i = 0; i < this.selected.length; i++) {
      var n = this.get(this.selected[i].id());
      if(n) newSel.push(n);
    }  
    this.selected = newSel;
  },

  addToSelection: function(id) {
    if(!this.multiselect || this.root.id() == id) return this.select(id);
    var node = this.get(id);
    if(!node) return;
    if(this.isSelected(this.root.id())) this.deselect(this.root.id());
    if(!this.isSelected(node)) {
      if(node.type() == node.TYPES.FOLDER) {
        var _this = this;
        dojo.lang.forEach(node.kids, function(k) { _this._deselect(k); });
      }
      this.selected.push(node);
      this._notifySelectionChanged();
    }
  },

  deselect: function(id) {
    var node = this.get(id);
    if(!node) return;
    this._deselect(node);
    this._notifySelectionChanged();
  },

  _deselect: function(node) {
    var i = dojo.lang.find(this.selected, node, true);
    if(i < 0) return;
    this.selected.splice(i, 1);
  },

  clearSelection: function() {
    this.selected = [];
    this._notifySelectionChanged();
  },

  isSelected: function(node) {
    if(dojo.lang.isString(node)) node = this.get(node);
    return (dojo.lang.find(this.selected, node, true) > -1);
  },

  isInSelectionChain: function(node) {
    if(dojo.lang.isString(node)) node = this.get(node);
    for(var i = 0; i < this.selected.length; i++) {
      var n = this.selected[i];
      if(n === node || n.parentNode === node) return true;
    }
    return false;
  },

  _notifySelectionChanged: function() {
    if(this._sendUpdates) this.onSelectionChanged();
    else this._sawUpdates = true;
    
  },

  get: function(id) {
    return this.root.get(id);
  },

  move: function(idToMove, refId, location) {
    var refNode = this.get(refId);
    if(refNode == null) { $bl.log("Invalid refId: " + refId); return false }
    var toMove = this.root.get(idToMove);
    if(toMove == null) { $bl.log("Invalid idToMove: " + idToMove); return false; }
   
    if(!dojo.lang.inArray(["before", "after", "append"], location)) { $bl.log("Invalid location:" + location); return false; }
    if(location == "append" && refNode.type() == refNode.TYPES.SUB) { $bl.log("Cannot append to a sub"); return false; }

    if(refNode === toMove) return false;

    switch(location) {
      case "before":
        if(refNode.prevSibling() === toMove) return false;
        toMove.parentNode.remove(toMove);
        refNode.parentNode.insertBefore(toMove, refNode);
        refNode.parentNode.sort_order(refNode.parentNode.SORT.UNSORTED);
	      refNode.parentNode.sort_order_text(bl.translate.get("unsorted"));        
        break;
      case "after":
        if(refNode.nextSibling() === toMove) return false;
        toMove.parentNode.remove(toMove);
        refNode.parentNode.insertAfter(toMove, refNode);
	      refNode.parentNode.sort_order(refNode.parentNode.SORT.UNSORTED);
	      refNode.parentNode.sort_order_text(bl.translate.get("unsorted"));
        
        break;
      case "append":
        if(refNode.indexOf(toMove.id()) > -1) return false;
        toMove.parentNode.remove(toMove);
        refNode.add(toMove);
        break;
    }
    return true;
  },

  moveSelected: function(refId, location) {
    var refNode = this.get(refId);  
    if(!refNode) { $bl.log("Invalid refId: " + refId); return false; }
    var nodesToMove = this.selected;
    if(nodesToMove.length == 0) return false;
    if(this.isSelected(refNode)) return false;
    if(!dojo.lang.inArray(["before", "after", "append"], location)) { $bl.log("Invalid location:" + location); return false; }
    if(location == "append" && refNode.type() == refNode.TYPES.SUB) { $bl.log("Cannot append to a sub"); return false; }

    if(nodesToMove.length == 1) return this.move(nodesToMove[0].id(), refId, location);

    var hasSubs = false;
    var hasFolders = false;
    dojo.lang.forEach(nodesToMove, function(n) {
      if(n.isSub()) hasSubs = true;
      else if(n.isFolder()) hasFolders = true;
      else throw new Error("Cannot move root!");
      if(hasSubs && hasFolders) return "break";
    });

    if(refNode.isFolder() && location == "append" && hasFolders) {
      $bl.log("Cannot append folders to folder");
      return false;
    }

    var _this = this;
    nodesToMove.sort(function(a,b) {
      var p = null;
      if(a.isSub() && b.isSub()) {
        if(a.parentNode === b.parentNode) {
          p = a.parentNode;
        } else {
          if(a.parentNode.isFolder()) a = a.parentNode;
          if(b.parentNode.isFolder()) b = b.parentNode;
          p = _this.root;
        }
      } else if(a.isFolder() || b.isFolder()) {
        p = _this.root;
        if(!a.parentNode.isRoot()) a = a.parentNode;
        if(!b.parentNode.isRoot()) b = b.parentNode;
      }
      return p.indexOf(a.id()) - p.indexOf(b.id());
     });

    var _this = this;
    switch(location) {
      case "before":
        dojo.lang.forEach(nodesToMove, function(n) {
          n.parentNode.remove(n);
          refNode.parentNode.insertBefore(n, refNode);
          refNode.parentNode.sort_order(refNode.parentNode.SORT.UNSORTED);
          refNode.parentNode.sort_order_text(bl.translate.get("unsorted"));
        });
        break;
      case "after":
        dojo.lang.forEach(nodesToMove, function(n) {
          n.parentNode.remove(n);
          refNode.parentNode.insertAfter(n, refNode);
          refNode.parentNode.sort_order(refNode.parentNode.SORT.UNSORTED);
          refNode.parentNode.sort_order_text(bl.translate.get("unsorted"));
          refNode = n;
        });
        break;
      case "append":
        dojo.lang.forEach(nodesToMove, function(n) {
          n.parentNode.remove(n);
          refNode.add(n);
        });
        break;
    }
    return true;
  },
  
  load: function() {
    this.loading();
    this.loader.load(dojo.lang.hitch(this, this.handleLoad), dojo.lang.hitch(this, this.handleLoadFailure));
  },

  handleLoad: function(data) {
    // compare data
    var oldUnreadCount = this.root.unread();
    var oldKeptCount = this.root.kept();
    var oldRoot = this.root;
    this.root = this._buildNodes(data);
    dojo.event.connect(this.root, "kidChanged", this, "kidChanged");
    dojo.event.connect(this.root, "notifyAdd", this, "onKidAdd");
    dojo.event.connect(this.root, "notifyRemove", this, "onKidRemove");
    this._fixupSelected();
    this.loaded(oldRoot, this.root);
    this.onLoadCountChange("unread", oldUnreadCount, this.root.unread());
    this.onLoadCountChange("kept", oldKeptCount, this.root.kept());
  },

  handleLoadFailure: function(err) {
    this.loaded();
    alert("error loading data for tree: " + err.name + "=> " + (err.message || err.description || "no message"));
  },

  kidChanged: function(who, what) {
    this.onChanged(who, what);
  },

  onKidAdd: function(who, what) {
    this.onChanged(who, "add");
  },

  onKidRemove: function(who) {
    this.onChanged(who, "remove");
  },

  // EVENTS
  onChanged: function(/* array of changed nodes */ who, /* change identifier */ what) {},
  onSelectionChanged: function() {},
  loading: function() {},
  loaded: function() {},
  onLoadCountChange: function(which, oldCount, newCount) {},
  
  // PRIVATE METHODS
  _buildNodes: function(data) {
    dojo.profiler.start("TreeModel::buildNodes");
    var nt = new bl.tree.TreeRootNode();
    nt.sort_order(data.sm);
    nt.sort_order_text(data.sms);
    var k = data.kids;
    for(var i = 0; i < k.length; i++) {
      if(k[i].t == nt.TYPES.SUB) {
        var sub = new bl.tree.TreeSubNode(k[i]);
        nt.replaceAtQuiet(sub, i);
      } else if(k[i].t == nt.TYPES.FOLDER) {
        var f = new bl.tree.TreeFolderNode(k[i]);
        nt.replaceAtQuiet(f, i);
      } 
    }
    dojo.profiler.end("TreeModel::buildNodes");
    return nt;    
  }
});

dojo.declare("bl.tree.BasicTreeController", null,
  function(model, view, cfg) {
    if(!model && !view && !cfg) return;
    this.view = view;
    this.model = model;
    this._lastPicked = null;

    this.config = { showCheckboxes: false, showCounts: true };

    if(cfg) dojo.lang.mixin(this.config, cfg);

    dojo.event.connect(this.view, "onPick", this, "handleViewPick");
    dojo.event.connect(this.view, "onClick", this, "handleViewClick");
    dojo.event.connect(this.view, "onDblClick", this, "handleViewDblClick");
  },
  {
    handleViewPick: function(id, evt) {
      var n = this.model.get(id);
      if(!n) return;

      if(!this.isAllowablePick(n)) return;

      if((this.model.multiselect && this.model.selected.length > 0 && (evt.ctrlKey || evt.shiftKey || evt.metaKey)) 
          || this.model.isSelected(id)) return;
      $bl.log("setting ignoreNext to true in TreeController");
      this.ignoreNextClick = true;
      this.selectionChanging();
      $bl.log("before select");
      this.model.select(id);
      $bl.log("after select");
      this._lastPicked = id;
    },

    isAllowablePick: function(node) {
      return true;
    },

    handleViewClick: function(id, evt) {
      
      if(this.ignoreNextClick) {
        $bl.log("ignoring click in TreeController");
        this.ignoreNextClick = false;
        return;
      }

      if(!this.model.multiselect) {
        if(!this.model.isSelected(id)) {
          this.model.select(id);
        }
      } else {

        if(!this.model.multiselect) { return; }

        var n = this.model.get(id);
        if(!n) return;

        if(n.isRoot()) {
          if(this.model.isSelected(id)) return;
          this.model.select(id);
          this._lastPicked = id;
          return;
        }

        if(this.model.isSelected(id)) {       
          if(this.model.selected.length > 1) {
            (evt.ctrlKey || evt.metaKey) ? this.model.deselect(id) : this.model.select(id);
          } 
        } else {
          this.selectionChanging();
          var addTo = evt.ctrlKey || evt.metaKey || false;
          var addRange = evt.shiftKey || false;

          if(addRange && this._lastPicked != null) {
            var last = this.model.get(this._lastPicked);
            if(last.parentNode != n.parentNode) return;
            var par = n.parentNode;
            var start = par.indexOf(n.id());
            var end = par.indexOf(last.id());
            if(start > end) { var t = start; start = end; end = t; }
            this.model.suspendSelectionUpdates();
            if(!addTo) this.model.clearSelection();
            for(var i = start; i <= end; i++) {
              this.model.addToSelection(par.kids[i].id());
            }
            this.model.resumeSelectionUpdates();
            this._lastPicked = id;
          } else {
            if(n.isSub() && n.parentNode.isFolder() && this.model.isSelected(n.parentNode.id())) {
              if(addTo) {
                this.model.suspendSelectionUpdates();
                this.model.deselect(n.parentNode.id());
                for(var i = 0; i < n.parentNode.kids.length; i++) {
                  if(n.parentNode.kids[i].id() != id) 
                    this.model.addToSelection(n.parentNode.kids[i].id());
                }
                this.model.resumeSelectionUpdates();
              } else { 
                this.model.select(id);
                this._lastPicked = id;
              }
            } else {
              if(n.isRoot()) addTo = false;
              addTo ? this.model.addToSelection(id) : this.model.select(id);
              this._lastPicked = id;
            }
          }
        }
      }
    },
    handleViewDblClick: function() {},
    selectionChanging: function() {},
    load: function() {
      this.model.load();
    }
  });

dojo.declare("bl.tree.InlineEditTreeController", bl.tree.BasicTreeController, 
  function(model, view, cfg) {
    this._editTimer = null;
    this._editing = null;
    this._lastPicked = null;
    dojo.event.connect(this.view, "onNodeMove", this, "handleViewNodeMove");
    dojo.event.connect(this.view, "onEditEnd", this, "finishCurrentEdit");
    dojo.event.connect(document.body, "onclick", this, "genericClick");
    this.ignoreNextClick = false;
  },
  {
  genericClick: function(evt) {
    if(evt.ctrlKey || evt.shiftKey || evt.metaKey || bl.alg.isInputElement(evt.target)) { return; }
    this.finishCurrentEdit();
    this.model.clearSelection();
  },

  selectionChanging: function() {
    this.finishCurrentEdit();
  },

  handleViewClick: function(id, evt) {
    if(this.ignoreNextClick) {
      this.ignoreNextClick = false;
      return;
    }
    this.inherited("handleViewClick", arguments);
    var n = this.model.get(id);
    if(!n) return;
    if(this.model.isSelected(id) && this.model.selected.length == 1) { if(this._editTimer != null) { window.clearTimeout(this._editTimer); }
      this._editTimer = dojo.lang.setTimeout(this, "edit", 200, id);
    }
  },

  handleViewDblClick: function(id, evt) {
    if(this._editTimer != null) {
      window.clearTimeout(this._editTimer);
      this._editTimer = null;
    }
  },
  
  handleViewNodeMove: function(idOfNode, idOfRef, location) { 
    return this.model.moveSelected(idOfRef, location);
  },

  edit: function(idOfNode) {
    if(this._editTimer) window.clearTimeout(this._editTimer);
    this._editTimer = null;
    if(this._editing != null) this.finishCurrentEdit();
    var n = this.model.get(idOfNode);
    if(!n) return;
    //if(n.isSearch) { window.alert(bl.translate.get("searchfeederror")); return; }
    this._editing = n;
    this.view.showEdit(idOfNode);
  },

  finishCurrentEdit: function() {
    if(this._editing == null || this._saving) return;
    var tv = this.view.currentEditValue();
    if(tv != null && tv != "" && tv != this._editing.name()) {
      this._saving = true;
      var args = this._makeArgs();
      bl.lang.extend(args, {
        subid: this._editing.ss_id(),
        domod: 1,
        Name: tv,
        folder: this._editing.parentNode.ss_id()       
      });
      if(!this._editing.notify_ignore()) args.Ignore = "on";
      if(this._editing.display_mobile()) args.DisplayMobile = "on";
      var _this = this;
      
      dojo.io.bind( {
        url: "/modsub",
        content: args,
        method: "POST",
        sendTransport: false,
        load: function() { 
          _this.view.commitEdit(); 
          _this._editing._name = tv;
          _this._editing = null; 
          _this._saving = false
        },
        error: function(err) { 
          window.alert("Failed to save the new name"); 
          _this.saving = false;
          _this._editing = null;
          _this.view.cancelEdit();
        }
      });
    } else {
      this.view.cancelEdit();
      this._editing = null;
      this._saving = false;
    }
  },
  _makeArgs: function() { return {}; }
});


dojo.declare("bl.tree.DefaultFormatter", null, 
  function(config) {
    this.cfg = { showCounts: true, showSort: false, showIndicators: false, showErrors: true, rootName: function(node, tree) { return node.sub_count(tree.showOnlyUnread) + (tree.showOnlyUnread ? " updated" : "") + " feeds";  } };
    if(config) dojo.lang.mixin(this.cfg, config);
  },
  {
   // ui is the ui node thats being built for the TreeView
   // model is the current node from the model
   format: function(b, model, tree) {
     playlistString = this.playlist?("&playlistid="+this.playlist):"";
     switch(model.type()) {
       case model.TYPES.ROOT:
         //dojo.profiler.start("formatRoot");
         b.append("<a ondragstart=\"return false;\" onclick=\"return false;\" href=\"/myblogs_display?all=1" + playlistString + "\" target=\"basefrm\" bl_id=\""+ model.id() +"\" tid=\""+tree.baseId+"a"+ model.id() +"\">");
         b.append(this.cfg.rootName(model, tree));
         b.append("</a>")
         if(tree.showOnlyUnread) {
           b.append(" <a ondragstart=\"return false;\" class=\"bl_actionLink\" href=\"#\" id=\"" + tree.baseId + "_uptgl\">("+ bl.translate.get("showall") +")</a>");
         }
         if(this.cfg.showSort) this._appendSort(b, model, tree);
         //dojo.profiler.end("formatRoot");
         break;
         
       case model.TYPES.FOLDER:
         //dojo.profiler.start("formatFolder");
         b.append("<a ondragstart=\"return false;\" onclick=\"bl.tree.TreeView.preventDefault(event);\" href=\"/myblogs_display?folder=" +  model.folder_id() 
                + playlistString
                + "\"" 
                + " target=\"basefrm\"" 
                + " title=\"" + model.name().replace(/"/gm, "&quot;") + ": " + model.unread() + " unread, " + model.kept() + " kept new\""
                + " bl_folder=\"" + model.folder_id() + "\""
                + " bl_id=\"" + model.id() + "\""
                + " bl_name=\"" + model.name() + "\""
                + " id=\""+tree.baseId+"a" + model.id() + "\"");
         var cl = null;
         if(this.cfg.showCounts) cl = this._getCountClasses(model);
         else cl = [];
         cl.push("bl_folderName");
         this._appendClasses(b, cl);
         b.append(">");
         b.append(model.name());
         if(this.cfg.showCounts) this._appendCounts(b, model, tree);
         b.append("</a>");
         if(this.cfg.showSort) this._appendSort(b, model, tree);
         if(this.cfg.showIndicators) this._appendIndicators(b, model, tree);
         //dojo.profiler.end("formatFolder");
         break;
         
       case model.TYPES.SUB:
         //dojo.profiler.start("formatSub");
         b.append("<a ondragstart=\"return false;\" onclick=\"return false;\" href=\"/myblogs_display?sub=" + model.sub_id() + "&site=" + model.site_id()
                + playlistString
                + "\"" 
                + " target=\"basefrm\"" 
                + " title=\"" + model.name().replace(/"/gm, "&quot;") + ": " + model.unread() + " unread, " + model.kept() + " kept new\""
                + " bl_sub=\"" + model.sub_id() + "\""
                + " bl_site=\"" + model.site_id() + "\""
                + " bl_id=\"" + model.id() + "\""
                + " id=\""+tree.baseId+"a" + model.id() + "\" ");
         if(this.cfg.showCounts) {
           this._appendClasses(b, this._getCountClasses(model));
         }
         b.append(">");
         b.append(model.name());
         if(this.cfg.showCounts) this._appendCounts(b, model, tree);
         b.append("</a>");
         if(this.cfg.showIndicators) this._appendIndicators(b, model, tree);
         if(this.cfg.showErrors && model.isError()) {
           b.append(" <a href=\"javascript:showError(" + model.error() + ");\" class=\"bl_feed_error\">[!]</a>");
         }
         
         //dojo.profiler.end("formatSub");
         break;
         
       default:
         throw new Error("Unknown model node type: " + model.type());
     }
  },

  _appendCounts: function(b, model, tree) {
    dojo.profiler.start("renderCounts");
    b.append(" <span id=\""+ tree.baseId + "un" + model.id() 
        + "\" style=\"" + ((model.unread() == 0 && model.kept() == 0) ? "display:none" : "")
        + "\" class=\"" + ((model.unread() > 0) ? "urb" : "ur") 
        + "\" title=\"" + model.unread() + " unread items" +  "\">(" + model.unread() + ")</span>");
    b.append(" <span id=\""+ tree.baseId + "kp" + model.id() 
        + "\" style=\"" + (model.hasKept() ? "" : "display:none")
        + "\" class=\"fl\" title=\"" + model.kept() + " items kept as new" +  "\">(" + model.kept() + ")</span>");
    dojo.profiler.end("renderCounts");
  },
  _appendSort: function(b, model, tree) {
    b.append("<span id=\""+tree.baseId + "srt" + model.id() + "\"class=\"sort\"> "+ model.sort_order_text() +"</span>");
  },
  _appendIndicators: function(b, model, tree) {
    b.append("<span class=\"indi\" id=\""+ tree.baseId + "ind" + model.id() +"\">");
    if(model.hidden()) b.append("<img ondragstart=\"return false;\" src=\"images/lock.gif\" style=\"width:11px;height:13px\" title=\""+ bl.translate.get("nonpublic")  +"\" />");
    if(model.notify_ignore()) b.append("<img ondragstart=\"return false;\" src=\"images/no_notify.gif\" style=\"width:13px;height:13px\" title=\""+ bl.translate.get("ignorenotify")  +"\" />");
    if(!model.display_mobile()) b.append("<img ondragstart=\"return false;\" src=\"images/no_display_mobile.gif\" style=\"width:12px;height:13px\" title=\""+ bl.translate.get("notmobile")  +"\" />");
    b.append("</span>");
  },
  _getCountClasses: function(model) {
    var c = [];
    if(model._unread > 0) c.push("unread");
    if(model._kept > 0) c.push("kept");
    return c;
  },
  _appendClasses: function(b, classes) {
    if(classes.length > 0) b.append(" class=\"" + classes.join(" ") + "\"");
  }
});

dojo.declare("bl.tree.ViewState", null, 
  function(cookie, prefix) {
    this.cookie = cookie;
    var cook = dojo.io.cookie.get(cookie);
    this.openFolders = [];
    if(cook) {
      this.openFolders = cook.split("^"); 
    }
    this.prefix = prefix || "";
  },
  {
    folderOpen: function() { 
      var id = this.prefix + arguments[0];
      switch(arguments.length) {
        case 1:
          return dojo.lang.find(this.openFolders, id) > -1;
          break;
        case 2:
          var i = dojo.lang.find(this.openFolders, id);
          if(arguments[1] && i < 0) {
            this.openFolders.push(id);
          } else if(!arguments[1] && i > -1) {
            this.openFolders.splice(i, 1);
          }
          dojo.io.cookie.set(this.cookie, this.openFolders.join("^"), 3650, null, null);
          break;
        default:
          throw new Error("Invalid number of arguments for folderState");
          break;
      }
    }
  });



dojo.declare("bl.tree.TreeView", null, 
  function(domNode, model, formatter, viewState) {
    this.model = model;
    this.baseId = domNode.id;
    dojo.event.connect(this.model, "onSelectionChanged", this, "_handleSelectionChange");
    dojo.event.connect(this.model, "onChanged", this, "_handleModelChange");
    dojo.event.connect(this.model, "loading", this, "showSpinner");
    dojo.event.connect(this.model, "loaded", this, "_handleModelLoaded");
    this.domNode = domNode;
  
    dojo.event.connect(this.domNode, "onmousedown", this, "_handleNodePick");
    dojo.event.connect(this.domNode, "onclick", this, "_handleNodeClick");
    dojo.event.connect(this.domNode, "ondblclick", this, "_handleNodeDblClick");

    this.formatter = formatter || new bl.tree.DefaultFormatter();
    this.viewState = viewState || new bl.tree.ViewState("blTree", "id");
    this._showCheckboxes = false;
    this._spinning = false;
    this._dropTargets = {};
    this._dragSources = {};
    this._currentEdit = null;
    this._renderTimer = null;
    this.selected = [];
    this.showOnlyUnread = false;
    this.dragAndDrop = false;
    this._suspendUpdates = false;
    this._sawUpdates = false;
    this._activeAnims = {};
  },
  {
  // CONSTANTS
  TOGGLE_OPEN_ICO:  "/images/tree_minus.gif",
  TOGGLE_CLOSE_ICO: "/images/tree_plus.gif",
  FOLDER_OPEN_ICO:  "/images/ftv2folderopen.gif",
  FOLDER_CLOSE_ICO: "/images/ftv2folderclosed.gif",
  SPINNER_ICO:      "/images/spin_2422.gif",
  SUB_DEFAULT_ICO:  "/images/ftv2doc.gif",
  
  // EVENTS
  onPick: function(id, evt) {},
  onClick: function(id, evt) {},
  onDblClick: function(id, evt) {},
  onNodeMove: function(idOfMoving, idOfRef, location) {},
  onEditEnd: function() {},

  onDrop: function(moving, ref, location, evt) {
    var m_id = this.blIdFor(moving);
    var ref_id = this.blIdFor(ref);
    this.onNodeMove(m_id, ref_id, location);
    return true;
  },

  byTreeId: function(id) {
    return dojo.byId(this.baseId + id);
  },

  blIdFor: function(node) {
    var n = dojo.dom.getFirstDescendant(node, function(n) { return $A(n, "bl_id") != null; } );
    return $A(n, "bl_id");
  },

  clear: function() {
    dojo.profiler.start("TreeView::clear");
    this._unhookEvents(this.domNode);
    var showOnly = dojo.byId(this.domNode.id + "_uptgl");
    if(showOnly) dojo.event.disconnect(showOnly, "onclick", this, "_handleShowToggle");
    if(this.domNode) dojo.dom.removeChildren(this.domNode);
    this._isRendered = false;
    this._spinNode = null;
    //dojo.event.browser.clean(this.domNode);
    dojo.profiler.end("TreeView::clear");
  },

  showCheckboxes: function(yesno) {
    if(this._showCheckboxes != yesno) {
      this._showCheckboxes = yesno;
      if(this._isRendered) this.render();	
    }
  },

  showEdit: function(id) {
    if(this._currentEdit != null) {
      this.onEditEnd();
      if(this._currentEdit != null) return;
    }

    var n = this.byTreeId("a" + id);
    var input = $E("input");
    input.type = "text";
    input.autocomplete = "off";
    input.value = dojo.string.trim(dojo.dom.textContent(n));
    var nwidth = dojo.style.getInnerWidth(n);
    var npwidth = dojo.style.getInnerWidth(n.parentNode) - 150;
    var inwidth = npwidth > nwidth ? npwidth : nwidth;
    input.style.width = inwidth  + "px";
    input.style.border = "1px solid #ccc";
    dojo.event.connect(input, "onkeypress", this, "_editKeyPress");
    //dojo.event.connect(input, "onblur", this, "onEditEnd");
    dojo.dom.insertAfter(input, n);
    dojo.html.hide(n);
    input.focus();
    //input.select();
    this._currentEdit = [input, n];
  },

  _editKeyPress: function(evt) {
    var k = evt.keyCode || evt.charCode;
    if(k == evt.KEY_ESCAPE) {
      dojo.event.browser.stopEvent(evt);
      this.cancelEdit();
    } else if(k == evt.KEY_ENTER) {
      evt.preventDefault();
      this.onEditEnd();
    }
  },

  currentEditValue: function() {
    if(this._currentEdit == null) {
      return null;
    }
    var n = this._currentEdit[0].value || "";
    n = dojo.string.trim(n);
    return n;
  },

  commitEdit: function() {
    if(this._currentEdit == null) return;
    var v = this.currentEditValue();
    var ce = this._currentEdit;
    this._currentEdit = null;
    //dojo.event.disconnect(ce[0], "onblur", this, "onEditEnd");
    dojo.event.disconnect(ce[0], "onkeypress", this, "_editKeyPress");
    dojo.dom.removeNode(ce[0]); 
    ce[1].innerHTML = bl.lang.escapeXml(v);
    dojo.html.show(ce[1], "");
  },

  cancelEdit: function() {
    if(this._currentEdit == null) return;
    var ce = this._currentEdit;
    this._currentEdit = null;
    //dojo.event.disconnect(ce[0], "onblur", this, "onEditEnd");
    dojo.event.disconnect(ce[0], "onkeypress", this, "_editKeyPress");
    dojo.dom.removeNode(ce[0]);
    dojo.html.show(ce[1], "");
  },

  _findSpinNode: function() {
    /*
    if(!this._isRendered) return null;
    if(this._spinNode == null) {
      this._spinNode = dojo.html.getElementsByClass("treeRoot", "h1")[0];
    }
    return this._spinNode;
    */
  },

  showSpinner: function() {
    /*
    if(!this._isRendered || this._spinning) return;
    this._spinning = true;
    var sn = this._findSpinNode();
    sn.style.backgroundImage = this.SPINNER_ICO;
    */
  },

  hideSpinner: function() {
    /*
    if(!this._isRendered || !this._spinning) return;
    this._spinning = false;
    var sn = this._findSpinNode();
    sn.style.backgroundImage = this.FOLDER_OPEN_ICO;
    */
  },

  render: function() {
    dojo.profiler.start("TreeView::render");
    if(!this.domNode) throw new Error("no node to build the tree upon");
    if(this.__renderTimer != null) {
      clearTimeout(this.__renderTimer);
      this.__renderTimer = null;
    }
    this.clear();
    
    var root = this.model.root;
    var b = new dojo.string.Builder();
    dojo.profiler.start("TreeView::render::buildHtml");
    b.append("<div id=\""+this.baseId+"l"+root.id()+"\" bl_id=\""+ root.id() +"\" class=\"treeRoot bl_root_header\">");
    
    if(this._showCheckboxes) this._appendCheck(b, "toplevel", "1");
    
    this.formatter.format(b, root, this);
    
    b.append("</div>");
    if(root.kids.length > 0) {
      b.append("<ul class=\"treeList rootList\" id=\""+this.baseId+"u"+ root.id() +"\">");
      for(var i = 0; i < root.kids.length; i++) {
        var moreClasses = "";
        if(i == 0) moreClasses = "first";
        if(i == (root.kids.length -1)) moreClasses = "last";
        if(root.kids[i].isFolder()) {
          this._renderFolder(root.kids[i], b, moreClasses);
        } else if(root.kids[i].isSub()) { 
          this._renderSub(root.kids[i], b, moreClasses);
        } else {
          throw new Error("Invalid node type: " + root.kids[i].type());
        }
      }
      b.append("</ul>");
    }

    dojo.profiler.end("TreeView::render::buildHtml");
    
    dojo.profiler.start("TreeView::render::assign");
    this.domNode.innerHTML = b.toString();
    dojo.profiler.end("TreeView::render::assign");

    this._hookupEvents();
    var showToggle = dojo.byId(this.domNode.id + "_uptgl");
    if(showToggle) dojo.event.connect(showToggle, "onclick", this, "_handleShowToggle");
    this._handleSelectionChange();
    this._isRendered = true;
    dojo.profiler.end("TreeView::render");

    dojo.profiler.dump(true);
  },
    
  _renderFolder: function(folder, b, moreClasses) {
    dojo.profiler.start("TreeView::renderFolder");
    if(moreClasses == null || moreClasses == "") moreClasses = "";
    else moreClasses = " " + moreClasses;
    var isOpen = this.viewState.folderOpen(folder.folder_id());
    if(this.showOnlyUnread && !(folder.hasUnread() || folder.hasKept() || this.model.isInSelectionChain(folder))) {
      dojo.profiler.end("TreeView::renderFolder");
      return;
    }
    b.append("<li class=\"folder "+ (isOpen ? "open" : "closed") + moreClasses
	    + "\" id=\""+this.baseId+"l"+ folder.id() 
	    + "\" bl_id=\"" + folder.id() 
	    + "\" bl_folder=\"" + folder.folder_id() 
	    + "\">"); 
    b.append("<div class=\"bl_folder_header\" id=\""+ this.baseId +"f"+ folder.id()  +"\">");
    b.append("<a class=\"bl_toggle\" href=\"#\"><img ondragstart=\"return false;\" class=\"toggle\""
        + " alt=\""+ (isOpen ? "Collapse " : "Expand ") + folder.name() +"\""
        + " src=\"" 
        + (isOpen ? this.TOGGLE_OPEN_ICO : this.TOGGLE_CLOSE_ICO) + "\" /></a>");
   
    if(this._showCheckboxes) this._appendCheck(b, folder.folder_id(), "2");   
    
    this.formatter.format(b, folder, this);   
        
    b.append("</div>");
 
    if(isOpen) {
      b.append("<ul class=\"treeList folderList\" id=\""+ this.baseId  +"u"+ folder.id() +"\">");

      for(var i = 0; i < folder.kids.length; i++) {
        if(folder.kids[i].isSub()) {
          this._renderSub(folder.kids[i], b);
        } else {
          throw new Error("Invalid node type for folder: " + folder.kids[i].type());
        }
      }
      b.append("</ul>");
    } 
    b.append("</li>");
    dojo.profiler.end("TreeView::renderFolder");
  },
  
  _renderSub: function(sub, b, moreClasses) {
    dojo.profiler.start("TreeView::renderSub");
    if(moreClasses == null || moreClasses == "") moreClasses = "";
    else moreClasses = " " + moreClasses;

    if(this.showOnlyUnread && !(sub.hasUnread() || sub.hasKept() || this.model.isSelected(sub))) {
      dojo.profiler.end("TreeView::renderSub");
      return;
    }
    b.append("<li class=\"sub dragHandle" + moreClasses
	    + "\" style=\"background-image: url(" + (sub.favicon() == null ? this.SUB_DEFAULT_ICO : sub.favicon()) + "); background-repeat: no-repeat;"
	    + "\" id=\"" + this.baseId + "l" + sub.id() 
	    + "\" bl_id=\""+ sub.id()  
	    + "\" bl_sub=\"" + sub.sub_id()  
	    + "\" bl_site=\"" + sub.site_id() 
	    + "\">");
    this.formatter.format(b, sub, this);
    b.append("</li>");
    dojo.profiler.end("TreeView::renderSub");
  },

  _appendCheck: function(b, name, value, checked) {
    b.append("<input type=\"checkbox\" name=\""+name+"\" value=\""+value+"\" " + (checked ? "checked=\"checked\"" : "") + " />");
  },

  _findId: function(node) {
    var id = node.id;
    if(id != null) return id;
    var n = dojo.dom.getAncestors(node, function(n) { n.id != null; }, true);
    return n.id;
  },

  _hookupEvents: function(parent) {
    dojo.profiler.start("TreeView::hookupEvents");
 
    parent = parent || this.domNode;    

    var isRootList = bl.alg.hasTagAndClass("ul", "rootList");
    var isFolderList = bl.alg.hasTagAndClass("ul", "folderList");
    var isFolderTarget = bl.alg.hasTagAndClass("div", "bl_folder_header");
    var isRootTarget = bl.alg.hasTagAndClass("div", "bl_root_header");
    var isFolderDragSource = bl.alg.hasTagAndAttribute("li", "bl_folder");
    var isSubDragSource = bl.alg.hasTagAndAttribute("li", "bl_site");
    var isToggle = bl.alg.hasTagAndClass("a", "bl_toggle");
    var _this = this;
    var tryThese = [];
    if(this.dragAndDrop) {
      tryThese = [
        function(n) { 
          if(isRootList(n)) {
            var dt = new bl.tree.TreeDropTarget(n, ["folder","sub"], 0, _this);
            var id = _this._findId(n);
            _this._dropTargets[id] = dt; 
          }
        },
        function(n) { 
          if(isRootTarget(n)) {
            var dt = new bl.tree.FolderDropTarget(n, ["folder", "sub"], 2, _this); 
            var id = _this._findId(n);
            _this._dropTargets[id] = dt;
          }
        },
        function(n) { 
          if(isFolderList(n)) {
            var dt = new bl.tree.TreeDropTarget(n, ["sub"], 1, _this); 
            var id = _this._findId(n);
            _this._dropTargets[id] = dt;
          }
        },
        function(n) { 
          if(isFolderTarget(n)) {
            var dt = new bl.tree.FolderDropTarget(n, ["sub"], 2, _this); 
            var id = _this._findId(n);
            _this._dropTargets[id] = dt;
          }
        },
        function(n) {
          //console.log("is folder drag source?");
          if(isFolderDragSource(n)) {
            //console.log("found tree drag source %n", n);
            var ds = new bl.tree.DragSource(n, "folder", null, _this);
            var id = _this._findId(n);
            _this._dragSources[id] = ds;
          }
        },
        function(n) { 
          //console.log("is sub drag source?");
          if(isSubDragSource(n)) {
            //console.log("found tree drag source %n", n);
            var ds = new bl.tree.DragSource(n, "sub", null, _this);
            var id = _this._findId(n);
            _this._dragSources[id] = ds; 
          }
        }];
    }
    tryThese.push(
      function(n) { 
        if(isToggle(n)) { dojo.event.connect(n, "onclick", _this, "_toggleFolderClickHandler"); } 
      });
        
    
    var nodes = [parent].concat(dojo.html.getElementsByTagNames(parent, "ul", "div", "li", "a"));
    
    //console.log("trying %o", tryThese); 
    for(var i = 0; i < nodes.length; i++) {
      for(var j = 0; j < tryThese.length; j++) {
        tryThese[j](nodes[i]);
      }
    }

    dojo.profiler.end("TreeView::hookupEvents");
  },

  _unhookDropTarget: function(node) {
    var id = this._findId(node);
    var dt = this._dropTargets[id];
    if(!dt) {
      return;
    } else {
      bl.dnd.manager.unregisterTarget(dt);
      delete this._dropTargets[id];
    }
  },

  _unhookDragSource: function(node) {
    var id = this._findId(node);
    var ds = this._dragSources[id];
    if(!ds) {
      return;
    }
    else {
      bl.dnd.manager.unregisterSource(ds);
      delete this._dragSources[id];
    }
  },

  _unhookEvents: function(parent) {
    dojo.profiler.start("TreeView::unhookEvents");
    parent = parent || this.domNode;

    var isRootList = bl.alg.hasTagAndClass("ul", "rootList");
    var isFolderList = bl.alg.hasTagAndClass("ul", "folderList");
    var isFolderTarget = bl.alg.hasTagAndClass("div", "bl_folder_header");
    var isRootTarget = bl.alg.hasTagAndClass("div", "bl_root_header");
    var isFolderDragSource = bl.alg.hasTagAndAttribute("li", "bl_folder");
    var isSubDragSource = bl.alg.hasTagAndAttribute("li", "bl_site");
    var isToggle = bl.alg.hasTagAndClass("a", "bl_toggle");
    var _this = this;
    var tryThese = [];
    if(this.dragAndDrop) {
      tryThese = [
        function(n) { 
          if(isRootList(n) || isRootTarget(n) || isFolderList(n) || isFolderTarget(n)) {
            _this._unhookDropTarget(n);
          }
        },
        function(n) { if(isFolderDragSource(n)) _this._unhookDragSource(n); },
        function(n) { if(isSubDragSource(n)) _this._unhookDragSource(n); }];
    }
    tryThese.push(function(n) { if(isToggle(n)) dojo.event.disconnect(n, "onclick", _this, "_toggleFolderClickHandler"); });
    
    var nodes = [parent].concat(dojo.html.getElementsByTagNames(parent, "ul", "div", "li", "a"));
    
    for(var i = 0; i < nodes.length; i++) {
      for(var j = 0; j < tryThese.length; j++) {
        tryThese[j](nodes[i]);
      }
    }
    
    dojo.profiler.end("TreeView::unhookEvents");
  },

  _partialRender: function(pid) {
    dojo.profiler.start("TreeView::partialRender");
    var what = this.model.root.get(pid);
    var tn = this.byTreeId("l" + pid);
    switch(what.type()) {
     case what.TYPES.ROOT:
       this.render();
       break;
     case what.TYPES.FOLDER:
       var b = new dojo.string.Builder();
       var _this = this;
       b.append("<ul class=\"treeList folderList\" bl_id=\""+ what.id() +"\">");
       dojo.lang.forEach(what.kids, function(k) { _this._renderSub(k, b); });
       b.append("</ul>");
       var subs = dojo.html.createNodesFromText(b.toString())[0];
       tn.appendChild(subs);
       this._hookupEvents(subs);
       break;
     default: 
       throw new Error("Don't know how to handle type: " + what.type()); 
    }
    dojo.profiler.end("TreeView::partialRender");
    dojo.profiler.dump(true);
  },

  _select: function(ids) {
    var _this = this;

    if(!ids || ids.length == 0) {
      if(this.selected.length > 0) {
        dojo.lang.forEach(this.selected, function(n) {
          var node = _this.byTreeId("l" + n);
          if(node) dojo.html.removeClass(node, "selected");
        });
        this.selected = [];
      }
      return;
    }

    var incoming = dojo.lang.map(ids, function(id) { return id.id(); });
    incoming.sort();
    var current = this.selected;
    current.sort();

    var removal = [];
    var newguys = [];
    var i = 0;
    var j = 0;
    while(i < current.length && j < incoming.length) {
      var cur = current[i];
      var inc = incoming[j];
      if(cur == inc) {
        i++; j++;
      } else if(cur > inc) {
        newguys.push(inc);
        j++;
      } else if(inc > cur) {
        removal.push(cur);
        i++;
      }
    }

    while(i < current.length) {
      removal.push(current[i]);
      i++;
    }

    while(j < incoming.length) {
      newguys.push(incoming[j]);
      j++;
    }

    dojo.lang.forEach(removal, function(n){
      var node = _this.byTreeId("l" + n);
      if(node) dojo.html.removeClass(node, "selected");
      var i = dojo.lang.find(_this.selected, n, true);
      _this.selected.splice(i, 1);
    });

    dojo.lang.forEach(this.selected, function(n) {
      var node = _this.byTreeId("l" + n);
      if(node) dojo.html.addClass(node, "selected");
    });

    dojo.lang.forEach(newguys, function(n) {
      var node = _this.byTreeId("l" + n);
      var mn = _this.model.get(n);
      if(!node) {
        if(mn && mn.isSub() && mn.parentNode.isFolder() && !_this.isFolderOpen(mn.parentNode.id())) {
          _this._toggleFolder(_this.byTreeId("l" + mn.parentNode.id()));
          node = _this.byTreeId("l" + n);
        }
      }
      if(mn.isSub() && mn.parentNode.isFolder()) _this.openFolder(mn.parentNode.id());
      dojo.html.addClass(node, "selected");
      _this.selected.push(n);
    });
    
  },

  isFolderOpen: function(fid) {
    var n = this.byTreeId("l" + fid);
    var ul = dojo.dom.getFirstChild(n, bl.alg.hasTag("ul"));
    return ul && ul.style.display != "none";
  },

  _toggleFolderClickHandler: function(evt) {
    dojo.event.browser.stopEvent(evt);
    var ctr = dojo.dom.getFirstAncestorByTag(evt.target, "LI");
    this._toggleFolder(ctr);
  },

  toggleFolder: function(fid) {
    this._toggleFolder(this.byTreeId("l" + fid));
  },

  closeFolder: function(fid) { if(this.isFolderOpen(fid)) this.toggleFolder(fid); },
  openFolder: function(fid) { if(!this.isFolderOpen(fid)) this.toggleFolder(fid); },
  
  _toggleFolder: function(fnode) {
    dojo.profiler.start("TreeView::toggleFolder");
    
    var container = fnode;
    var header = dojo.dom.getFirstChild(container, bl.alg.hasTagAndClass("div", "bl_folder_header"));
    var fname = dojo.dom.getFirstChild(header, bl.alg.hasTagAndClass("a", "bl_folderName"));
    fname = fname.getAttribute("bl_name");
    var toggle = dojo.dom.getFirstChild(header, bl.alg.hasTagAndClass("a", "bl_toggle"));
    var img = dojo.dom.getFirstChild(toggle, bl.alg.hasTagAndClass("img", "toggle"));

    var id = dojo.html.getAttribute(container, "bl_folder");

    // now toggle the ul inside the li
    var ul = dojo.dom.getFirstChild(container, bl.alg.hasTag("ul"));
        
    if(ul == null || ul.style.display == "none") {
      this.viewState.folderOpen(id, true);
      if(ul == null) {
	      this._partialRender(dojo.html.getAttribute(container, "bl_id"));
        ul = dojo.dom.getFirstChild(container, bl.alg.hasTag("ul"));
      }
      ul.style.display = "";
      img.src = this.TOGGLE_OPEN_ICO;
      img.setAttribute("alt", "Collapse " + fname);
      dojo.html.replaceClass(container, "open", "closed");
    } else {
      ul.style.display = "none";
      img.src = this.TOGGLE_CLOSE_ICO;
      img.setAttribute("alt", "Expand " + fname);
      dojo.html.replaceClass(container, "closed", "open");
      this.viewState.folderOpen(id, false);
    }
    dojo.profiler.end("TreeView::toggleFolder");
    dojo.profiler.dump();
  },

  _handleNodePick: function(evt) {
    if(!bl.event.isLeftMouseClick(evt)) return;
    
    var t = dojo.dom.getFirstAncestorByTag(evt.target, "A");
    if(!t) { return; }
    evt.preventDefault();
    var bid = $A(t, "bl_id");
    this.onPick(bid, evt);
  },
   
  _handleNodeClick: function(evt) {
    if(!bl.event.isLeftMouseClick(evt)) return;
    
    var t = dojo.dom.getFirstAncestorByTag(evt.target, "A");
    if(!t) { return; }
    var bid = $A(t, "bl_id");
    if(!bid) return;
    dojo.event.browser.stopEvent(evt);
    this.onClick(bid, evt);
  },

  _handleNodeDblClick: function(evt) {
    if(!bl.event.isLeftMouseClick(evt)) return;
    var t = dojo.dom.getFirstAncestorByTag(evt.target, "A");
    if(!t) { return;}
    var bid = $A(t, "bl_id");
    if(!bid) return;
    dojo.event.browser.stopEvent(evt);
    this.onDblClick(bid, evt);
  },

  _handleShowToggle: function(evt) {
    dojo.event.browser.stopEvent(evt);
    this.showOnlyUnread = !this.showOnlyUnread;
    this.render();
  },

  suspendUpdates: function() {
    this._suspendUpdates = true;
    this._sawUpdates = false;
  },

  resumeUpdates: function() {
    this._suspendUpdates = false;
    if(this._sawUpdates) this.render();
    this._sawUpdates = false;
  },

  _handleModelChange: function(who, what) {
    if(this._suspendUpdates) { this._sawUpdates = true; return; }
    switch(what) {
      case "unread":
        this._updateUnread(who);
        break;
      case "kept":
        this._updateKept(who);
        break;
      default:
        if(this._renderTimer != null) clearTimeout(this._renderTimer);
        this._renderTimer = dojo.lang.setTimeout(this, "render", 50);
        break;
    }
  },

  _updateUnread: function(who) {
    var self = this;
    dojo.lang.forEach(who, function(n) {
      if(n.isRoot()) return;
      var ln = self.byTreeId("l" + n.id());
      if(!ln) return; // not rendered yet
      var tn = self.byTreeId("un" + n.id());
      if(!tn) return; // not showing counts
      tn.innerHTML = "(" + n.unread() + ")";
      tn.title = n.unread() + " unread items";
      var an = self.byTreeId("a" + n.id());
      an.title = n.unread() + " unread items, " + n.kept() + " items kept as new";
      if(n.unread() == 0) {
        if(n.kept() == 0) tn.style.display = "none";
        dojo.html.removeClass(an, "unread");
        dojo.html.replaceClass(tn, "ur", "urb");
      } else {
        tn.style.display = "";
        dojo.html.addClass(an, "unread");
        dojo.html.replaceClass(tn, "urb", "ur");
        var tid = tn.id;
        if(self._activeAnims[tid] == null) {
          var anim = dojo.lfx.highlight(tn, "#ffff00", 500, dojo.lfx.easeOut, function() { delete self._activeAnims[tid]; });
          self._activeAnims[tid] = anim;
          anim.play();
        }
     }
    });
  },

  _updateKept: function(who) {
    var self = this;
    dojo.lang.forEach(who, function(n) {
      if(n.isRoot()) return;
      var id = n.id();
      var ln = self.byTreeId("l" + id);
      if(!ln) return; // not rendered yet
      var tn = self.byTreeId("kp" + id);
      var un = self.byTreeId("un" + id);
      if(!(tn && un)) return; // not showing counts
      var an = self.byTreeId("a" + id);
      tn.innerHTML = "(" + n.kept() + ")";
      tn.title = n.kept() + " items kept as new";
      an.title = n.unread() + " unread items, " + n.kept() + " items kept as new";
      if(n.kept() > 0) {
        tn.style.display = "";
        un.style.display = "";
        dojo.html.addClass(an, "kept");  
        var tid = tn.id;
        if(self._activeAnims[tid] == null) {
          var anim = dojo.lfx.highlight(tn, "#ffff00", 500, dojo.lfx.easeOut, function() { delete self._activeAnims[tid]; });
          self._activeAnims[tid] = anim;
          anim.play();
        }

      } else {
        if(n.unread() == 0) {
          tn.style.display = "none";
          un.style.display = "none";
        }
        dojo.html.removeClass(an, "kept");
      }
    });
  },

  _handleModelLoaded: function(evt) {
    this.hideSpinner();
    this._handleModelChange();
  },

  _handleSelectionChange: function() {
    if(this.model.selected != null)
      this._select(this.model.selected);    
  }
});

bl.tree.TreeView.preventDefault = function(e) {
    if(!e) e = window.event;
    if(!e.preventDefault) e.returnValue = false;
    else e.preventDefault();
}


//bl.dnd.init();
