// Richard  A. DeVenezia
// September 2003
// http://www.devenezia.com/javascript
//
// Many thanks to Lasse Nielson, Richard Conford, and other experts
// on comp.lang.javascript
//
// 01JUL04 RAD display:block

//----------------------------------------------------------------
var NavBar = function (NavData)
{

  // regular expression and var for extracting ##token## from templates

  var reToken = /##(.+)##/g

  // regular expression for checking #rrggbb color values

  var reColorHex = /#([a-f0-9]){2}([a-f0-9]){2}([a-f0-9]){2}/i ;
  var reColorDec = /rgb\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/ ;


  // *********************************************
  // *  Inner object constructor and prototypes  *
  // *********************************************

  //------------------------------------------------------------
  function Transition (element)
  //------------------------------------------------------------
  {
    this.element = element                              // element whose background will change
    this.from = element.style.backgroundColor           // background color transitioning from
    this.to   = element.handlerData.outColor.substr(1)  // background color transitioning to
    this.step = 0                                       // transition step counter

    var rgb

    // mozilla will return colors as "rgb(r,g,b)"

    var rgb = this.from.match (reColorHex)
    if (!rgb) {
      rgb = this.from.match (reColorDec)
      if (!rgb) rgb = [ 256, 0, 0 ]
      this.from = htmlColorString ( parseInt(rgb[1],10)
                                  , parseInt(rgb[2],10)
                                  , parseInt(rgb[3],10)
                                  )
    }

    this.from = this.from.substr(1);

    this.duration = element.handlerData.duration         // time to elapse over entire transition
    this.steps    = element.handlerData.steps            // number of steps to take to transition

    if ( ! this.duration ) this.duration = 200
    if ( ! this.steps )    this.steps = 10

    this.interval = this.duration / this.steps

    this.timerId = 0
  }

  //------------------------------------------------------------
  Transition.prototype.doStart = function ()
  //------------------------------------------------------------
  {
    // do the first transition step immediately
    this.doStep()

    // and proceed at timed intervals
    var thisObj = this
    this.timerId = setInterval ( function () { thisObj.doStep() }, this.interval )
  }

  //------------------------------------------------------------
  Transition.prototype.doStep = function ()
  //------------------------------------------------------------
  {
    var from  = this.from
    var   to  = this.to
    var step  = this.step
    var steps = this.steps

    var r0 = parseInt (from.substr(0,2),16)
    var g0 = parseInt (from.substr(2,2),16)
    var b0 = parseInt (from.substr(4,2),16)

    var r1 = parseInt (  to.substr(0,2),16)
    var g1 = parseInt (  to.substr(2,2),16)
    var b1 = parseInt (  to.substr(4,2),16)

    var r = Math.floor (r0 * ((steps-step)/steps) + r1 * (step/steps))
    var g = Math.floor (g0 * ((steps-step)/steps) + g1 * (step/steps))
    var b = Math.floor (b0 * ((steps-step)/steps) + b1 * (step/steps))

    var color = htmlColorString (r,g,b)

    if (!color.match(reColorHex)) color = '#FF0000'

    this.element.style.backgroundColor = color

    this.step ++

    if ( this.step > this.steps )
      this.doStop()
  }

  //------------------------------------------------------------
  Transition.prototype.doStop = function ()
  //------------------------------------------------------------
  {
    clearInterval ( this.timerId )
    this.element.transition = null  // clear a known reference to me
  }

  // ********************
  // *  Event handlers  *
  // ********************

  //------------------------------------------------------------
  function over (element)
  //------------------------------------------------------------
  {
    if (element.transition)
      element.transition.doStop()

    element.style.backgroundColor = element.handlerData.overColor
  }

  //------------------------------------------------------------
  function out (element)
  //------------------------------------------------------------
  {
    element.transition = new Transition (element)
    element.transition.doStart()
  }

  // *************************
  // *  Rendering functions  *
  // *************************

  //--------------------------------------------------------------
  function renderError (message)
  //--------------------------------------------------------------
  {
    document.write ('<P><B STYLE="color:white; background-color:red">' + ' ERROR: '+message+'</B></P>')
    return false
  }

  // function 'global vars'

  var page    // assigned in renderNavBar, used in renderLink
  var ipr     // assigned in renderNavBar, used in appendTD
  var maxColumnCount // assigned in renderNavBar, used in appendTD and renderNavBar
  var table   // set by getElementById()
  var row     // set by insertRow
  var groupHasText
  var cells   // holds cell refs needed to equi-size column widths

  //--------------------------------------------------------------
  function renderNavBar (NavData)
  //--------------------------------------------------------------
  {
    //
    // determine current page
    // if there is a link to the current page it will be styled differently
    //

    page = null
    ipr = null
    maxColumnCount = null
    table = null
    row = null
    groupHasText = null
    cells = null

    var p

    page = document.location.toString()

    p = page.lastIndexOf('/');
    page = page.substr (p+1)

    p = page.lastIndexOf('\\')  // hailing windows
    page = page.substr (p+1)

    p = page.lastIndexOf('#')
    if (p>0)
      page = page.substring(0,p)

    //
    // determine nature of NavData
    //

    var nav

    if (typeof (NavData) == 'function') {
      nav = new NavData
    }
    else
    if (typeof (NavData) == 'object') {
      nav = NavData
    }
    else {
      return renderError ( 'Please pass a constructor or object to NavBar(). <PRE>You passed this ' + typeof (NavData) + ':<BR>'+NavData+'</PRE>' )
    }

    //
    // check for problems with NavData object
    //

    var groups = nav.groups

    if (typeof(groups) == 'undefined')
      return renderError ('Your data should have a <I>groups</I> property.')

    if (typeof(groups) != 'object' || typeof(groups.length) == 'undefined')
      return renderError ('The <I>groups</I> property should be an array.')

    //
    // dealing with items per row specification requires
    // a function global variable
    //

    if (typeof(nav.maxItemsPerRow) != 'undefined') {
      if (isPositiveInteger (nav.maxItemsPerRow))
        ipr = nav.maxItemsPerRow
      else
        return renderError ("maxItemsPerRow is not a positive integer.")
    }
    else
      ipr = -1

    //
    // tracking max number of columns rendered requires
    // a function global variable.  is used to equi-width cells
    //

    maxColumnCount = 0  // max encountered while generating

    //
    // render base table for javascript to operate on
    // will not work if id specified is not unique
    //

    var id = ( nav.id ) ? nav.id : 'navbar'+Math.random(0).toString().substring(2)

    if (document.getElementById (id) != null)
      return renderError ('element id "' + id + '" is not unique.')

    var idProp = ' ID="' + id + '"'

    var klassProp = ( nav.klass ) ? ' CLASS="'+nav.klass+'"' : ''
    var styleProp = ( nav.style ) ? ' STYLE="'+nav.style+'"' : ''

    var baseTable = '<TABLE' + idProp + klassProp + styleProp + '><TR><TD>&nbsp;</TD></TR></TABLE>'

    document.write (baseTable)

    table = document.getElementById (id)

    for (var i=0; i < table.rows.length; ) {
      table.deleteRow(0)
    }

    //
    // render groups
    //

    cells = []
    groupHasText = false

    for (var i=0; i < groups.length; i++ )
    {
      groups[i].parent = nav
      if (! renderGroup(groups[i])) break
    }

    for (var i=0; i < cells.length; i++)
    {
//    cells[i].style.width = (100/maxColumnCount).toPrecision(4)+'%'
      var width = Math.floor((100/maxColumnCount) * 100)/100
      cells[i].style.width = width+'%'
      cells[i] = null
    }
    cells = null

    for (var i=0; i < table.rows.length; i++) {
        row = table.rows[i]
        for (var j=row.cells.length; j<maxColumnCount; j++) {
            row.insertCell (j)
        }
    }

    if (nav.postHTML)
      document.write ( nav.postHTML )

    return
  } // renderNavBar


  //------------------------------------------------------------
  function appendTD ( td )
  //------------------------------------------------------------
  {
    if (td.text == '')
      if (td.dest != '')
        td.text = '[link]'
      else
        return

    if (row == null) {
      row = table.insertRow (table.rows.length)
    }

    if (0 < ipr && ipr <= row.cells.length)
    {
      row = table.insertRow (table.rows.length)
      if (ipr > 1 && groupHasText) {
        cell = row.insertCell ( row.cells.length )
      }
    }

    cell = row.insertCell ( row.cells.length )
//  cells.push (cell)
    cells[cells.length] = cell

    // too impatient to use appendChild

    if ( td.dest != '' )
         { cell.innerHTML = '<A STYLE="display:block" HREF="'+td.dest+'">' + td.text + '</A>'  }
    else { cell.innerHTML = td.text }

    if (td.id    != '') cell.id = td.id
    if (td.klass != '') cell.className = td.klass
    if (td.style != '') cell.style = td.style

    if (td.type == 'link' && td.dest != '') {
      cell.style.backgroundColor = td.outColor
      cell.handlerData = { overColor: td.overColor, outColor: td.outColor, duration: td.duration, steps: td.steps }

      cell.onmouseover = function ( E ) { over (this) }
      cell.onmouseout  = function ( E ) { out  (this) }
      cell.transition  = null
    }

    cell = null // release reference to cell, otherwise memory leak would occur

    maxColumnCount = Math.max (maxColumnCount, row.cells.length)
  } // appendTD


  //------------------------------------------------------------
  function renderGroup (group)
  //------------------------------------------------------------
  {
    if (typeof(group) != 'object')
      return renderError ("A group was not an object.")

    var startNewRow, id, klass, style, dest, text

    startNewRow = coalesce ( group.startNewRow, group.parent.groupsStartNewRow, 0 )
    startNewRow = /yes|1|true/i.test ( startNewRow )

    if (startNewRow && (!row || row && row.cells.length > 0))
      row = table.insertRow (table.rows.length)

    var td =
    { type  : 'group'
    , id    : coalesce ( group.id, '' )
    , klass : coalesce ( group.klass, group.parent.groupClass, '' )
    , style : coalesce ( group.style, group.parent.groupStyle, '' )
    , dest  : coalesce ( group.dest, '' )
    , text  : coalesce ( group.text, '' )
    }

    appendTD (td)

    groupHasText = (td.text != '')

    //
    // render links
    //

    var links = group.links;

    if (typeof(links) == 'undefined')
      return renderError ("A <I>group</I> is missing the <I>links</I> property.")

    if (typeof(links) != 'object' || typeof(links.length) == 'undefined')
      return renderError ("The <I>links</I> property should be an array.")

    for (var i=0; i<links.length; i++)
    {
      links[i].parent = group
      if (! renderLink(links[i])) return
    }

    return true
  } // renderGroup


  //----------------------------------------------------------
  function renderLink (link)
  //----------------------------------------------------------
  {
    if (typeof(link) != 'object')
      return renderError ("A link in the <I>links</I> array is not an object.")

    var P1 = link.parent
    var P2 = link.parent.parent

    var td =
    { type      : 'link'
    , outColor  : coalesce ( link.outColor, P1.outColor, P2.outColor )
    , overColor : coalesce ( link.overColor,P1.overColor,P2.overColor )
    }

    if (typeof(td.overColor) == 'undefined') return renderError ("A link could not determine it's overColor")
    if (typeof(td.outColor)  == 'undefined') return renderError ("A link could not determine it's outColor")
    if (!reColorHex.test(td.overColor)) return renderError (td.overColor+' is not <I>#rrggbb</I>.')
    if (!reColorHex.test(td.outColor))  return renderError (td.outColor +' is not <I>#rrggbb</I>.')

    // process the templates if present

    var textTemplate = coalesce ( link.textTemplate, P1.textTemplate, P2.textTemplate )

    var destTemplate = coalesce ( link.destTemplate, P1.destTemplate, P2.destTemplate )
    var re

    // token replacement

    var token

    if (textTemplate) {
      while ((token = reToken.exec(textTemplate)) != null) {
        token = token[1]
        re = new RegExp ('##' + token + '##', 'g')
        if (typeof(link[token]) == 'undefined')
          return renderError ('Problem with ' + textTemplate + ', ' + token + ' is not a property of the link.')
        textTemplate = textTemplate.replace (re, link[token])
    } }
    else
      textTemplate = coalesce (link.text, '')

    //--

    if (destTemplate) {
      while ((token = reToken.exec(destTemplate)) != null) {
        token = token[1]
        re = new RegExp ('##' + token + '##', 'g')
        if (typeof(link[token]) == 'undefined')
          return renderError ('Problem with ' + destTemplate + ', ' + token + ' is not a property of the link.')
        destTemplate = destTemplate.replace (re, link[token])
    } }
    else
      destTemplate = coalesce (link.dest, '')

    //--

    td.text = textTemplate
    td.dest = destTemplate

    td.id    = coalesce (link.id, '')

    if ( td.dest != page ) {
      td.klass    = coalesce (link.klass, P1.linkClass, P2.linkClass, '')
      td.style    = coalesce (link.style, P1.linkStyle, P2.linkStyle, '')
      td.duration = coalesce (link.duration, P1.duration, P2.duration)
      td.steps    = coalesce (link.steps   , P1.steps   , P2.steps)
    }
    else {
      td.dest  = ''
      td.klass = coalesce (link.atClass, P1.linkAtClass, P2.linkAtClass, '')
      td.style = coalesce (link.atStyle, P1.linkAtStyle, P2.linkAtStyle, '')
    }

    appendTD (td)

    td = null

    return true
  } // renderLink


  // ***********************
  // *  Support functions  *
  // ***********************

  //--------------------------------------------------------------
  function isPositiveInteger (x)
  {
    return ( isFinite (x) && x >= 1 && Math.floor(x) == x )
  }

  //--------------------------------------------------------------
  function coalesce ()
  {
    for (var i=0; i<arguments.length; i++) {
      if (typeof(arguments[i]) != 'undefined') return arguments[i]
    }
  }

  //----------------------------------------------------------------
  function htmlColorString (r,g,b) {
    var r = r.toString(16); if (r.length == 1) r = '0'+r;
    var g = g.toString(16); if (g.length == 1) g = '0'+g;
    var b = b.toString(16); if (b.length == 1) b = '0'+b;

    return "#" + r + g + b
  }

  // *****************
  // *  Entry point  *
  // *****************

  renderNavBar (NavData)
}