module('apps.ObjectExplorer').requires().toRun(function() {

/**
 * @class TreeNode: A node
 *
 * @example
 * node = new TreeNode(
 *   text: "Foo"
 * );
 * node.appendChild(
 *   new TreeNode({
 *     text: "Bar",
 *     leaf: true
 *   })
 * )
 */
Object.subclass('TreeNode', {
  initialize: function(opts) {
    if(!opts) opts = {};

    var expanded = opts.expanded;
    delete opts.expanded;

    // Apply some defaults
    Object.extend(this, {
      depth: 0,
      leaf: false,
      items: []
    });
    Object.extend(this, opts);

    if(expanded) this.expand();

    return this;
  },

  /**
   *
   * @param rec True if all expanded subnodes should be collapsed, too
   */
  collapse: function(rec){
    this.onBeforeCollapse(this);
    // Expand sub nodes
    if(rec === true){
      this.items.each(function(item){
        if(item.isExpanded()) item.collapse(rec);
      });
    }
    this.expanded = false;
    this.onCollapse(this);
  },

  /**
   * Collapses all children, except this node.
   *
   */
  collapseAll : function() {
	  this.items.each(function(item){
	        if(item.isExpanded()) item.collapse(true);
	      });
  },

  expandAll : function() {
    if(this.isExpandable() && !this.isExpanded()) this.expand();
    this.items.invoke("expandAll");
  },

  expand: function(){
    this.onBeforeExpand(this);
    this.expanded = true;
    this.onExpand(this);
  },

  offset: function(){
    var offset = this.position;

    var calcOffset = function(items, maxIndex){
      if(maxIndex == undefined) maxIndex = items.length;
      for(var i = 0; i < maxIndex; i++){
        if(items[i].isExpanded()){
          offset += items[i].items.length;
          calcOffset(items[i].items);
        }
      }
    }
    calcOffset(this.parent.items, this.position);

    return offset;
  },

  getText: function() {
    return this.text;
  },

  setText: function(value) {
    var oldText = this.text;
    this.text = value;
    if(value !== oldText) this.onNodePropertyChanged(this, 'text', value, oldText);
  },

  setParent: function(parent) {
    this.parent = parent;
    this.position = this.parent.items.indexOf(this);
    this._updateDepth();
  },

  _updateDepth: function(){
    var oldDepth = this.depth;
    this.depth = this.parent ? (this.parent.depth + 1) : 0; // root nodes have depth 0
    if(oldDepth != this.depth && this.items){
      this.items.each(function(item){
        item._updateDepth();
      });
    }
  },

  isLeaf: function() {
    return this.leaf;
  },

  setLeaf: function(isLeaf) {
    var oldValue = this.leaf;
    this.leaf = isLeaf;
    if(isLeaf !== oldValue) this.onNodePropertyChanged(this, 'leaf', isLeaf, oldValue);
  },

  isExpanded: function() {
    return this.expanded;
  },

  isExpandable: function(){
    return !this.isLeaf();
  },

  // Called before this node is expanded
  onBeforeExpand: function(treeNode){
    // Overwrite if needed
  },

  // Called when this node is expanded
  onExpand: function(treeNode){
    // Overwrite if needed
  },

  // Called before this node is collapsed
  onBeforeCollapse: function(treeNode){
    // Overwrite if needed
  },

  // Called when this node is collapsed
  onCollapse: function(treeNode){
    // Overwrite if needed
  },

  onNodesAdded: function(treeNode, addedNodes){
    // Overwrite if needed
  },

  onNodesRemoved: function(treeNode, removedNodes){
    // Overwrite if needed
  },

  onTextChanged: function(treeNode, newValue, oldValue){
    // Overwrite if needed
  },

  onNodePropertyChanged: function(treeNode, property, newValue, oldValue){
    // Overwrite if needed
  },

  // A node a or an array of nodes (or node configs)
  // return created node or array of nodes
  appendChild: function(nodes){
    // Transform input into array
    var single = !Object.isArray(nodes);
    if(single) nodes = [nodes];

    nodes = nodes.collect(function(node){
      // Transform input into TreeNode
      if(node.constructor === Object) node = new TreeNode(node);

      this.items.push(node);
      node.setParent(this);

      return node;
    }, this);

    this.onNodesAdded(this, nodes);

    return (single ? nodes[0] : nodes);
  },

  removeChild: function(nodes){
    if(!Object.isArray(nodes)) nodes = [nodes];

    nodes.each(function(node){
      node.collapse(true);
      this.items = this.items.without(node);
    }, this);

    this.onNodesRemoved(this, nodes);

    nodes.each(function(node){delete node});
  }
});

/**
 * @class ObjectExplorer: The main application class
 */

/**
 * The object explorer class.
 *
 * @param opts.objectToExplore The object to explore (default: Global)
 */
Object.subclass('ObjectExplorer', {

  initialize: function(cfg) {
    if(!cfg) cfg = {};

    // Pointers to main visual structures
    this.panel = null;        // The main panel that contains three other panes
    this.rootNode = null;

    //constants
    ObjectExplorer.HIDE_FUNCTIONS = 0;
    ObjectExplorer.MORPH_VIEW = 1;

    //initialize default config options
    this.config = [];
    this.config[ObjectExplorer.HIDE_FUNCTIONS] = true;
    this.config[ObjectExplorer.MORPH_VIEW] = false;

    this.objectToExplore = cfg.objectToExplore || Global;
    this.isShowingSource = false;
    return this;
  },

  openIn: function(world, location, extent) {
    var window = new WindowMorph(this.buildView(extent || pt(400, 600)), "Object Explorer (" + Object.inspect(this.objectToExplore).truncate(30) + ")");

    world.addMorphAt(window, location);

    // Make sure scroll bars are initialized correctly
    this.panel.adjustForNewBounds();
    return this;
  },

  buildView: function(extent) {


	var panel = this.panel = PanelMorph.makePanedPanel(extent, [
      [ 'treePanel',
        function(initialBounds) {
          this.rootNode = new ObjectExplorer.TreeNode({
            expanded: true,
            objectToExplore: this.objectToExplore,
            objectExplorer: this
          });

          this.treeMorph = new TreeMorph(initialBounds, this.rootNode)
          this.treeMorph.onSelectionChanged = function(treeMorph, treeMorphNode) {
            this.showFunctionSource(treeMorph, treeMorphNode);
          }.bind(this);

          var scrollPane = new ScrollPane(this.treeMorph, initialBounds);
          scrollPane.connectModel({model: this, getMenu: "getSideMenu"});
          return scrollPane;
        }.bind(this),
        new Rectangle(0.0, 0.00, 1.0, 0.8)
      ], [
        'textPane',
        this.createConsoleView.bind(this),
        new Rectangle(0.0, 0.8, 1.0, 0.2)
      ]
    ]);

    // Collapse root node on remove. This is a small hack, how can you register on an remove event?
    panel.remove = panel.remove.wrap(function(proceed){
		var ret = undefined;
		if( typeof proceed == "function" ) {
			ret = proceed();
		}
      // Unregister all observed objects by collapsing root node
      if(ret) this.rootNode.collapse(true);
      return ret;
    }.bind(this));

    // Initialize console functionality
    panel.textPane.innerMorph().connectModel({
      model: {contextForEval: function(){return this.objectToExplore}.bind(this)},
      getDoitContext: 'contextForEval'
    });

    // Update panel if content of tree changes (e.g. scroll bars)
    this.getTreePanel().onContentChanged = this.panel.adjustForNewBounds.bind(this.panel);

    return panel;
  },

  createConsoleView: function(initialBounds, defaultText) {
	  var console = newTextPane(initialBounds, defaultText);
	  console.innerMorph().subMenuItems = function(evt) {
			var items = [];
			if (this.isShowingSource)
				items.push(["Save function source" , this.saveFunctionSource.curry(console.innerMorph()).bind(this)]);

			items.unshift(["Text functions" , console.innerMorph().editMenuItems(evt)]);
			return items;
	  }.bind(this);

	  return console;
  },

  saveFunctionSource : function(textMorph) {
	  //use doit functionality
	  var s = textMorph.textString;
	  var treeNode = this.treeMorph.getSelectedNode();
	  s = this.recursiveGetTreeNodePrefix(treeNode, "") + "=" + s;

	  textMorph.tryParseAndBoundEval(s, 0);
  },

  recursiveGetTreeNodePrefix : function(treeNode, prefix) {
	  if (treeNode.parent !== undefined) {
		  prefix = this.recursiveGetTreeNodePrefix(treeNode.parent, prefix)+ "." + treeNode.treeNode.propertyName; //recursive build prefix string
	  }
	  else {
		  prefix = "this" + prefix; //abort condition
	  }

	  return prefix;
  },

  /** Returns list of items used to display the menu */
  getSideMenu : function() {
    var items = [];
    items.push(["Collapse all", this.collapseAll.bind(this)]);
    if (this.config[ObjectExplorer.MORPH_VIEW])
    	items.push(["Expand all", this.expandAll.bind(this)]);
    items.push([this.config[ObjectExplorer.MORPH_VIEW] ? "Disable morph browsing" : "Enable morph browsing", this.toggleConfig.curry(ObjectExplorer.MORPH_VIEW, true).bind(this)]);
    if (!this.config[ObjectExplorer.MORPH_VIEW])
    	items.push([this.config[ObjectExplorer.HIDE_FUNCTIONS] ? "Show Functions" : "Hide Functions", this.toggleConfig.curry(ObjectExplorer.HIDE_FUNCTIONS, true).bind(this)]);

    return items;
  },

  showFunctionSource : function(treeMorph, treeMorphNode) {
    //check if it is a function
    if (typeof treeMorphNode.treeNode.objectToExplore === "function" ) {
      this.panel.textPane.innerMorph().setTextString(treeMorphNode.treeNode.objectToExplore.toString());
      this.isShowingSource = true;
    } else {
      this.panel.textPane.innerMorph().setTextString("");
      this.isShowingSource = false;
    }
  },

  getTreePanel: function(value) {
    return this.panel.treePanel.innerMorph();
  },

  collapseAll: function() {
    this.rootNode.collapseAll();
  },

  expandAll: function() {
    this.rootNode.expandAll();
  },

  getConfig: function() {
	  return this.config;
  },

  /**
   *
   * @param attr {String}
   * @param [update=false] {Boolean} True if view needs update for change and should be updated
   */
  toggleConfig: function(attr, update){
    var toggle = function(){ this.config[attr] = !this.config[attr]; }.bind(this);

    if (attr === ObjectExplorer.MORPH_VIEW && !this.config[ObjectExplorer.MORPH_VIEW]) {
    	//disable functions for morph view
    	this.config[ObjectExplorer.HIDE_FUNCTIONS] = true;
    }

    if(update){
      this.rootNode.update(toggle);
      this.panel.treePanel.scrollToTop(); //scroll to beginning
    } else {
      toggle();
    }
  }
});

/**
 * A tree node representing an objectToExplore.
 *
 * @param opts.objectToExplore
 */
TreeNode.subclass('ObjectExplorer.TreeNode', {

  initialize: function($super, opts){
    this.objectExplorer = opts.objectExplorer;
    delete opts.objectExplorer;
    this.parent = opts.parent;
    delete opts.parent;
	this.propertyName = opts.propertyName;
    delete opts.propertyName;
    this.setObjectToExplore(opts.objectToExplore);
    delete opts.objectToExplore;
    
    opts.leaf = this.leaf;

    $super(opts);

    this.alreadyProcessed = false;
  },

  /**
   * Sets whether the node should be a leaf regarding the specified node's object.
   */
  updateLeaf: function(){
    // Returns true if at least one property exist which should be displayed
    var haveItems = function(){
      for(var prop in this._getObjectToExploreContext()){
        if(!this.shouldBeFiltered(prop)) return true;;
      }
      return false;
    }.bind(this);

    this.setLeaf((typeof this._getObjectToExploreContext() !== 'object') || !haveItems());
  },

  updateText: function(){
    var text = '';
    var config = this.getObjectExplorer().getConfig();
    if(!config[ObjectExplorer.MORPH_VIEW]) text += this.propertyName + " : ";
    text += this.getObjectName(this.objectToExplore);
    this.setText(text);
  },

  _getObjectToExploreContext: function(){
	var config = this.getObjectExplorer().getConfig();
    return config[ObjectExplorer.MORPH_VIEW] ? this.objectToExplore.submorphs : this.objectToExplore;
  },

  getObjectExplorer: function(){
    return this.objectExplorer ? this.objectExplorer : this.parent.getObjectExplorer();
  },

  _createItems: function(){
    var items = [];

    for(var prop in this._getObjectToExploreContext()){
      if(this.shouldBeFiltered(prop)) continue;

      items.push(new ObjectExplorer.TreeNode({
        propertyName: prop,
        objectToExplore: this._getObjectToExploreContext()[prop],
        expanded : false,
        parent : this
      }));
    }

    items.sort(ObjectExplorer.TreeNode.compare);

    return items;
  },

  shouldBeFiltered: function(prop){
    var config = this.getObjectExplorer().getConfig();
    if(config[ObjectExplorer.HIDE_FUNCTIONS] && typeof this._getObjectToExploreContext()[prop] === "function") return true;
    // Hide internal functions, e.g. used by object observer. prop can be a number (e.g. array index), to cast it to string before.
    if(prop.toString().match(/__.*__/)) return true;
    return false;
  },

  /**
   * If somebody expands the node, add child tree nodes lazily so that not the whole tree of to be build up from
   * the beginning.
   */
  onBeforeExpand: function(){
    if(this.alreadyProcessed || this.isLeaf()) return;

    // When object explorer explores himself, a warning is displayed instead of registering for notifications
    if(this.getObjectExplorer().treeMorph === this.objectToExplore){
      alert("Live updates won't be available for "+Object.inspect(this.objectToExplore)+" because this would lead to infinite recursion. Please open another object explorer instance for exploring this object explorer.");
    } else {
      ObjectObserver.register(this._getObjectToExploreContext(), this.objectChangedCallback = ObjectExplorer.TreeNode.objectChangedCallback.bind(this));
    }

    this.appendChild(this._createItems());

    this.alreadyProcessed = true;
  },

  onBeforeCollapse: function(){
    if(this.objectChangedCallback){
      ObjectObserver.unregister(this._getObjectToExploreContext(), this.objectChangedCallback);
      this.objectChangedCallback = null;
    }
  },

  setObjectToExplore: function(obj){
    this.update(function(){
      this.objectToExplore = obj;
      this.updateText();
      this.updateLeaf();
    }.bind(this));
  },

  /**
   * Guesses an object's description by considerung type and/ or Object#inspect return value.
   * @return {String}
   */
  getObjectName : function(object) {
    switch(typeof object) {
      case 'number':
      case 'boolean':
        return object.toString();
      case 'string':
        return '"' + object + '"'; // Return the string enclosed in quotes
      case 'function':
        return 'function()'
      case 'object':
        if(object == null) return "null";

        // Try to inspect object
        try {
          return Object.inspect(object);//(object.inspect ? object.inspect() : object.toString());
        } catch (err) {
          // Just try another way to get a string representation ...
          console.warn("Error while expecting object: " + err);
        }

        return object.constructor.name;
      default: //e.g. case 'undefined'
        return ''+object;
    }

    return "";
  },

  /**
   * Collapses this node, deletes all children, calls fn and then expand if it has been expanded before.
   * fn can be very helpful for the cases when either objectToExplorContext or objectToExplore itself changes, whereby
   * collapse should operate on the old value and expand on the new.
   */
  update : function(fn) {
	  var wasExpanded = this.isExpanded();
	  if (wasExpanded) this.collapse(true);

    if(this.items){
      this.items.each(function(item){
        delete item;
      });

      this.items = [];
      this.alreadyProcessed = false;
    }

    fn();

	  if (wasExpanded) this.expand();
  }
});

/**
 * Some static properties.
 */
Object.extend(ObjectExplorer.TreeNode, {
  /**
   * Compare function, can be used for sorting a collection of ObjectExplorer.TreeNode
   * @param a {ObjectExplorer.TreeNode}
   * @param b {ObjectExplorer.TreeNode}
   * @return {Number}
   */
  compare: function(a, b){
    // Sort by (non-)function
    if(typeof a.objectToExplore === "function" && typeof b.objectToExplore !== "function") return 1;
    if(typeof a.objectToExplore !== "function" && typeof b.objectToExplore === "function") return -1;

    var anum, bnum;
    if(isNaN(anum = parseInt(a.text)) || isNaN(bnum = parseInt(b.text))){
      // Sort alphabetically
      var atext = a.text.toLowerCase();
      var btext = b.text.toLowerCase();
      return( atext ==  btext) ? 0 : ( atext > btext ) ? 1 : -1;
    } else {
      // Sort numerically, e.g. for array indeces
      return( anum == bnum) ? 0 : ( anum > bnum ) ? 1 : -1;
    }
  },
  /**
   * This callback is used for being passed to ObjectObserver#register.
   * Don't forget to bind it a ObjectExplorer.TreeNode instance!
   * @param object
   * @param methodName
   * @param value The new value.
   * @param changeKey {added|deleted|changed}
   */
  objectChangedCallback: function(object, methodName, value, changeKey){
    if(object !== this._getObjectToExploreContext()) console.warn("Something went wrong, this shouldn't happen!");

    var node;
    if(changeKey !== 'added'){
      node = this.items.detect(function(item){
        return item.propertyName == methodName;
      }, this);
      
      if(!node && !this.shouldBeFiltered(methodName)) {
        console.warn("Node for " + Class.className(object.constructor) + "#" + methodName + " not found, there is something wrong!");
        return;
      }
    }

    switch(changeKey) {
      case 'added':
        if(this.shouldBeFiltered(methodName)) break;
        this.appendChild(new ObjectExplorer.TreeNode({
          propertyName: methodName,
          objectToExplore: value,
          expanded : false
        }));
        break;
      case 'deleted':
        this.removeChild(node);
        break;
      case 'changed':
        node.setObjectToExplore(value);
        break;
      default:
        console.warn("Unkown changeKey " + changeKey);
    }
  },
});

(function(){
  var idSeed = 0;

  /**
  * Generates unique ids. If the element already has an id, it is unchanged
  * @param {Mixed} el (optional) The element to generate an id for
  * @param {String} prefix (optional) Id prefix (defaults "lively")
  * @return {String} The generated Id.
  */
  lively.id = function(prefix){
    return (prefix || 'lively') + (++idSeed);
  }
})();

/**
 * @class CaptionTextMorph: Variant of TextMorph, nothing more...
 */
TextMorph.subclass("CaptionTextMorph", {

  initialize: function($super, initialBounds, text, treeNodeMorph) {
    $super(initialBounds, text);
    this.setWrapStyle(lively.Text.WrapStyle.None);
    this.setFill(Color.gray);
    this.setBorderWidth(0); // Use unbordered text
    this.suppressHandles = true; // No handles!
    this.focusHaloBorderWidth = 0; // No halo around text!
    this.treeNodeMorph = treeNodeMorph;
	this.applyStyle({textColor: Color.black, fontSize: 12});
    this.openForDragAndDrop = false;
  },

  // Prevent the object from being moved accidentally
  okToBeGrabbedBy: function(event) {
      return null;
  },
  
  //overwrite to disable textselection behavior
  onMouseDown: function() {
	  this.treeNodeMorph.wasClicked();
  }

});

/**
 * @class CaptionImageMorph: Variant of ImageMorph that automatically
 * loads the application-specific icons during instantiation
 */
ImageMorph.subclass("CaptionImageMorph", {
  initialize: function($super, initialBounds, url) {
    $super(initialBounds, url);
    this.suppressHandles = true; // No handles!

    return this;
  },

  // Prevent the object from being moved accidentally
  okToBeGrabbedBy: function(event) {
      return null;
  },

  // Needed so that onMouseUp is called, wtf?
  handlesMouseDown: function(event) {
    return true;
  },

  onMouseUp: function(event) {
    // Override if needed.
  }
});

/**
 * @class TreeNodeMorph: Displays an individual row (folder or note)
 * in the tree morph  Each row contains a CaptionImageMorph
 * and a CaptionTextMorph.
 */
BoxMorph.subclass("TreeNodeMorph", {

  // Constants for item placement
  TOPPADDING: 4,
  INDENTFACTOR: 16,
  ITEMWIDTH: 600,
  ITEMHEIGHT: 20,

  // Constants for icon placement
  ICONLEFTPADDING: 3,
  ICONTOPPADDING: 0,
  // the icon has a width of 16, but somehow that results in a sqeezed image in Chrome.
  ICONWIDTH: 17,
  ICONHEIGHT: 18,

  // Constants for text placement
  TEXTLEFTPADDING: 16,
  TEXTTOPPADDING: 0,
  TEXTWIDTH: 500,
  TEXTHEIGHT: 20,

  ICONEXPANDED: new URL(module('projects.ObjectExplorer.TreeMorph').uri()).withFilename('media/expanded.png').relativePathFrom(URL.source),
  ICONCOLLAPSED: new URL(module('projects.ObjectExplorer.TreeMorph').uri()).withFilename('media/collapsed.png').relativePathFrom(URL.source),

  /**
   *
   * @param opts.treeNode The TreeNode which is displayed by this morph
   * @param opts.parent The parent TreeNodeMorph which represent the parent of opts.treeNode
   */
  initialize: function($super, opts) {
    $super(new Rectangle(
      (opts.treeNode.depth-1) * this.INDENTFACTOR,  // x
      0, // y, calculated in addNodeMorph
      this.ITEMWIDTH, // width
      this.ITEMHEIGHT) // height
    );

    Object.extend(this, opts);

    this.isSelected = false;
    this.setFill(Color.gray);
    this.setBorderColor(Color.gray);
    this.suppressHandles = true; // No handles!
    this.ignoreEvents(); // Will not respond nor get focus!

    this._createSubmorphs();

    this._bindEvents();

    return this;
  },

  _bindEvents: function(){
    this.treeNode.onExpand = this.onExpand.bind(this);
    this.treeNode.onCollapse = this.onCollapse.bind(this);
    this.treeNode.onNodesAdded = this.onNodesAdded.bind(this);
    this.treeNode.onNodesRemoved = this.onNodesRemoved.bind(this);
    this.treeNode.onNodePropertyChanged = this.onNodePropertyChanged.bind(this);

    if(this.treeNode.expanded) this.onExpand(this.treeNode);
  },

  // Prevent the object from being moved accidentally
  okToBeGrabbedBy: function(event) {
      return null;
  },

  /**
   * Initialize the substructures (icon and text)
   */
  _createSubmorphs: function(){
    this.imageMorph = new CaptionImageMorph(new Rectangle(
            this.ICONLEFTPADDING, this.ICONTOPPADDING,
            this.ICONWIDTH, this.ICONHEIGHT), this._getIcon());
	this.imageMorph.setExtent(pt(this.ICONWIDTH, this.ICONHEIGHT));
    this.imageMorph.setFill(Color.gray);
    this.addMorph(this.imageMorph);

    this._updateIconVisibility();

    this.textMorph = new CaptionTextMorph(new Rectangle(
          this.TEXTLEFTPADDING, this.TEXTTOPPADDING,
          this.TEXTWIDTH, this.TEXTHEIGHT), this.treeNode.getText(), this);
    this.addMorph(this.textMorph);
  },

  _onIconMouseUp: function(){
    if (this.treeNode.isExpandable()) {
      this.treeNode.isExpanded() ? this.treeNode.collapse(true) : this.treeNode.expand();
    }
  },
  
  wasClicked : function(selected) {
	  this.owner.wasClicked(this);
  },
  
  selectIt : function(selectIt) {
	  this.textMorph.setFill(selectIt ? Color.yellow : Color.gray);
  },
  
  onNodePropertyChanged: function(treeNode, property, newValue, oldValue){
    switch(property){
      case 'text':
        this.textMorph.setTextString(newValue);
        break;
      case 'leaf':
        this._updateIconVisibility();
        break;
    }
  },

  // Called when this node is expanded
  onExpand: function(treeNode){
    this._updateIcon();

    this.owner.addNodeMorphs(this.treeNode.items.map(function(node){
      return new TreeNodeMorph({treeNode: node, parent: this});
    }, this));
  },

  // Called when this node is collapsed
  onCollapse: function(treeNode){
    this._updateIcon();

    var index = this.owner.submorphs.indexOf(this) + 1;

    var morphsToRemove = [];
    for(var i = 0; i < this.treeNode.items.length; i++) {
      morphsToRemove.push(this.owner.submorphs[index + i]);
    }

    this.owner.removeNodeMorphs(morphsToRemove);
  },

  onNodesAdded: function(treeNode, addedNodes){
    if(!treeNode.isExpanded()) return;

    this.owner.addNodeMorphs(addedNodes.map(function(node){
      return new TreeNodeMorph({treeNode: node, parent: this});
    }, this));
  },

  onNodesRemoved: function(treeNode, removedNodes){
    if(!treeNode.isExpanded()) return;

    this.owner.submorphs.each(function(morph){
      if(!removedNodes.include(morph.treeNode)) return;

      this.owner.removeNodeMorphs([morph]);
    }, this);
  },

  _updateIconVisibility: function(){
    this.imageMorph.setVisible(!this.treeNode.isLeaf());
    // Unregister listener
    this.imageMorph.onMouseUp = this.treeNode.isLeaf() ? function(){} : this._onIconMouseUp.bind(this);
  },

  _updateIcon: function(){
    this.imageMorph.loadFromURL(this._getIcon());
  },

  _getIcon: function(){
    return this.treeNode.isExpanded() ? this.ICONEXPANDED : this.ICONCOLLAPSED;
  },

  // Prevent the object from being moved accidentally
  okToBeGrabbedBy: function(event) {
    return null;
  }
});

/**
 * @class TreeMorph: Displays the contents of the entire tree
 */
Morph.subclass("TreeMorph", {
  initialize: function($super, initialBounds, rootNode) {
    $super(new lively.scene.Rectangle(initialBounds));
    this.setFill(Color.gray);

    // Add a TreeNodeMorph for root, but don't add it to the tree. This way, the tree listen still to all events
    // of the root node, as it would be a child nodes.
    this.rootNodeMorph = new TreeNodeMorph({
      treeNode: (rootNode || new TreeNode({expanded: true})),
      owner: this
    });

    this.suppressHandles = true;
    
    return this;
  },

  wasClicked : function(treeNodeMorph) {
	 if (this.selectedNode !== undefined)
		 this.selectedNode.selectIt(false); //deselect old node
  
	 treeNodeMorph.selectIt(true);
	 this.selectedNode = treeNodeMorph;

	 this.onSelectionChanged(this, this.selectedNode);
  },
  
  getSelectedNode : function() {
	  return this.selectedNode;
  },
  
  // Prevent the object from being moved accidentally
  okToBeGrabbedBy: function(event) {
    return null;
  },

  getRootNode: function(){
    return this.rootNodeMorph.treeNode;
  },

  /**
   * !!! Assumes that all inserted morphs belongs together, so the offset of the first one is taken and all other morphs
   *     are inserted behind !!!
   *
   * @param morphs
   */
  addNodeMorphs: function(morphs){
    if(!morphs || morphs.length == 0) return;

    var offset = morphs.first().treeNode.offset();

    if(morphs.first().parent){
      offset += this.indexOfSubmorph(morphs.first().parent) + 1;
    }

    // Free some space to insert subnodes
    for(var i = offset; i < this.submorphs.length; i++) {
      this.submorphs[i].translateBy(pt(0, morphs.length * TreeNodeMorph.prototype.ITEMHEIGHT));
    }

    morphs.each(function(morph){
      // Add subnodes
      this.addMorphFrontOrBack(morph, offset);

      // Correct y coordinate
      morph.setBounds(morph.bounds().withY(
        TreeNodeMorph.prototype.TOPPADDING + offset * TreeNodeMorph.prototype.ITEMHEIGHT)
      );

      offset++;
    }, this);

    this.onContentChanged();
  },

  removeNodeMorphs: function(morphs){
    if(!morphs || morphs.length == 0) return;

    var index = this.submorphs.indexOf(morphs.first());

    morphs.each(function(morph){
      this.removeMorph(morph);
    }, this);

    // Clean the freed space
    for(var i = index; i < this.submorphs.length; i++) {
      this.submorphs[i].translateBy(pt(0, -morphs.length * TreeNodeMorph.prototype.ITEMHEIGHT));
    }

    this.onContentChanged();
  },

  onContentChanged: function(){
    // Overwrite if needed to adjust layouts etc.
  },

  onSelectionChanged : function(treeMorph, treeMorphNode) {
	  //Overwrite if needed
  },

  /**
   * Implements morph insertion at a specific position
   *
   * !!! Do not call this method directly, it is invoked via Morph.prototype.addMorphFrontOrBack(m, isFront) !!!
   *
   * @override Morph.prototype.insertMorph
   * @param m The morph to be added
   * @param isFront True, false or a number
   */
  insertMorph: function(m, isFront) {
    var insertionPt = null;
    var index = 0;
    if(this.submorphs.length > 0){
      if(Object.isNumber(isFront) && isFront >= this.submorphs.length) isFront = true;

      if(Object.isNumber(isFront)){
        index = isFront;
        insertionPt = this.submorphs[isFront].rawNode;
      } else if(isFront === true){
        insertionPt = this.submorphs.last().rawNode.nextSibling;
        index = this.submorphs.length;
      } else {
        insertionPt = this.submorphs.first().rawNode;
        index = 0;
      }
    }

    this.rawNode.insertBefore(m.rawNode, insertionPt);

    // Insert into submorphs
    this.submorphs.splice(index, 0, m);

    m.owner = this;

    return m;
  }
});

/**
 * @example
    var myObject = {
      blub: function(){
        alert("Blub")
      }
    };

    var callback1 = function(){alert("Callback 1")};
    var callback2 = function(){alert("Callback 2")};

    ObjectObserver.register(myObject, callback1);
    ObjectObserver.register(myObject, callback2);

    myObject.blub() // #=> alerts "Blub", "Callback 1" and "Callback 2"

    ObjectObserver.unregister(myObject, callback2);

    myObject.blub() // #=> alerts "Blub" and "Callback 1"
 */
ObjectObserver = {
  register: function(object, callback){
    // Add callback
    if(!object.__callbacks__) object.__callbacks__ = new ObjectObserver.Callbacks();
    object.__callbacks__.add(callback);

    // Wrap properties
    ObjectObserver.PropertyWrapper.wrapAll(object);

    // Install PollObserver
    ObjectObserver.PollObserver.install(object);
  },
  unregister: function(object, callback){
    object.__callbacks__.remove(callback);

    // If there are no other callbacks defined, unwrap properties and uninstall observer
    if(object.__callbacks__.length == 0){
      delete object.__callbacks__;

      ObjectObserver.PropertyWrapper.unwrapAll(object);

      ObjectObserver.PollObserver.uninstall(object);
    }
  }
};

ObjectObserver.PollObserver = {
  /**
   * Polling interval, used for checking changes of arrays (in seconds).
   */
  POLLING_INTERVAL: 0.3,
  /**
   * This method installs polling observers to given object.
   * @param object {Object} Any object which should be observed
   * @param diffFn {Function} Should calculate the difference between new and older "values".
   * @param diffValuesFn {Function} Defines what should be taken as "values" for diffFn
   */
  install: function(object){
    // If there is already an observer, return
    if(object.__observeFn__) return;

    var oldProperties = ObjectObserver.ObjectExtensions.wrappableProperties(object);
    if(Object.isArray(object)) var oldArray = object.clone();
    object.__observeFn__ = function(){
      // If function has been unregistered, see #uninstall
      if(!object.__observeFn__) return;

      var newProperties = ObjectObserver.ObjectExtensions.wrappableProperties(object);
      var diff = this._checkForDifferences(newProperties, oldProperties);
      oldProperties = newProperties;

      if(Object.isArray(object)){
        var newArray = object.clone();
        var d = this._checkArray(newArray, oldArray);
        oldArray = newArray;
        object.__callbacks__.invokeForDiff(object, d);
      }

      object.__callbacks__.invokeForDiff(object, diff);

      // For added properties a wrapper doesn't exist yet, so install one
      diff.added.each(function(prop){
        ObjectObserver.PropertyWrapper.wrap(object, prop);
      });

      object.__observeFn__.delay(this.POLLING_INTERVAL);
    }.bind(this);
    object.__observeFn__.delay(this.POLLING_INTERVAL);
  },
  uninstall: function(object){
    // on next call of object.__observeFn__, it is checked if this function still exists.
    delete object.__observeFn__;
  },
  /**
   * @param array
   * @param oldArray
   * @return {Object} Contains keys enumerating added, changed and deleted fields
   *
   * @example
   *   var array = [1];
   *   var oldArray = array.clone();
   *   array[3] = 5;
   *   ObjectObserver._checkArray(array, oldArray); //#=> {added: [1,2,3], changed: [], deleted: []}
   */
  _checkArray: function(array, oldArray){
    var diff = {added: [], changed: [], deleted: []};

    // Calculate changes
    var minLength = Math.min(array.length, oldArray.length);
    for(var i = 0; i < minLength; i++){
      if(array[i] !== oldArray[i]){
        diff.changed.push(i);
      }
    }

    // Are there any deleted or added elements?
    if(array.length > oldArray.length){ // There are elements which have been added
      for(var i = oldArray.length; i < array.length; i++){
        diff.added.push(i);
      }
    } else if(array.length < oldArray.length){ // There are elements which have been deleted
      for(var i = array.length; i < oldArray.length; i++){
        diff.deleted.push(i);
      }
    }

    return diff;
  },
  _checkForDifferences: function(keys, oldKeys){
    var diff = {added: [], changed: [], deleted: []};

    var keysProp = {};
    var oldKeysProp = {};

    for(var i = 0; i < keys.length; i++) {
      keysProp[keys[i]] = true;
    }
    for(var i = 0; i < oldKeys.length; i++) {
      oldKeysProp[oldKeys[i]] = true;
      // is the old key in current keys?
      if(!keysProp[oldKeys[i]]) diff.deleted.push(oldKeys[i]);
    }
    for(var i = 0; i < keys.length; i++) {
      // is the current key in old keys?
      if(!oldKeysProp[keys[i]]) diff.added.push(keys[i]);
    }

    return diff;
  }
};

ObjectObserver.ObjectExtensions = {
  wrappableProperties: function(object){
    var keys = [];
    for (var prop in object){
      if(!(Object.isArray(object) && parseInt(prop) == prop)) keys.push(prop);
    }
    return keys;
  }
};

ObjectObserver.PropertyWrapper = {
  wrapAll: function(object){
    ObjectObserver.ObjectExtensions.wrappableProperties(object).each(function(prop){
      this.wrap(object, prop);
    }.bind(this));
  },
  /**
   * Wraps all setters of given object (and defines setters where not available yet) if it hasn't been wrapped yet.
   * @param object
   * @param attr [Array,String] If an array is passed, wrap will be call for each element.
   */
  wrap: function(object, attr){
    this._assureAccessors(object, attr);

    var setter = object.__lookupSetter__(attr);

    // If a setter couldn't be defined (e.g. there has been a getter but no setter) or has already been wrapped, return
    if(!setter || setter.__wrapped__) return;

    var wrappedFunction = function(val){
      // Invoke the wrapped method
      setter.call(object, val);
      // Call all callbacks
      object.__callbacks__.invoke(object, attr, val, 'changed');
    };
    wrappedFunction.__wrapped__ = setter;

    object.__defineSetter__(attr, wrappedFunction);
  },
  unwrapAll: function(object){
    ObjectObserver.ObjectExtensions.wrappableProperties(object).each(function(prop){
      this.unwrap(object, prop);
    }.bind(this));
  },
  unwrap: function(object, attr){
    var setter = object.__lookupSetter__(attr);

    if(!setter || !setter.__wrapped__) return;

    object.__defineSetter__(attr, setter.__wrapped__);
  },
  /**
   * Defines accessors for given object if there aren't any.
   * @param obj
   * @param attr
   */
  _assureAccessors: function(obj, attr){
    if(!obj.__lookupSetter__(attr) && !obj.__lookupGetter__(attr)){
      // This variable will hold the value of the property
      var value = obj[attr];
      // Define the setter.
      obj.__defineSetter__(attr, function(v) {
        value = v;
      });
      // Define corresponding getter.
      obj.__defineGetter__(attr, function() {
        return value;
      });
    }
  }
};

Array.subclass('ObjectObserver.Callbacks', {
  remove: function(cb){
    var index = this.indexOf(cb);
    if(index == -1){
      console.warn("ObjectObserver.Callbacks: Callback which should be removed is not registered!");
      return;
    }
    this.splice(index, 1);
  },
  add: function(cb){
    if(this.indexOf(cb) == -1) this.push(cb);
  },
  invoke: function(object, attribute, value, changeKey){
    for(var i=0; i < this.length; i++){
      this[i](object, attribute, value, changeKey);
    }
  },
  invokeForDiff: function(object, diff){
    for(var changeKey in diff){
      var diffChangeKey = diff[changeKey];
      for(var i=0; i < diffChangeKey.length; i++){
        this.invoke(object, diffChangeKey[i], object[diffChangeKey[i]], changeKey);
      }
    }
  }
});

}) // end of module