//
// Copyright Tim Pizey 12/02/2008
//
// This file contains Javascript classes defining an abstract TreeNode
// containing data, and two implementation of an 
// expandable/collapsible tree.
//

// ****************
// TreeNode Object
// ****************

  //Constructor
function TreeNode(troid, displayName, depth, isLeaf) {
  this.troid = troid; // Table row object id - server id  
  this.displayName = displayName;
  this.depth = depth;
  this.isLeaf = isLeaf;

  this.parent = null;
  this.children = [];
  if (depth == 0) 
    this.isOpen = true;
  else 
    this.isOpen = false;
  this.index = null; // index in the Tree's flattened array
  this.selected = false;
}

TreeNode.prototype.setParent = function (parent) {
  this.parent = parent;
  parent.addChild(this);
};

TreeNode.prototype.addChild = function (child) {
  this.children[this.children.length] = child;
};

TreeNode.prototype.getChildren = function () {
  return this.children;
};

TreeNode.prototype.getAncestors = function() {
  var ancestors = [];
  var ancestor = this.parent;
  while (ancestor != null) {
    ancestors[ancestors.length] = ancestor;
    ancestor = ancestor.parent;
  }
  return ancestors;
};

// Return array includes this as first element
TreeNode.prototype.getDescendants = function() {
  return this.flatten(true, -1);
};

TreeNode.prototype.flatten = function (depthFirst, depth) {
  var agenda = [this];
  var results = [];
  while (agenda.length != 0) {
//    current = agenda.shift();  // Only available in JScript 5.5+
    var current = agenda[0];
    agenda = agenda.slice(1);
    results[results.length] = current;

    if (this.depth < depth || depth < 0) {
      var kids = current.getChildren();
      if (depthFirst) {
        agenda = kids.concat(agenda);
      }
      else {
        agenda = kids;
      }
    } 
  }
  return results;
};


TreeNode.prototype.writeTree = 
  // lastChild whether this is the last child
  // aboves an array of booleans leading to root 
function (lastChild, aboves) {
  var str;
  str = this.writeNode(lastChild,aboves);
  if (this.isOpen) {
    var kids = this.getChildren();
    var newAboves = [];
    for (var i=0; i<aboves.length; i++)
      newAboves[i] = aboves[i];
    newAboves[newAboves.length] = !lastChild;
    for(var i=0; i<kids.length; i++) {
      str += kids[i].writeTree(i==kids.length-1, newAboves);
    }
  }
  return str;
};

  
// Common Constructor 
function StaticTree(nodesByKey, 
                    contentDiv,
                    selectedDiv,
                    roots,  // an array of root nodes 
                    chosen, // an array of troids already selected
                    myName, // javascript variable name
                    maxChoice,
                    selectNodes,   // whether nodes are selectable
                    selectLeaves)  // whether leaves are selectable 
{

  // Members
  this.contentDiv = contentDiv;
  this.selectedDiv = selectedDiv;
  this.roots = roots;     // Should be an array of TreeNodes
  this.chosen = chosen;   // An array of troids, currently
  this.myName = myName;   // so that we can write calls to ourself
  this.maxChoice = maxChoice;

  this.selectNodes = selectNodes;
  this.selectLeaves = selectLeaves;

  this.flattened = [];  
  
  this.maxDepth = this.indexNodes(myName);

  for (var i = 0; i < chosen.length; i++ ) {
    this.flattened[nodesByKey[chosen[i]].index].selected = true;
  }

}
//********************
// StaticTree methods
//********************

StaticTree.prototype.indexNodes = function (treeName)  {
  var maxDepth = 0;
  for(var i=0; i<this.roots.length; i++) {
    var f = this.roots[i].flatten(true,-1);
    for(j=0; j<f.length; j++) {
      this.flattened[this.flattened.length] = f[j];
      if (f[j].depth > maxDepth) 
        maxDepth = f[j].depth;
    }
  }
  for(i=0; i<this.flattened.length; i++) { 
    this.flattened[i].index = i;
    this.flattened[i].treeName = treeName;
    this.flattened[i].tree = this;
  }
  return maxDepth;
};

StaticTree.prototype.getSelectedTroids = 
function() {
  var selected = [];
  for(i=0;i < this.flattened.length; i++) {
    var node = this.flattened[i];
    if(node.selected) {
      selected[selected.length]=node.troid;
    }
  }
  return selected;
};

StaticTree.prototype.getSelectedDisplayNames = 
function ()  {
  var selected = [];
  for(i=0;i < this.flattened.length; i++) {
    var node = this.flattened[i];
    if(node.selected) {
      selected[selected.length]=unHtmlEncode(node.displayName);
    }
  }
  return selected;
}
function unHtmlEncode(escapedText) {
  var returnString = escapedText.replace(/\&amp\;/gi, "&");
  returnString = returnString.replace(/\&lt\;/gi, "<");
  returnString = returnString.replace(/\&gt\;/gi, ">");
  return returnString;
}
StaticTree.prototype.clearSelected = 
function() { 
  for(var i=0;i < this.flattened.length; i++) {
    this.flattened[i].selected = false;
  }
};
  

// Constructor 
function GraphicTree(nodesByKey,
                     contentDiv,
                     selectedDiv,
                     roots,  // an array of root nodes 
                     chosen, // an array of troids already selected
                     myName,
                     maxChoice,
                     selectNodes, selectLeaves,
                     backgroundColour, stylesheet,
                     verticalLinkImage, spacerImage,
                     openedTImage, openedLImage,
                     closedTImage, closedLImage,
                     leafTImage, leafLImage,
                     openedFolderImage, closedFolderImage, leafImage) {
  // Object creation by augmentation of a prototype
  // see http://javascript.crockford.com/prototypal.html 
  function it() {}
  it.prototype = new StaticTree(nodesByKey, 
                                contentDiv,
                                selectedDiv,
                                roots,  // an array of root nodes 
                                chosen, // an array of troids already selected
                                myName,
                                maxChoice,
                                selectNodes, 
                                selectLeaves);

  it.backgroundColour = backgroundColour;
  it.stylesheet = stylesheet;
  it.spacerImage = spacerImage;  // needed in nodes and in tree

  // -----------------------------
  // Subclassed TreeNode variables
  // -----------------------------
  TreeNode.prototype.spacerImage = spacerImage;
  TreeNode.prototype.verticalLinkImage = verticalLinkImage;
  TreeNode.prototype.openedTImage = openedTImage;
  TreeNode.prototype.openedLImage = openedLImage;
  TreeNode.prototype.closedTImage = closedTImage;
  TreeNode.prototype.closedLImage = closedLImage;
  TreeNode.prototype.leafTImage = leafTImage;
  TreeNode.prototype.leafLImage = leafLImage;
  TreeNode.prototype.openedFolderImage = openedFolderImage;
  TreeNode.prototype.closedFolderImage = closedFolderImage;
  TreeNode.prototype.leafImage = leafImage;


  TreeNode.prototype.writeNode = 
    // lastChild whether this is the last child
    // aboves an array of booleans length equal to our depth 
    // Return a string 
  function (lastChild,aboves) {
    var str = "<table NOWRAP CELLPADDING='0' CELLSPACING='0' BORDER='0' class='node'><TR><TD valign='middle'>";

    for(var i=0; i<aboves.length; i++)
    {
      if (aboves[i] == true) {
        str += "<IMG align='middle' SRC='" + this.verticalLinkImage + "' border='0' HEIGHT='22' WIDTH='16'>";
      }
      else {
        str += "<IMG align='middle' SRC='" + this.spacerImage + "' border='0' HEIGHT='22' WIDTH='16'>";
      }
    }
  
    if (this.isLeaf) {
      str += "<img align='middle' border='0' src='";
      if (lastChild == true) { 
        str += this.leafLImage;
      } else { 
        str += this.leafTImage;
      }
      
      str +="'>";
      str += "<IMG align='middle' src='"+this.leafImage+"' border='0' />";
    } else {
      str += "<a onClick='" + this.treeName + ".toggle("+this.index+"); return false;'>"
      str += "<IMG align='middle' BORDER='0' SRC='";
      if (this.isOpen) {
        str += (lastChild) ? this.openedLImage : this.openedTImage;
      } else {
        str += (lastChild) ? this.closedLImage : this.closedTImage;
      }
      str += "'>";
      str += (this.isOpen) ? 
          "<IMG align='middle' src='"+this.openedFolderImage+"' border='0' />" 
         : "<IMG align='middle' src='"+this.closedFolderImage+"' border='0' />";
      str += "</a>";
    }
    
    str += "</TD><TD valign=middle><nobr>";
    if (this.selected) {
      str += "<b>";
      str += "<font size='3'>";
    } else {
      str += "<font size='1'>";
    }
    str += "&nbsp; <a onClick='" + this.treeName + ".selectNode(\"" + this.index + "\");' title='Choose " + this.displayName + "' class='node'>" + this.displayName + "<\/a>";
    str += "</font>";
    if (this.selected)
      str += "</b>";

    str += "</nobr>";
    str += "</TD></TR></table>\n";
    return str;
  };

  it.prototype.display = 
  function () {
    var str;
    str = "";

    for(i=0; i<this.roots.length; i++) {
      str += this.roots[i].writeTree((i == (this.roots.length -1)), []);
     }
    //alert("about to render");
    //var startTime = new Date();
    this.contentDiv.innerHTML = str;      
    //var endTime = new Date();
    //alert("Done: " + (endTime - startTime));
    this.showSelected();
  };
  
  it.prototype.toggle = 
  // Change an open node to a closed one and visa versa
  function(index)  {
    var node = this.flattened[index];
    var nodeAlreadyOpen = node.isOpen;
    if (node.depth == 1 && !nodeAlreadyOpen) {
      for(var i=0; i<this.flattened.length; i++)
      {
        if (this.flattened[i].depth == 1 && this.flattened[i].isOpen) {
          this.flattened[i].isOpen = false;
        }
      }
      this.flattened[index].isOpen = true;
    } else {
      node.isOpen = !node.isOpen;
    }
    this.display();
  };
  it.prototype.deselect = 
  function (index) { 
    this.flattened[index].selected = false;
    this.display();
  };

  it.prototype.selectNode = 
  function (index) {
    this.toggle(index);
    var node = this.flattened[index];
  
    var ancestors = node.getAncestors();
    for (var i = 0; i< ancestors.length; i++) {
      ancestors[i].selected = false;
    }
    var descendants = node.getDescendants();
    // Ignore first element - it is us
    for (var i = 1; i< descendants.length; i++) {
      this.flattened[descendants[i].index].selected = false;
    }
    if (this.getSelectedTroids().length == this.maxChoice) { 
      alert("Selecting this item would take your selection to more than the maximum of " + this.maxChoice);
    }  else { 
      if (selectNodes && !node.isLeaf) {
        node.selected = true;
      } else { 
        if (selectLeaves && node.isLeaf) {
          node.selected = true;
        }
      }
    }
    this.display();  
  };
  
  it.prototype.showSelected = 
  function() { 
    var chosenTable = "<table>";
    for(var i=0; i < this.flattened.length; i++) {
      if (this.flattened[i].selected == true) { 
        chosenTable += "<tr><td>" + "<" + "a class='node' onclick='" + this.myName + ".deselect(\"";
        chosenTable += this.flattened[i].index;
        chosenTable += "\");'>";
        chosenTable += this.flattened[i].displayName;
        chosenTable += "<" + "/a>" + "<" + "/td><" + "/tr>\n";
      }
    }
    chosenTable += "<" + "/table>\n";
    this.selectedDiv.innerHTML = chosenTable;;      
  };


  var ret = new it();
  ret.display();
  return ret;
}
// Constructor 
function DropDownTree(nodesByKey,
                      contentDiv,
                      selectedDiv,  // Not used
                      roots,        // an array of root nodes 
                      chosen,       // an array of troids already selected
                      myName,       // javascript variable name
                      minDepth,      // minimum number of dropdowns to display, zero based
                      selectNodes,   // whether nodes are selectable
                      selectLeaves,  // whether leaves are selectable 
                      returnElement) // form element object
{
  function it() {}
  it.prototype = new StaticTree(nodesByKey,
                    contentDiv,
                    selectedDiv,
                    roots,  // an array of root nodes 
                    chosen, // an array of troids already selected
                    myName,
                    1,      // maxDepth
                    selectNodes, selectLeaves);

  it.prototype.returnElement = returnElement;
  it.prototype.minDepth = minDepth;
  
  
  it.prototype.init = 
  function () {
    if (chosen.length == 1) {
      var node = this.flattened[nodesByKey[chosen[0]].index];
      var ancestors = node.getAncestors();
      for (var i = 0; i < ancestors.length; i++) 
        ancestors[i].selected = true;
      node.selected = true;    
      this.selectedNode = nodesByKey[chosen[0]];
    } else {
      this.selectedNode = null;
    }
  }
  it.prototype.clearSelected = 
  function () {
    for(var i=0;i < this.flattened.length; i++) {
      this.flattened[i].selected = false;
    }
    this.returnElement.value = "";
  }
  
  
  it.prototype.display = 
  function () {
    var str;
    str = "<div style='height:" + ((this.maxDepth  * 32) -4 ) + "px;'>";
    for(i=0; i<this.roots.length; i++) {
      str += this.roots[i].writeTree(this.minDepth, 0);
    }
    
    str += "</div>";
    //alert("returning:" + this.returnElement.value);
    //alert(str);
    this.contentDiv.innerHTML = str;
  };
  
  it.prototype.selectNode = 
  function (indexFromSelect) {
    var flattenedIndex;
    this.clearSelected();
    if (indexFromSelect == 'nothing') {
      if (this.selectedNode != null) {
        this.selectedNode = this.selectedNode.parent;
        if (this.selectedNode != null) {
          flattenedIndex = this.selectedNode.index;
        } else {
          flattenedIndex = 0;
        }
      } else {
        flattenedIndex = 0;
      }
    } else { 
      flattenedIndex = indexFromSelect;
    }
    if (flattenedIndex == 0) { 
      this.returnElement.value = "nothing";
    } else { 
      var node = this.flattened[flattenedIndex];
      var ancestors = node.getAncestors();
      for (var i = 0; i < ancestors.length; i++) 
        ancestors[i].selected = true;
      node.selected = true;
      this.selectedNode = node;
      this.returnElement.value = node.troid.substring(1,node.troid.length);
    }
    this.display();  
  };

  var instance = new it();
  instance.init();
  TreeNode.prototype.tree = instance;
  
  TreeNode.prototype.writeTree = 
  function (minDepth, currentDepth) {
    var str;
    var selectedChild = null;
    var front;
    str = "<select name='" + this.treeName + "_dummy" + currentDepth + "' ";
    str += "id='" + this.treeName + "_dummy" + currentDepth + "' ";
    str += "onChange='" + this.treeName + ".selectNode(options[selectedIndex].value);'>\n";
    str += "<option value='nothing' ";
    //alert(this.tree.selectedNode);
    //if (this.tree.selectedNode == null)
    //  str += "selected" 
    str += ">Any</option>\n";
    var kids = this.getChildren();
    for(var i=0; i<kids.length; i++) {
      str += kids[i].writeNode();
      if (kids[i].selected)
        selectedChild = kids[i];
    }
    str += "</select>\n";
    
    if (selectedChild != null) {
      if (selectedChild.getChildren().length != 0) { 
        str += "<br /><br />\n" + selectedChild.writeTree(minDepth, currentDepth + 1);
      }
    }  else {
      if (this.depth < minDepth) {
        str += "<br /><br />\n<select><option>Any</option></select>\n"
      }
    }
    return str;
  };

  
  TreeNode.prototype.writeNode = 
    // Return a string 
  function () {
    if (this.depth == 0 ) 
      return "";
    else {
      var str = "<option class='node' value='" + this.index +"'";
      if (this.selected) { 
        str += " selected";
      }
      str += ">\n";
      str += this.displayName;
      str += "</option>\n";
      return str;
    }
  };

  //instance.display();
  return instance;
}

