/*
  $Id: autonav.js 8375 2008-03-18 06:32:31Z a1155544 $
  
  AutoNav object
  
  Hooks into the DOM to collapse and reopen the list nav to indicate current position,
  building the crumb along the way.

  Author: Andrew Kirkpatrick
  Copyright: The University of Adelaide, 2007
*/

function AutoNav() {
  this.display_nav = true;
  // id of the nav root node
  this.nav_root_id = "nav-container";
  // LIs with this class at the top of the nav will have their links prepended to the crumb
  this.parent_class = "nav-parent";
  this.crumb_root_id = "crumb-container";
  this.crumb_breaker = "&nbsp;>&nbsp;";
  this.page_alternatives = ["index.html", "index.htm"];
  this.page_alts_string = this.buildAlternatives(this.page_alternatives, false);
  this.page_alts_regexp = new RegExp(this.page_alts_string+"(?:(?:\\?|#).*)?$", "i");

  // these are defined in the funcs that use them
  this.url_match_regexp = null;
  this.url_host_regexp = null;
  this.url_pathname_regexp = null;
  this.url_clean_regexp = null;

  //console.debug("AutoNav constructed");
}

// builds an alternating regex source from the given array
// if capt is false, the expression won't capture
AutoNav.prototype.buildAlternatives = function(alternatives, capt) {
  var str = capt ? "(" : "(?:";
  for (var i=0; i<alternatives.length; i++) {
    if (i > 0) str += "|";
    str += alternatives[i];
  }
  str += ")";
  return str;
};

// given two lists, returns the index of the first element that differs
AutoNav.prototype.findDiffInLists = function(data1, data2, fromIdx) {
  if (fromIdx == null) fromIdx = 0;
  if (data1 instanceof Array && data2 instanceof Array)
    for (var i = fromIdx; i < data1.length; i++)
      if (data1[i] != data2[i])	return i;
  return null;
};

// iterate nextSibling until we get a DOM object
AutoNav.prototype.nextObject = function(node) {
  var elem = node.nextSibling;
  while (elem && elem.innerHTML == null) {
    elem = elem.nextSibling;
  }
  return elem;
};

// return the url given, with common implicit names (index.html) removed
AutoNav.prototype.urlClean = function(url) {
  if (this.url_clean_regexp == null)
    this.url_clean_regexp = new RegExp("^(.*/)"+this.page_alts_string);

  if (url.match("/\/$/")) {
    return url;
  }
  if (url.match(this.url_clean_regexp)) {
    return RegExp.$1;
  }
  return url;
};

// return the pathname component of a url
// if collapse is true, removes common implicit names like index.html
AutoNav.prototype.urlPathname = function(url, collapse) {
  if (this.url_pathname_regexp == null)
    this.url_pathname_regexp = new RegExp("\/\/[^\/]+([^?#]*).*?$");

  if (url.match(this.url_pathname_regexp)) {
    if (collapse) {
      var pathname = RegExp.$1;
      return pathname.replace(this.page_alts_regexp, "");
    } else {
      return RegExp.$1;
    }
  }
  return url;
};

// return the host component of a url
AutoNav.prototype.urlHost = function(url) {
  if (this.url_host_regexp == null)
    this.url_host_regexp = new RegExp("\/\/([^\/]+).*$");

  if (url.match(this.url_host_regexp)) {
    return RegExp.$1;
  } else if (url.match(/^([^\/]+).*$/)) {
    return RegExp.$1;
  }
  return null;
};

// relaxed matching to allow for implicit index pages
// returns boolean indicating match
AutoNav.prototype.urlMatch = function(url, target) {
  if (this.url_match_regexp == null)
    this.url_match_regexp = new RegExp("^(.*/)"+this.page_alts_string+"$");
  if (url == target) {
    return true;
  } else {
    if (url.match(this.url_match_regexp) && RegExp.$1 == target) {
      return true;
    }
  }
  return false;
};

// what it says (if tag is null will search for tagless nodes)
AutoNav.prototype.firstChildByTagName = function(node, tag) {
  if (!node) return false;
  if (tag) tag = tag.toUpperCase();
  var child = node.firstChild;
  if (child) {
    do {
      if (child.tagName) {
        if (child.tagName.toUpperCase() == tag) {
          return child;
        }
      } else if (! tag) {
        return child;
      }
    } while (child = child.nextSibling);
  }
  return null;
};

// walk from the current node to the top level of the nav, returning
// an array of 'parent' links. If expand is true, expand ULs appropriately
AutoNav.prototype.walkNavToRoot = function(current, expand) {
  var path = [];
  while (current.parentNode.parentNode &&
         current.parentNode.parentNode.id != this.nav_root_id) {
    if (current.tagName == 'UL') {
      if (expand) current.style.display = "";
    } else if (current.tagName == 'LI') {
      path.push(this.firstChildByTagName(current, 'A'));
    }
    current = current.parentNode;
  }
  if (current.tagName == 'LI') {
    path.push(this.firstChildByTagName(current, 'A'));
  }
  return path;
};

// find the closest match in the nav by choosing from the given array,
// that which has a parent that most closely matches the path components
// of the current page
AutoNav.prototype.findClosestNavLinkByStructure = function(links) {
  var pathlist = window.location.pathname.split(/\//);
  var best_link;
  var best_score = 0;
  for (var i=0; i<links.length; i++) {
    var path = this.walkNavToRoot(links[i]);
    if (! path) continue;
    var max = 0;
    for (var j=0; j<path.length; j++) {
      var current_list = this.urlClean(path[j].pathname).split(/\//);
      var score = this.findDiffInLists(current_list, pathlist, 1);
      if (score > max) {
        max = score;
        //console.debug("pathname %d,%d=> %s, %d",i,j,current_url,max);
      }
    }
    if (max > best_score) {
      best_link = links[i];
      best_score = max;
    }
    //console.debug("findClosestNavLinkByStructure() %s => %d",link.text,path.length);
  }
  return best_link;
};

// try various strategies to obtain the best link in the nav for this page
AutoNav.prototype.findClosestNavLink = function(links) {
  //console.group("findClosestNavLink()");
  var i;
  var pathname = window.location.pathname;
  var href = window.location.href;
  var host = window.location.host;
  
  var closest = [];
  // exact match
  for (i=0; i<links.length; i++) {
    if (links[i].href == pathname || links[i].href == href) {
      //console.debug("found by exact match #%d: %o", i, links[i]);
      closest.push(links[i]);
    }
  }

  if (closest.length == 0) {
    // account for implied index.html etc
    for (i=0; i<links.length; i++) {
      if (this.urlMatch(links[i].href, pathname) ||
          this.urlMatch(links[i].href, href)) {
        //console.debug("found by this.urlMatch() #%d: %o", i, links[i]);
        closest.push(links[i]);
      }
    }
  }

  if (closest.length > 0) {
    if (closest.length == 1) {
      return closest[0];
    } else {
      // fall back to examining link parentage
      var closest_link = this.findClosestNavLinkByStructure(closest);
      //console.debug("best link by structure: %o(%s)", closest_link, closest_link.text);
      return closest_link;
    }
  }

  // fall back to closest match
  var best_links = [];
  var best_score = 0;
  var path = pathname.split(/\//);

  for (i=0; i<links.length; i++) {
    var current_link = links[i];
    var current_host = this.urlHost(current_link.href);
    if (current_host && current_host != host) {
      //console.debug("Skipping link to %s", current_host);
      continue;
    }
    var current_href = current_link.href;
    current_href = this.urlPathname(current_href);
    var linkpath = current_href.split(/\//);
    var current_score = 0;
    for (var j=1; j<linkpath.length; j++) {
      if (j >= path.length) {
        var dec = (linkpath.length - path.length);
        //console.debug("decrementing score from %d by %d on %o(%s)", current_score, dec, current_link, current_link.text);
        current_score -= dec;
        break;
      }
      if (linkpath[j] == path[j]) {
        current_score++;
        //console.debug("link %s matched %s for score %d", current_href, path[j], current_score);
      }
    }
    if (current_score == best_score) {
      //console.debug("adding link %o(%s) with score %s", current_link, current_link.text, current_score);
      best_links.push(current_link);
    } else if (current_score > best_score) {
      //console.debug("ditching previous in favour of %o(%s)", current_link, current_link.text);
      best_score = current_score;
      best_links = [current_link];
    }
  }
  var bestest_link;
  var bestest_pathname;    
  if (best_links.length > 0) {
    for (var i=0; i<best_links.length; i++) {
      //console.debug("best link #%d: %o(%s)", i, best_links[i].href, best_links[i].href);
      if (! bestest_link) {
        bestest_link = best_links[0];
        bestest_pathname = this.urlPathname(bestest_link.href, true);
        //console.debug("this.urlPathname("+bestest_link.href+", true) => "+bestest_pathname);
        continue;
      }
      var current_pathname = this.urlPathname(best_links[i].href, true);
      //console.debug("this.urlPathname("+best_links[i].href+", true) => "+current_pathname);
      if (current_pathname.length < bestest_pathname.length) {
        bestest_link = best_links[i];
        bestsest_pathname = current_pathname;
      }
    }
    //console.info("bestest link: %o(%s)", bestest_link, bestest_link.text);
  } else {
    //console.warn("best link: null");
  }
  //console.groupEnd();
  return bestest_link;
};

// sets display to none on all descendant UL tags
AutoNav.prototype.collapseMenu = function(node) {
  if (!document.getElementById) return false;
  if (!node) node = document.getElementById(this.nav_root_id);
  if (!node) return false;

  var nodes = node.getElementsByTagName('UL');
  for (var i=0; i<nodes.length; i++) {
    if (nodes[i].parentNode != node) {
      nodes[i].style.display = "none";
    }
  }
};

// expand the nav appropriately for where we are in the site
AutoNav.prototype.expandMenu = function(root) {
  if (!document.getElementById || !document.getElementsByTagName) return false;
  if (!root) root = document.getElementById(this.nav_root_id);
  if (!root) return false;

  if (root.tagName == 'UL') {
    var node = root;
  } else {
    var node = this.firstChildByTagName(root, 'UL');
    if (!node) return false;
  }

  var links = node.getElementsByTagName('A');
  if (!links) { return false; }
  
  var path;
  var link = this.findClosestNavLink(links);
  if (link) {
    var current = link;
    var parent = link.parentNode;
    current.className = "current_link";
    parent.className = "current";
    // expand child branch if there
    var children = parent.childNodes;
    for (var j=0; j<children.length; j++) {
      if (children[j].tagName == 'UL') {
        children[j].style.display = "";
      }
    }
    // step back, expanding branches and accumulating path
    path = this.walkNavToRoot(current, true);
  } else {
    path = [];
  }
  // push any parent site links onto the list
  var prepend = [];
  var li_top = this.firstChildByTagName(node, 'LI');
  while (li_top && li_top.className == this.parent_class) {
    if (li_top.tagName == 'LI') {
      var a_top = this.firstChildByTagName(li_top, 'A');
      if (a_top) {
        prepend.push(a_top);
      }
    }
    li_top = this.nextObject(li_top);
  }
  // now pick up the site root if we haven't already
  if (li_top) {
    var a_top = this.firstChildByTagName(li_top, 'A');
    if (a_top) {
      if (path.length) {
        if (a_top != path[path.length-1]) {
          path.push(a_top);
        }
      } else {
        path.push(a_top);
      }
    }
  }

  for (var i=prepend.length-1; i>=0; i--) {
    path.push(prepend[i]);
  }

  path.reverse();
  return path;
};

// creates breadcrumbs from the nav path generated by expandMenu
AutoNav.prototype.makeCrumbs = function(path, node) {
  if (!path) return false;
  if (!node) node = document.getElementById(this.crumb_root_id);
  if (!node) return false;

  // clear any crumb present
  var nodes = node.getElementsByTagName('UL');
  for (var i=0; i<nodes.length; i++) {
    //console.debug("removing previous crumb");
    node.removeChild(nodes[i]);
  }

  var ul_elem = document.createElement('ul');
  for (var i=0; i<path.length; i++) {
    if (i > 0) {
      var breaker = document.createElement('li');
      breaker.innerHTML = this.crumb_breaker;
      ul_elem.appendChild(breaker);
    }
    var li_elem = document.createElement('li');
    li_elem.setAttribute('class', 'crumb');
    var a_elem = document.createElement('a');
    a_elem.setAttribute('href', path[i].href);
    a_elem.innerHTML = path[i].innerHTML;
    li_elem.appendChild(a_elem);
    ul_elem.appendChild(li_elem);
  }
  node.appendChild(ul_elem);
};

// finds the nav, shows the children, expands the menu, leaves crumbs
AutoNav.prototype.process = function() {
  var root = document.getElementById(this.nav_root_id);
  if (root) {
    if (this.display_nav) {
      var children = root.childNodes;
      for (var i=0; i<children.length; i++) {
        var child = children[i];
        if (child.tagName == 'UL') {
          child.style.display = "block";
        }
      }
    }
    this.collapseMenu();
    var path = this.expandMenu();
    this.makeCrumbs(path);
  }
};

// politely adds a function to the end of the window.onload chain
AutoNav.prototype.addLoadHandler = function(func) {
  var oldonload = window.onload;
  if (typeof window.onload != 'function') {
    window.onload = func;
  } else {
    window.onload = function() {
      oldonload();
      func();
    }
  }
};

// install a DOM ready event handler in whatever way we can, falling back to onload
AutoNav.prototype.addReadyHandler = function(func) {
  this.onready_func = func;
  var anav = this;
  var handle_ready = function() { anav.readyHandler() };

  if (document.addEventListener) {
    document.addEventListener("DOMContentLoaded", handle_ready, false);

  } else if (document.all && !window.opera) {
    document.write('<script type="text/javascript" id="domready_trigger" defer="defer">void(0)</script>')
    var domready_trigger = document.getElementById("domready_trigger")
    domready_trigger.onreadystatechange = function() {
      if (this.readyState == "complete") anav.readyHandler();
    }

  } else if(/Safari/i.test(navigator.userAgent)){ // handle safari
    var timer_id = setInterval(function() {
                                 if (/loaded|complete/.test(document.readyState)) {
                                   clearInterval(timer_id);
                                   handle_ready();
                                 }
                               }, 20)
  }

  this.addLoadHandler(function() { setTimeout(handle_ready, 0) });
};

// the ready handler only acts once
AutoNav.prototype.readyHandler = function() {
  if (! this.ready_handled) {
    this.ready_handled = true;
    this.onready_func.call();
  }
};

// set up the event handler to run, when the DOM is ready if possible
AutoNav.prototype.runOnReady = function() {
  var anav = this;
  this.addReadyHandler(function() { anav.process() });
  document.write('<style type="text/css">\n' +
                 '  #nav { display: none; }\n' +
                 '  #crumb-container { display: block; }\n' +
                 '</style>');
};

(new AutoNav()).runOnReady();
