/* 
 * Classes for displaying and manipulating phylogenetic trees.
 * 
 * These are combined view/controller classes. The underlying model consists of nodes which
 * have these attributes:
 * - name
 * - longName
 * - branchLength
 * - speciesName
 * - url (possibly null)
 * - children (array of nodes)
 * 
 * Changes in the selection are passed to a selection listener function.  The argument
 * to the function is all the node model objects.  Additional attributes can be put
 * into the node model for use by listeners (e.g. gene id)
 */
var calhoun; 
if (typeof(calhoun) == "undefined") calhoun={};

calhoun.phylotree = {};

// Config class
calhoun.phylotree.Config = function(config) {
   // defaults
   this.lineHeight = 16
   this.imageWidth = 600 // Total width we have to work with
   this.margin = 20 
   this.maxLeafWidth = 300 // Width of space we allow for leaf text
   this.handleSize = 4;
   this.handleSpacing = 4;
   this.labelNodes = true;
   this.heightBy = "font";
   
   // overrides
   for (key in config) {
       this[key] = config[key];
   }
   
};
// END Config class

// TreeView class
// graphics is either a jsGraphics instance or something which
// presents the same interface
// eventSource is either a calhoun.util.EventSource instance or something
// which presents the same interface
calhoun.phylotree.TreeView = function(rootNodeData,graphics,eventSource,config) {
   this.config = new calhoun.phylotree.Config(config)
   this.rootNodeView = new calhoun.phylotree.NodeView(rootNodeData,null,this.config)

   this.nodes = {};
   var indexNodes = function(nodeView) {
       this.nodes[nodeView.nodeData.globalId] = nodeView;
   };
   this.traverse(this.rootNodeView,indexNodes)   

   this.graphics = graphics
   this.eventSource = eventSource

   if (eventSource) { 
      eventSource.addListener("mousemove",this,"handleMouseOver")
      eventSource.addListener("click",this,"handleClick")
   }

   this.needsPlaceNodes = true

   this.selectedNode = null
   this.highlightedNode = null
};

(function() {
   var TreeViewClass = calhoun.phylotree.TreeView.prototype

   TreeViewClass.getDesiredHeight = function() {
      if (this.needsPlaceNodes) {         
         this.placeNodes()
      }

      var result = 0
      var findMaxY = function(nodeView) {
         if (nodeView.isCollapsed || nodeView.children.length == 0) {
            result = Math.max(result,nodeView.yPos+nodeView.textHeight)
         }
      };
      this.traverse(this.rootNodeView,findMaxY)
      return result + this.config.margin
   }

   // Assigns coordinates to each visible node
   TreeViewClass.placeNodes = function() {
      // count lines and get max total branch length:
      var maxTotalBranchLength = 0
      var nodeCalc = function(nodeView) {
         maxTotalBranchLength = Math.max(maxTotalBranchLength,nodeView.totalBranchLength)
      }
      this.traverse(this.rootNodeView,nodeCalc)
      
      var branchLengthFactor = 1
      if (maxTotalBranchLength > 0) {
         var cf = this.config
         branchLengthFactor = (cf.imageWidth - cf.margin*2 - cf.maxLeafWidth)/maxTotalBranchLength
      }

      // Calculate x- and y- coordinates in separate passes.
      // x-coordinates are calculated top-down, and y-coordinates
      // are calculated bottom-up
       var calcX = function(nodeView) {
         if (nodeView.parent != null) {
            nodeView.xPos = Math.round(nodeView.parent.xPos + branchLengthFactor*nodeView.nodeData.branchLength)
         }
         else {
            nodeView.xPos = this.config.margin
         }
      }

      var currYOffset = this.config.margin // updated during traversal
      var calcY = function(nodeView) {
         if (nodeView.isCollapsed || nodeView.children.length == 0) {
            nodeView.yPos = currYOffset
            if (this.config.heightBy == "font") 
                nodeView.calcTextHeight()
            else 
                nodeView.textHeight = this.config.lineHeight
            currYOffset += nodeView.textHeight
         }
         else {
            // Halfway between first child and last child
            var c = nodeView.children
            nodeView.yPos = Math.round((c[0].yPos + c[c.length-1].yPos)/2)
         }
      }

      var calcRects = function(nodeView) { nodeView.calcRects() }

      this.traverse(this.rootNodeView,calcX,false)
      this.traverse(this.rootNodeView,calcY,true)
      this.traverse(this.rootNodeView,calcRects,true)

      this.needsPlaceNodes = false
   } // end placeNodes

   TreeViewClass.draw = function draw() {
      var r // rectangle, used for select/highlight

      if (this.needsPlaceNodes) this.placeNodes()
      this.graphics.clear()

      // Filled box around selected subtree:
      if (this.selectedNode) {
         r = this.selectedNode.subtreeBounds
         this.graphics.setColor("lightblue")
         this.graphics.fillRect(r.xMin,r.yMin,r.xMax-r.xMin,r.yMax-r.yMin)

         this.graphics.setColor("blue")
         this.graphics.drawRect(r.xMin,r.yMin,r.xMax-r.xMin,r.yMax-r.yMin)
      }

      var drawNode = function(nodeView) {
         nodeView.draw(this.graphics)
      }
      this.traverse(this.rootNodeView,drawNode)

      // Unfilled box around highlighted subtree:
      if (this.highlightedNode) {
         r = this.highlightedNode.subtreeBounds
         this.graphics.setColor("blue")
         this.graphics.drawRect(r.xMin,r.yMin,r.xMax-r.xMin,r.yMax-r.yMin)       
      }

      this.graphics.paint()
   } // end draw

   TreeViewClass.findNode = function findNode(x,y) {
      var result = null
      var testFunc = function(nodeView) {
         var handleBounds = nodeView.subtreeBounds
         
         if (handleBounds.xMin <= x && handleBounds.xMax >= x &&
             handleBounds.yMin <= y && handleBounds.yMax >= y) {
            result = nodeView
         }
         else {
            // Since subtree bounds nest, we can prune the traversal
            return false 
         }
      }

      this.traverse(this.rootNodeView,testFunc,false)
      return result
   }

   TreeViewClass.handleClick = function handleClick(event) {
      this.selectedNode = this.highlightedNode
      if (this.selectedNode != null) {
         var handleBounds = this.selectedNode.handleBounds
         var x = this.eventSource.getSourceX(event)
         var y = this.eventSource.getSourceY(event)
         //if (this.selectedNode.descendantCount > 0 &&
         //    x >= handleBounds.xMin && x <= handleBounds.xMax &&
         //    y >= handleBounds.yMin && y <= handleBounds.yMax) {
         //   this.selectedNode.isCollapsed=!this.selectedNode.isCollapsed
         //   this.needsPlaceNodes = true
         //}
      }
      this.draw()

      // Notify listener, if any
      if (this.selectionListener) {
         var selectedData = []
         var getSelectedData = function(nodeView) {
	    if (nodeView.children.length == 0) selectedData.push(nodeView.nodeData)
         }

         if (this.selectedNode != null) {
            this.traverse(this.selectedNode,getSelectedData)
         }
         else {
            // Treat empty selection the same as global selection:
            this.traverse(this.rootNodeView,getSelectedData)
         }
         this.selectionListener.call(null,selectedData)
      }
   }

   TreeViewClass.handleMouseOver = function handleMouseOver(event) {
      var targetNode = this.findNode(this.eventSource.getSourceX(event),this.eventSource.getSourceY(event))
      if (targetNode != this.highlightedNode) {
         this.highlightedNode = targetNode
         this.draw()
      }
   }

   TreeViewClass.traverse = function traverse(nodeView,func,postOrder) {
      // The function can explicitly return false to prune the traversal
      var callResult
      if (!postOrder) {
         callResult = func.call(this,nodeView)
      }
      if (!nodeView.isCollapsed && callResult != false) {
         for (var i=0;i<nodeView.children.length;i++) {
            this.traverse(nodeView.children[i],func,postOrder)
         }
      }
      if (postOrder) {
         func.call(this,nodeView)
      }
   } // end traverse

})()
// END TreeView class

// NodeView class
calhoun.phylotree.NodeView = function(nodeData,parent,treeConfig) {
   this.nodeData = nodeData
   this.parent = parent
   this.treeConfig = treeConfig
   this.isCollapsed = false
   this.xPos = null
   this.yPos = null
   this.descendantCount = 0
   if (parent) {
      this.totalBranchLength = parent.totalBranchLength + nodeData.branchLength
   }
   else {
      this.totalBranchLength = 0
   }
   this.children = new Array()
   if (nodeData.children) {
      for (var i=0;i<nodeData.children.length;i++) {
         var newChild = new calhoun.phylotree.NodeView(nodeData.children[i],this,treeConfig)
         this.children.push(newChild)
         this.descendantCount += newChild.descendantCount+1
      }
   }
   this.globalId = null;
};

(function() {
   var NodeViewClass = calhoun.phylotree.NodeView.prototype

   NodeViewClass.calcTextHeight = function() {
      var label = this.getLabel()
      var labelX = this.xPos + this.treeConfig.handleSize + this.treeConfig.handleSpacing
      this.textHeight = calhoun.util.calcTextSize(label,this.treeConfig.imageWidth-labelX).height
   }

   NodeViewClass.calcHandleBounds = function() {
      var yMin = this.yPos + (this.treeConfig.lineHeight-this.treeConfig.handleSize)/2
      this.handleBounds = { xMin:this.xPos, yMin:yMin, xMax:this.xPos+this.treeConfig.handleSize, yMax:yMin+this.treeConfig.handleSize }
   }

   // Note -- this must be invoked bottom-up after coordinates are set
   NodeViewClass.calcSubtreeBounds = function() {
      if (this.children.length == 0 || this.isCollapsed) {
         this.subtreeBounds = { xMin:this.xPos, 
                  yMin: this.yPos, 
                  xMax: this.treeConfig.imageWidth-1,
                  yMax: this.yPos+this.textHeight
                }
      }
      else {
         var b1 = this.children[0].subtreeBounds
         var b2 = this.children[this.children.length-1].subtreeBounds

         this.subtreeBounds = { xMin: this.xPos-1,
                  yMin: b1.yMin,
                  xMax: this.treeConfig.imageWidth-1,
                  yMax: b2.yMax }
      }
   }

   NodeViewClass.calcRects = function() {
      this.calcHandleBounds()
      this.calcSubtreeBounds()
   }

   NodeViewClass.getLabel = function() {
      var label = ""
      if (this.isCollapsed) {
         label = "[collapsed: " + this.descendantCount + " nodes]"
      }
      else if (this.children.length == 0 && this.treeConfig.labelNodes) {
         label = this.nodeData.name
         if (this.nodeData.link) {
            if (this.nodeData.longName) 
               label = "<a href='"+this.nodeData.link+"' title='" + this.nodeData.longName + "'>"+label+"</a>"
            else 
               label = "<a href='"+this.nodeData.link+"'>"+label+"</a>"
         }
         if (this.nodeData.speciesName) label = label + "  ["+this.nodeData.speciesName+"]"
      }
      return label
   }

   NodeViewClass.draw = function draw(graphics) {
      var handleBounds = this.handleBounds
      graphics.setColor("black")
      graphics.fillRect(handleBounds.xMin,
                        handleBounds.yMin,
                        handleBounds.xMax-handleBounds.xMin,
                        handleBounds.yMax-handleBounds.yMin)
      if (this.isCollapsed || this.children.length == 0) {
         var labelX = handleBounds.xMax+this.treeConfig.handleSpacing
         var maxWidth = this.treeConfig.imageWidth - labelX
         graphics.drawStringRect(this.getLabel(),labelX,this.yPos,maxWidth,"left")
      }
      else {
         // draw lines to children
         var ca = this.children
         graphics.drawLine(this.xPos+this.treeConfig.handleSize/2,ca[0].yPos+this.treeConfig.lineHeight/2,
                           this.xPos+this.treeConfig.handleSize/2,ca[ca.length-1].yPos+this.treeConfig.lineHeight/2)
         for (var i=0;i<ca.length;i++) {
            graphics.drawLine(this.xPos+this.treeConfig.handleSize/2,ca[i].yPos+this.treeConfig.lineHeight/2,
                              ca[i].xPos,ca[i].yPos+this.treeConfig.lineHeight/2)
         }
      }
   }

})()
// END NodeView class

