/******************************************************************************
 * Copyright (c) 2002,2003 Peter 'Merlin' Balsiger and Fredy 'Mythos' Dobler
 * All rights reserved
 *
 * This file is part of Dungeon Master Assistant.
 *
 * Dungeon Master Assistant is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Dungeon Master Assistant is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Dungeon Master Assistant; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *****************************************************************************/

//------------------------------------------------------------------- header

/**
 * 
 * This script contains stuff for handling tables.
 * 
 * @file          table.js
 * 
 * @author        Peter Balsiger
 * @responsible   Peter Balsiger
 * 
 * @reviewer      
 * 
 * @distribution  
 * 
 * @bugs          
 * @to_do         
 * 
 * @see           
 * 
 * @keywords      
 * 
 *@cvs _________________________________________________________________ CVS
 * 
 * @version $Revision: 1.12 $
 * 
 * @cvs
 * $Source: /home/cvsroot/jDMA/resources/html/js/table.js,v $
 * 
 * $Locker:  $
 * $State: Exp $
 * $Log: table.js,v $
 * Revision 1.12  2007/03/26 21:57:26  merlin
 * *** empty log message ***
 *
 * Revision 1.11  2006/11/03 16:46:34  merlin
 * added dynamic updating of table
 *
 * Revision 1.10  2006/08/16 20:25:32  merlin
 * fixed sorting of groups with numbers
 *
 * Revision 1.9  2006/08/13 21:07:44  merlin
 * fixed groups when only one element in it
 *
 * Revision 1.8  2006/08/13 20:51:07  merlin
 * added switch()
 *
 * Revision 1.7  2006/08/12 14:58:03  merlin
 * fixed selection of group values
 *
 * Revision 1.6  2006/08/12 14:16:06  merlin
 * fixed add menu
 * removed some debugging statements
 *
 * Revision 1.5  2006/08/12 14:14:02  merlin
 * corrected setting with 'empty' groups
 *
 * Revision 1.4  2006/08/09 20:45:51  merlin
 * fixed problem with menu and wrong showing of columns
 *
 * Revision 1.3  2006/08/09 20:41:56  merlin
 * fixed problem with recursion in menus
 *
 * Revision 1.2  2006/08/07 21:34:37  merlin
 * adjusted to using -table- prefix
 *
 * Revision 1.1  2006/08/07 18:53:43  merlin
 * *** empty log message ***
 *
 *
 *____________________________________________________________________ class
 *
 * @pattern       
 *
 * @example       
 *
 * @derivation    
 *
 */

//..........................................................................

//__________________________________________________________________________

/** The Table object and it's constructor.
 *
 * @param inID            the id of the table
 * @param inDefaultSplits the column number to split per default
 * @param inImagePath     the path to the images, if any
 * @param ...             the individual columns
 *
 * The table object uses the following object structure:
 * 
 * Table (the complete table)
 * |
 * |-- State
 *
 *
 * 
 */
function Table(inID, inDefaultSplits, inImagePath /* ... the columns ... */)
{
  //-------------------------------------------------------------- debugging

  /* A debugging element for debugging output. */
  if(false)
  {    
    this.debug = new Object();

    this.startTimed = function(inName)
      {
        this.debug[inName] = new Date();
      }

    this.stopTimed = function(inName)
      {
        if(this.debug[inName])
          gui.debug(inName + " " + elapsedTime(this.debug[inName]));

        this.debug[inName] = undefined;
      }

    this.startTimed("init");
    this.startTimed("total");
    this.startTimed("preprocessing");
  }
  else
  {
    // no debugging
    this.startTimed = function() {};
    this.stopTimed   = function() {};
  }  

  //........................................................................

  /* The table id. */
  this.id = inID; 

  /* The default splittings. */
  this.defaultSplits = inDefaultSplits;

  /* The path to the images. */
  this.imagePath = inImagePath;

  /* The column definitions. */
  this.columns = new Object();

  // store the names of the columns
  var j = 0;
  for(var i = 3; i < arguments.length; j++)
  {
    var name   = arguments[i++];
    var title  = arguments[i++];
    var images = arguments[i++];
    var hide   = arguments[i++];
    
    var column = new Column(this, name, j, title, images, hide);    

    // store with id an number
    this.columns[j]    = column;
    this.columns[name] = column;    
  } 

  /* The state loaded for the table (or null if not yet initialized). */
  this.state = null;

  /* All the table rows. */
  this.rows = new Array();

  /* The html element of the table in the page. */
  this.element = document.getElementById(this.id);

  /* The title row of the table. */
  this.title = null;

  /* The split bars with the groups, if any. */
  this.bars = null;

  /* A flag if the table is currently shown or not. */
  this.shown = false;

  /* The rows currently shown. */
  this.currentRows = this.rows;

  /* A flag if the table is currently showing. */
  this.showing = false;

  /* The categories that can be used for grouping */
  this.categories = new Object();

  /* The categories that are current used. */
  this.category = null;

  /** The html element for the next page. */
  this.nextPage = null;

  /** The html element for the previous page. */
  this.previousPage = null;

  /** The number of rows to show on a single page. */
  this.tablePage = 20;

  /** The starting row to show inside the table. */
  this.startRow = 0;

  /** The regular expression to use for in place search, if any. */
  this.inplaceSearch = null;

  //--------------------------------- init ---------------------------------

  /** Initialize the table when first used. This can only be done after
   *  all data has been added to the table. */
  this.init = function()
  {
    // already initialized?
    if(this.state)
      return;

    // check if the table element was found
    if(!this.element)
      gui.alert("Could not find table '" + this.id + "'!");

    // load the state from the cookie
    this.state = new State(this.id, this.defaultSplits, this.columns);

    // show the title
    gui.pushStatus("showing title");

    this.showTitle();

    // build up the default groups
    if(this.state.splits.length > 0)
    {
      // build up the currently selected groups 
      var column = this.state.splits[0].column;
 
      this.category = new Category(this, column.name, null, column.images ? 
                                   this.imagePath + "/" + column.images 
                                   : null, this.state.splits, 0, 
                                   this.state.groups);

      // add the selection bar according to the current category
      this.bars = this.category.rootBar;
    }
    else
    {
      // we create a default bar to allow us to add the add and rest buttons
      this.bars                = document.createElement("div");      
      this.bars.style.position = "relative";

      // setup the default rows
      this.currentRows = this.rows;
    }

    // add to page
    this.element.parentNode.insertBefore(this.bars, this.element);

    // add the reset button
    var reset = document.createElement("div");
    
    reset.className = "-table-reset";
    reset.title     = "reset to default view";
    reset.innerHTML = "&nbsp;";
    reset.table     = this;
    reset.onclick   = function()
    { 
      // clear cookie
      this.table.state.reset();
      
      // reload page
      document.location = document.location; 
      
      return true; 
    }
    
    this.bars.insertBefore(reset, this.bars.firstChild);

    // add the add button
    var add = document.createElement("div");
    
    add.className = "-table-add";
    add.category  = this.category;
    add.table     = this;
    add.title     = "add another grouping layer";
    add.onclick   = function(inEvent)
    {
      var menu = gui.menu();
      
      for(var i = 0; this.table.state.columns[i]; i++)
      {
        if(contains(this.table.state.splits, this.table.state.columns[i]))
          continue;
        
        // ignore untitled columns
        if(!this.table.state.columns[i].column.title)
          continue;

        gui.addMenuItem(menu, this.table.state.columns[i].column.title, 
                        function(inThis)
        {
          // update the split hide status
          if(inThis.column.column.hide)
            inThis.column.splitHide = true;

          inThis.parentNode.table.state.splits.push(inThis.column); 
          
          inThis.parentNode.table.bars.parentNode.removeChild
            (inThis.parentNode.table.bars);
          inThis.parentNode.table.show();              
        }, null).column = this.table.state.columns[i];
             
      }
      
      menu.table = this.table;
     
      // adjust the menu to the left
      menu.style.right = "0px";
 
      this.appendChild(menu);

      // allow removing of menu by clicking outside
      document._onclick = document.onclick;
      document.onclick  = function(inEvent)
      {
        // remove the menu again
        if(menu.parentNode)
          menu.parentNode.removeChild(menu);
        
        document.onclick = document._onclick;
        
        if(document.onclick)
          document.onclick(inEvent)
      }    

      inEvent.stopPropagation();
    }
    
    this.bars.insertBefore(add, reset);      

    // add the pagination buttons
    this.nextPage = document.createElement("div");
        
    this.nextPage.table         = this;
    this.nextPage.className     = "-table-navigation -table-next";
    this.nextPage.innerHTML     = "next";
    this.nextPage.style.display = "none";
    this.nextPage.onclick       = function()
    {
      this.table.startRow += this.table.tablePage;          

      this.table.showRows();
    }
    
    // add to the page
    insertAfter(this.nextPage, this.element);

    this.previousPage = document.createElement("div");
        
    this.previousPage.table         = this;
    this.previousPage.className     = "-table-navigation -table-previous";
    this.previousPage.innerHTML     = "previous";
    this.previousPage.style.display = "none";
    this.previousPage.onclick       = function()
    {
      this.table.startRow -= this.table.tablePage;
      
      this.table.showRows();
    }
    
    // add to the page
    insertAfter(this.previousPage, this.element);

    // select the current group and setup the current rows (if we have a
    // default)
    if(this.state.groups && this.category)
      this.currentRows = this.category.select(this.state.groups);       

    this.stopTimed("init");
  }

  //........................................................................  
  //------------------------------ showTitle -------------------------------

  /** Show the table title.
   *
   */
   this.showTitle = function()
   {
     this.startTimed("title");

     // remove a title if any is already there
     if(this.title)
       while(this.title.firstChild)
         this.title.removeChild(this.title.firstChild);
     else
     {
       // create or replace the title when (re)showing
       this.title = document.createElement("tr"); // using insertRow is much
                                                  // slower
       this.element.appendChild(this.title);
     }
     
     for(var i = 0; this.state.columns[i]; i++)
     {
       if((this.state.columns[i].hidden 
           && this.state.columns[i].hidden == true)
          || (this.state.columns[i].splitHide 
              && this.state.columns[i].splitHide == true))
         continue;
       
       var td = document.createElement("td");
       td.className = "title " + this.state.columns[i].column.name;
       
       // sort on click (must store the reference to the column here)
       td.table  = this;
       td.column = this.state.columns[i].column; 
       
       this.state.columns[i].column.cell = td;
       
       td.onclick = function() { this.column.sort(); return false; };
       td.oncontextmenu = function(inEvent) 
         { 
           this.appendChild(this.table.showMenu(inEvent, this.column)); 
           return false;
         };
       
       if(this.state.sort == this.state.columns[i])
       {
         if(this.state.sort.sort == "ascending")
           td.className += " sort-up";
         else
           td.className += " sort-down";
       }
       
       td.innerHTML = this.state.columns[i].column.title;
       
       this.title.appendChild(td);
     }
     
     this.stopTimed("title");                
   }

  //........................................................................  
  //------------------------------ createRow -------------------------------

  /** Create a row (html) from the given table row.
   *
   * @param  inRow the row to create html for
   *
   * @return the html element for the row
   *
   */
  this.createRow = function(inRow)
  {
    var tr = document.createElement("tr"); // using insertRow is much slower
      
    for(var j = 0; this.columns[j]; j++)
    {
      // skip hidden or split hidden rows
      if((this.state.columns[j].hidden && this.state.columns[j].hidden == true)
         || (this.state.columns[j].splitHide 
             && this.state.columns[j].splitHide == true))
        continue;
        
      var td = document.createElement("td");
        
      td.innerHTML = inRow[this.state.columns[j].column.number].text;
      
      tr.appendChild(td);    
    }

    return tr;
  }

  //........................................................................ 
  //-------------------------------- index ---------------------------------

  /** Search the position in the given array to add the other value given
   *
   * @param inData      the data to search in
   * @param inValue     the value to compare with
   * @param inAscending flag if sorting ascending
   * @param inSelect    the method to get the sort value out from the data
   *                    element (if any)
   *
   * @return the array element to insert before or length + 1 if to add at the
   *         end
   */
  this.index = function(inData, inValue, inAscending, inSelect)
  {
    if(!inSelect)
      inSelect = function(a) { return a; };

    // do a binary search
    return this.indexImpl(inData, inValue, inAscending ? 1 : -1, inSelect, 0, 
                          inData.length);
  }

  this.indexImpl = function(inData, inValue, inOrder, inSelect, inStart, 
                            inEnd)
  {
    if(inEnd - inStart <= 10)
    {
      for(var i = inStart; i < inEnd; i++)        
        if(compareWithNumber(inValue, inSelect(inData[i])) * inOrder < 0)
          return i;    

      return inEnd + 1;
    }
    
    var index = Math.floor((inStart + inEnd) / 2);

    // a real binary search step
    var check = 
      compareWithNumber(inValue, inSelect(inData[index]));

    if(check == 0)
      return index;

    if(check * inOrder < 0)
      return this.indexImpl(inData, inValue, inOrder, inSelect, inStart, 
                            index);
    
    return this.indexImpl(inData, inValue, inOrder, inSelect, index + 1, 
                          inEnd);
  }

  //........................................................................
  //--------------------------------- add ----------------------------------

  /** Add a row to the table.
   *
   * @param ... the individual values for each cell
   *
   */
  this.add = function(/* ... */)
  {
    // if we are searching, only add stuff to the page that matches
    if(this.state.search)
    {
      for(var i = 0; i < arguments.length; i++)
      {
        var text = arguments[i];

        if(typeof text == "object")
          text = text.text;

        if(text.match(this.state.search))
          break;
      }

      // text not found
      if(i >= arguments.length)
        return;
    }

    var row = new Array();

    for(var i = 0; i < arguments.length; i++)
      if(typeof arguments[i] == "object")
        row.push(arguments[i]);
      else
        row.push(new Table.Cell(arguments[i], null));

    if(this.category)
    {
      // add to the complete set of rows 
      this.rows.push(row);

      // now add it to the relevant category
      this.category.add(row[this.state.splits[0].column.number].groups, row, 0,
                        this.state.splits.slice(1));
      
      // select it as default if it is the first value
      if(this.rows.length == 1)
        this.currentRows = this.category.select(this.state.groups);       
    }
    else
      this.insertRow(row);
  }

  //........................................................................  
  //------------------------------- showRows -------------------------------

  /** Show the given rows in the table, remove the old ones.
   *
   * @param inRows the rows to show
   *
   */  
  this.showRows = function(inRows)
  {
    if(inRows)
      this.currentRows = inRows;

    // do we do an inplace search?
    if(this.inplaceSearch 
       && (!this.currentRows.search 
           || this.currentRows.search != this.inplaceSearch))
    {
      var old = this.currentRows;

      this.currentRows = new Array();

      this.currentRows.old = old;
      this.currentRows.search = this.inplaceSearch;

      for(var i in this.rows)
        for(var j in this.rows[i])
          if(this.rows[i][j].text.match(this.inplaceSearch))
          {
            this.currentRows.push(this.rows[i]);

            break;
          }
    }

    // remove the old rows    
    this.startTimed("remove rows");
    if(this.title)
      while(this.title.nextSibling)
        this.element.removeChild(this.title.nextSibling);

    this.stopTimed("remove rows");
    
    // the above alone is not enough for FF, as it inserts some additional
    // newlines that show up...
    var parent = this.element.parentNode;
    var next   = this.element.nextSibling;
    
    if(this.element.nextSibling)
      parent.insertBefore(parent.removeChild(this.element), next);
    else
      parent.appendChild(parent.removeChild(this.element));      

    // the value rows
    this.startTimed("rows");

    for(var i = this.startRow; 
        i < this.currentRows.length && i < this.startRow + this.tablePage; 
        i++)
    {
      var tr = this.createRow(this.currentRows[i]);
      
      if(i % 2 == 0)
        tr.className += " even";
      else
        tr.className += " odd";

      this.element.appendChild(tr);
    }

    // show page icons if necessary
    if(this.startRow + this.tablePage < this.currentRows.length)
      this.nextPage.style.display = "";
    else
      this.nextPage.style.display = "none";
    
    if(this.startRow - this.tablePage >= 0)
      this.previousPage.style.display = "";
    else
      this.previousPage.style.display = "none";

    this.stopTimed("rows");       
  }

  //........................................................................
  //------------------------------ insertRow -------------------------------

  /** Insert the given row into the currently displayed rows.
   *
   * @param inRow the row to insert
   *
   */
  this.insertRow = function(inRow)
  {
    // do we have to add it sorted or not
    if(this.state.sort)
    {
      var tableRows = this.element.getElementsByTagName("tr");
      
      var column = this.state.sort.number;
      var index  = this.index(this.currentRows, inRow[column].sort, 
                              this.state.sort.sort == "ascending",
                              function(a) { return a[column].sort; });

      if(index < this.currentRows.length)
      {
        // add it at this position
        this.currentRows.splice(index, 0, inRow);
            
        if(index < this.startRow + this.tablePage)
        {
          if(index >= this.startRow)
          {
            // add it to the table (table rows start at 1 to skip title)
            this.element.insertBefore(this.createRow(inRow), 
                                      tableRows[index - this.startRow + 1]);
          }
          else
          {
            // we have the current display for one element, as we inserted
            // one before
            this.element.insertBefore
              (this.createRow(this.currentRows[this.startRow]), 
               tableRows[1]);
            
            index = this.startRow + 1;
          }          
          
          // fix the even/odd stuff
          for(var i = index - this.startRow; i < tableRows.length; i++)
            tableRows[i].className = (i % 2 == 1 ? "even" : "odd");
          
          // if we have more rows than fit on the page, remove the last one
          // (note, tableRows.length contains title row)
          if(tableRows.length - 1> this.tablePage)
          {
            this.element.removeChild(tableRows[tableRows.length - 1]);
            
              this.nextPage.style.display = "";
          }
        }
        
        return;
      }
    }
      
    // not yet added, add it directly to the current rows (at the end)
    this.currentRows.push(inRow);
    
    // add only if it fits on the current page
    if(this.currentRows.length <= this.startRow + this.tablePage)
    {
      var tr = this.createRow(inRow);
        
      // no sorting, just add it to the end
      if(this.currentRows.length % 2 == 0)
        tr.className += " even";
      else
        tr.className += " odd";
      
      this.element.appendChild(tr);              
    }
    else
      this.nextPage.style.display = "";
  }

  //........................................................................ 
  //--------------------------------- show ---------------------------------

  /** Show the table by adding all the elements to the HTML table object.
   *
   */
  this.show = function()
  {
    this.stopTimed("preprocessing");

    this.startTimed("categories");

    if(this.state.splits.length > 0)
    {
      // build up the currently selected groups 
      var column = this.state.splits[0].column;
      
      this.category = new Category(this, column.name, null, column.images ? 
                                   this.imagePath + "/" + column.images 
                                   : null, this.state.splits, 0);
      
      // add the rows
      for(row in this.rows)
        this.category.add
          (this.rows[row][this.state.splits[0].column.number].groups, 
           this.rows[row], 0, this.state.splits.slice(1));

      // add the selection bar according to the current category
      this.bars = this.category.rootBar;
    }
    else
    {
      // we create a default bar to allow us to add the add and rest buttons
      this.bars = document.createElement("div");      
      this.bars.style.position = "relative";
      
      this.category = null;
    }

    // add to page
    this.element.parentNode.insertBefore(this.bars, this.element);

    // add the reset button
    var reset = document.createElement("div");
    
    reset.className = "-table-reset";
    reset.title     = "reset to default view";
    reset.innerHTML = "&nbsp;";
    reset.table     = this;
    reset.onclick   = function()
    { 
      // clear cookie
      this.table.state.reset();
      
      // reload page
      document.location = document.location; 
      
      return true; 
    }
    
    this.bars.insertBefore(reset, this.bars.firstChild);

    // add the add button
    var add = document.createElement("div");
    
    add.className = "-table-add";
    add.category  = this.category;
    add.table     = this;
    add.title     = "add another grouping layer";
    add.onclick   = function(inEvent)
    {
      var menu = gui.menu();
      
      for(var i = 0; this.table.state.columns[i]; i++)
      {
        if(contains(this.table.state.splits, this.table.state.columns[i]))
          continue;
        
        // ignore untitled columns
        if(!this.table.state.columns[i].column.title)
          continue;

        gui.addMenuItem(menu, this.table.state.columns[i].column.title, 
                        function(inThis)
        {
          // update the split hide status
          if(inThis.column.column.hide)
            inThis.column.splitHide = true;

          inThis.parentNode.table.state.splits.push(inThis.column); 
          
          inThis.parentNode.table.bars.parentNode.removeChild
            (inThis.parentNode.table.bars);
          inThis.parentNode.table.show();              
        }, null).column = this.table.state.columns[i];
             
      }
      
      menu.table = this.table;
     
      // adjust the menu to the left
      menu.style.right = "0px";
 
      this.appendChild(menu);

      // allow removing of menu by clicking outside
      document._onclick = document.onclick;
      document.onclick  = function(inEvent)
      {
        // remove the menu again
        if(menu.parentNode)
          menu.parentNode.removeChild(menu);
        
        document.onclick = document._onclick;
        
        if(document.onclick)
          document.onclick(inEvent)
      }    

      inEvent.stopPropagation();
    }
    
    this.bars.insertBefore(add, reset);      
    
    this.stopTimed("categories");      
    
    // select the current group
    if(this.category)
      this.category.select(this.state.groups);     
    else
      this.showRows(this.rows);
  
    this.shown   = true;
    this.showing = false;
    
    // store the current state in a cookie
    this.state.store();
    
    this.stopTimed("total");
    gui.popStatus();
  }

  //........................................................................   
  //--------------------------------- sort ---------------------------------

  /** Sort the current table with the given column.
   *
   * @param inColumn    the column to sort
   * @param inAscending flag if sorting ascending or descending (false)
   * @param inShow      true if to show table (default), false if not
   *
   */
  this.sort = function(inColumn, inAscending, inShow)
  {
    // check if we really have to sort
    if(this.currentRows.sortColumn && this.currentRows.sortColumn == inColumn 
       && this.currentRows.sortOrder == inAscending)
      return;

    this.startTimed("sorting");    

    // show sorting status
    gui.pushStatus("sorting...");

    // store what is currently sorted
    this.currentRows.sortColumn = inColumn;
    this.currentRows.sortOrder  = inAscending;

    if(this.state.sort)
    {
      // remove the old sort status
      this.state.sort.column.cell.className = 
        this.state.sort.column.cell.className.replace
        (/\s*\bsort-(up|down)\b\s*/g, " ");

      this.state.sort.sort = undefined;
    }

    this.state.sort = this.state.columns[inColumn.name];

    // show again from the beginning
    this.startRow = 0;

    if(inAscending)
      this.state.sort.sort = "ascending";
    else
      this.state.sort.sort = "descending";

    // determine the number of the column    
    var column = inColumn.number;

    // sort the array
    if(inAscending == true)
      this.currentRows.sort(function(a, b)
      {
        return compareWithNumber(a[column].sort, b[column].sort);
      });      
    else
      this.currentRows.sort(function(a, b)
      {
        return -compareWithNumber(a[column].sort, b[column].sort);
      });

    // adjust the title to show the sorting
    inColumn.cell.className = 
      inColumn.cell.className.replace(/\s*\bsort-(up|down)\b\s*/g, " ");

    if(inAscending)
      inColumn.cell.className += " sort-up";
    else
      inColumn.cell.className += " sort-down";

    // show the table again
    if(!(inShow == false) && this.showing == false)
      this.showRows(this.currentRows);

    // store the state
    this.state.store();

    // done
    gui.popStatus();

    this.stopTimed("sorting");
  }

  //........................................................................  
  //--------------------------------- left ---------------------------------

  /** Move the column to the left.
   *
   * @param inColumn the the column to move
   * @param inShow   flag if to show table or not
   * 
   */
  this.left = function(inColumn, inShow)
  {
    var right = this.state.columns[inColumn.name];
    var left  = this.state.columns[right.number - 1];
  
    // ignore hidden columns
    while(left.hidden)
      left = this.state.columns[left.number - 1];    

    // swap the numbers
    var swap     = right.number;
    right.number = left.number;
    left.number  = swap;

    this.state.columns[right.number] = right;
    this.state.columns[left.number]  = left;

    if(inShow != false)
      this.show();
  }

  //........................................................................
  //--------------------------------- right --------------------------------

  /** Move the column to the right.
   *
   * @param inColumn the column to move
   * @param inShow   flag if to show table or not
   * 
   */
  this.right = function(inColumn, inShow)
  {
    var left  = this.state.columns[inColumn.name];
    var right = this.state.columns[left.number + 1];
  
    // ignore hidden columns
    while(right.hidden)
      right = this.state.columns[right.number + 1];    

    // swap the numbers
    var swap     = right.number;
    right.number = left.number;
    left.number  = swap;

    this.state.columns[right.number] = right;
    this.state.columns[left.number]  = left;

    if(inShow != false)
      this.show();
  }

  //........................................................................
  //-------------------------------- hasLeft -------------------------------

  /** Determine if the given column has a visible left column.
   *
   * @param inColumn the number of the column to check for
   *
   * @return true if there is a visible left column, false if not
   *
   */
  this.hasLeft = function(inColumn)
  {
    for(var i = inColumn - 1; i >= 0; i--)
      if(!this.state.columns[i].hidden)
        return true;

    return false;
  }

  //........................................................................  
  //-------------------------------- hasRight ------------------------------

  /** Determine if the given column has a visible right column.
   *
   * @param inColumn the number of the column to check for
   *
   * @return true if there is a visible right column, false if not
   *
   */
  this.hasRight = function(inColumn)
  {
    for(var i = inColumn + 1; this.state.columns[i]; i++)
      if(!this.state.columns[i].hidden)
        return true;

    return false;
  }

  //........................................................................  
  //------------------------------- showMenu -------------------------------

  /** Show a context menu.
   *
   * @param inEvent  the event that resulted in this menu
   * @param inColumn the table column this menu is for
   *
   */
  this.showMenu = function(inEvent, inColumn)
  {
    var number = this.state.columns[inColumn.name].number;

    var menu = gui.menu("Sort Ascending", function(inThis) 
      { inThis.parentNode.column.sort(true); return false; }, "sort-ascending",
                        "Sort Descending", function(inThis)
      { inThis.parentNode.column.sort(false); return false; }, 
                        "sort-descending",
                        null, null, null,
                        "Hide", function(inThis) 
      { inThis.parentNode.column.hide(); return false; } , "hide",
                        "Move Left",  
                        inColumn.table.hasLeft(number) ? 
                        function(inThis) 
      { inThis.parentNode.column.left(); return false; } : null, "move-left",
                        "Move Right",  
                        inColumn.table.hasRight(number) ? 
                        function(inThis) 
      { inThis.parentNode.column.left(); return false; } : null, "move-right", 
                        null, null, null,
                        "Search: "
                        + "<input onclick='event.stopPropagation()' "
                        + "name='search' type='text' " 
                        + (this.inplaceSearch ? 
                           "value='" + this.inplaceSearch.source + "'" : "") 
                        + " onkeyup='if(event.which == 13) "
                        + "this.parentNode.onclick()' />",
                        function(inThis)
      {
        var search = inThis.getElementsByTagName("input")[0].value;
        
        // set new search
        inThis.parentNode.column.table.inplaceSearch = new RegExp(search, "i");

        // hide bar
        inThis.parentNode.column.table.bars.style.display = "none";

        // show search 'bar'
        if(!inThis.parentNode.column.table.search)
        {
          inThis.parentNode.column.table.search = 
            document.createElement("div");
          
          inThis.parentNode.column.table.search.className = "-table-search"; 
          
          var label = document.createElement("span");
          
          label.className = "label";
          label.innerHTML = "Search:";

          var text = document.createElement("span");

          text.className = "text";

          inThis.parentNode.column.table.search.appendChild(label);
          inThis.parentNode.column.table.search.appendChild(text);
          
          // add to the page
          inThis.parentNode.column.table.element.parentNode.insertBefore
            (inThis.parentNode.column.table.search, 
             inThis.parentNode.column.table.element);
        }
        
        inThis.parentNode.column.table.search.style.display = "";
        inThis.parentNode.column.table.search.lastChild.innerHTML = search;
        
        // show rows found
        inThis.parentNode.column.table.showRows();

      }, "search", 
                        "All", 
                        this.inplaceSearch ? function(inThis)
      {
        // clear search
        inThis.parentNode.column.table.inplaceSearch = null;
        inThis.parentNode.column.table.currentRows = 
          inThis.parentNode.column.table.currentRows.old;

        // show bar
        inThis.parentNode.column.table.bars.style.display = "";

        // hide search
        inThis.parentNode.column.table.search.style.display = "none";

        // show old rows
        inThis.parentNode.column.table.showRows();
      } : null,                                                
                        "all");

    // store the column to be used in the code
    menu.column = inColumn;

    // show hidden columns
    var first = true;
    for(var i = 0; inColumn.table.state.columns[i]; i++)
      if(inColumn.table.state.columns[i].hidden == true 
         || inColumn.table.state.columns[i].splitHide == true)
      {
        if(first)
        {
          // add a delimiter if we have something to show
          gui.addMenuItem(menu, null, null, null);

          first = false;
        }

        var column = inColumn.table.state.columns[i];
        gui.addMenuItem(menu, "Show " + column.column.title, 
                        function(inThis)
        { inThis.parentNode.column.show(inThis.column); return false; }, 
                        "show").column = column;
      }

    // allow removing of menu by clicking outside
    document._onclick = document.onclick;
    document.onclick  = function(inEvent)
    {
      // remove the menu again
      if(menu.parentNode)
      {
        menu.parentNode.removeChild(menu);
      
        document.onclick = document._onclick;
        document._onclick = undefined;
        
        if(document.onclick)
          document.onclick(inEvent)
      }
    }
    
    return menu;
  }

  //...........................................................................
  //-------------------------------- switch --------------------------------

  /** Switch to some other split up.
   *
   * @param inURL    the url of the page to go, if not this one
   * @param inSplits the split ups to use
   *
   */
  this.rearrange = function(inURL, inSplits)  
  {
    // check if the url says we have to go to another page
    if(!document.URL.match(inURL + "($|\\?)"))
    {
      document.location 
        = "../" + inURL + "?split=" + inSplits.join("&split=");

      return;
    }

    // ok, we can do this internally
    this.state.url = true;

    this.state.splits = new Array();

    for(var i in inSplits)
      this.state.splits.push(this.state.columns[inSplits[i]]);

    // clean up and show the table
    this.state.groups = null;
    this.bars.parentNode.removeChild(this.bars);
    this.show();
  }

  //........................................................................  

  //------------------------------------------------------------------ State

  /* This object stores the current, persistent state of the table. This
   * object contains everything that needs to be persistent.
   *
   * @param inDefaultSplits the splittings to use as default
   *
   * It uses the following structure
   *
   */
  function State(inID, inDefaultSplits, inColumns)
  {
    /* The name of the state. */
    this.name    = "table-" + inID + "-state";

    /* The split columns. */
    this.splits  = new Array();

    /* The columns parsed. */
    this.columns = new Object();
    
    /* The default group to show. */
    this.groups = null;

    /* The column to sort. */
    this.sort = null;
    
    /* The cookie used. */
    this.cookie = null;

    /* A flag if the state was determined from an url, thus not to be stored */
    this.url = false;

    //-------------------------------- load --------------------------------

    /* Load the state from the current cookie. */
    this.load = function(inDefaultSplits, inColumns)
    {
      // the cookie has the following structure
      // {[\<\>][\(][#<split level>#]<column name>[\)]}+
      // \*\*{<default group>::}*<default group>

      var state = 
        document.cookie.match("\\b" + this.name + "=(.*)\\*\\*(.*?)(;|$)");

      if(state)
      {     
        // the cookie read
        this.cookie = state[0];

        // the default group to use
        this.groups = state[2].split("::");
        
        // treat the individual columns
        var columns = state[1].split(/\|/);

        for(var i = 0; i < columns.length; i++)
        {
          var match = columns[i].match(/^(<|>)?(\()?(#.*?#)?(.*?)(\))?$/); 
          
          // some error reading the cookie here...
          if(!match)
          {
            gui.alert("invalid cookie column '" + columns[i] + "', ignored");

            continue;
          }
          
          var column    = new Object();
          column.name   = match[4];
          column.number = i;
          
          // do we have a splitting column?
          if(match[3])
          {
            this.splits[match[3].match(/#(.*)#/)[1]] = column;
            column.split = i;
          }
          
          // any sorting
          if(match[1] == "<")
          {
            this.sort = column;
            column.sort = "ascending";
          }
          else
            if(match[1] == ">")
            {
              this.sort = column;
              column.sort = "descending";
            }
          
          // a hidden column?          
          if(match[2] && match[5])
            column.hidden = true;
          
          // find the table column associated with this
          column.column = inColumns[column.name];

          // flag the column to hide if it is split and may be hidden
          if(column.column.splitHide == true && column.split)
            column.splitHide = true;

          // store it
          this.columns[i]           = column;
          this.columns[column.name] = column;
        }
      }    
      else
        this.initDefault(inDefaultSplits, inColumns);
    }

    //......................................................................
    //-------------------------------- store -------------------------------

    /** Store the current state into a cookie.
     *
     */
    this.store = function()
    {
      // don't store the state if given by url
      if(this.url)
        return;

      var state = "";    

      for(var i = 0; this.columns[i]; i++)
      {
        if(this.columns[i].sort)
          if(this.columns[i].sort == 'ascending')
            state += "<";
          else
            if(this.columns[i].sort == 'descending')
              state += ">";
        
        if(this.columns[i].hidden && this.columns[i].hidden == true)
          state += "(";

        // split groups            
        for(var j in this.splits)
          if(i == this.splits[j].number)
            state += "#" + j + "#";
        
        state += this.columns[i].name;

        if(this.columns[i].hidden && this.columns[i].hidden == true)
          state += ")";

        if(this.columns[i + 1])
          state += "|";
      }    

      // currently selected group
      state += "**" + (this.groups ? this.groups.join("::") : "");

      // store the cookie
      document.cookie = this.name + "=" + state;
  }

  //........................................................................ 
    //------------------------------- reset --------------------------------

    /* Reset the state by resetting the cookie. */
    this.reset = function()
    {
      document.cookie = this.name + "=";
    }

    //...................................................................... 
    //---------------------------- initDefault -----------------------------

    /* Setup the default values for the state. */
    this.initDefault = function(inDefaultSplits, inColumns)
    {      
      // no cookie, thus we make the standard setup
      // determine columns from columns setup in table
      for(var i = 0; inColumns[i]; i++)
      {
        var column    = new Object();        
        
        column.name      = inColumns[i].name;
        column.number    = i;
        column.column    = inColumns[i];

        if(contains(inDefaultSplits, i))
           column.splitHide = inColumns[i].splitHide;
        
        if(column.name.match(/^-/))
          column.hidden = true;
        else
          if(column.name.match(/^\+/))
          {
            column.hidden = true;
            column.multi  = true;
          }
        
        this.columns[i]           = column;
        this.columns[column.name] = column;
      }

      // set the default splits
      for(var i in inDefaultSplits)
        this.splits.push(this.columns[inDefaultSplits[i]]); 
    }

    //......................................................................
    //------------------------------ parseURL ------------------------------

    /* Parse the URL arguments, if any.
     *
     */
    this.parseURL = function(inDefaultSplits, inColumns)
    {
      if(!document.URL.match("\\?(.*)$"))
        return false;
      
      // setup default
      this.initDefault(inDefaultSplits, inColumns)

      this.url = true;

      var arguments = document.URL.match("\\?(.*)$")[1].split("&");

      // if we have splits, remove the default ones
      if(document.URL.match(/\bsplit=/))
        this.splits = new Array();

      for(var i in arguments)
      {
        var keys = arguments[i].split("=");

        var found = false;

        switch(keys[0])
        {
          case "split":
            
            for(var j in this.columns)
              if(j == keys[1])
              {
                this.splits.push(this.columns[j]);

                found = true;
                break;
              }
            
            if(!found)
              gui.alert("invalid column given '" + keys[1] 
                        + "' for splitting");

            break;
            
          case "ascending":
            
            if(this.columns[keys[1]])
              this.columns[keys[1]].sort = "ascending";
            else
              gui.alert("invalid column '" + keys[1] 
                        + "' given for ascended sorting");
            
            break;
            
          case "descending":
                        
            if(this.columns[keys[1]])
              this.columns[keys[1]].sort = "descending";
            else
              gui.alert("invalid column '" + keys[1] 
                        + "' given for descended sorting");
            
            break;
            
          case "hidden":
            
            for(var j in this.columns)
              if(j == keys[1])
              {
                this.columns[this.columns[j].number].hidden = true;
                this.columns[this.columns[j].name].hidden   = true;
                
                found = true;
                break;
              }
            
            if(!found)
              gui.alert("invalid column given '" + keys[1] + "' for hiding");

            break;
            
          case "group":
            
            if(this.groups)
              this.groups = this.groups.concat(keys[1].split("::"));
            else
              this.groups = keys[1].split("::");
            
            break;

          case "search":

            this.search = new RegExp(keys[1], "i");

            break;
            
          default: 
            
            gui.alert("invalid URL argument key '" + keys[0] + "' ignored");
        }
      }
      
      // if we have a search query, we remove all splits
      if(this.search)
        this.splits = new Array();

      return true;
    }  

    //......................................................................
    //------------------------------- debug --------------------------------

    /* Show the state for debugging purposes. */
    this.debug = function()
    {
      var splits = "";
      for(var i in this.splits)
        splits += this.splits[i].name + ", ";

      var sort = "";
      for(var i = 0; this.columns[i]; i++)      
        if(this.columns[i].sort)
        sort = this.columns[i].name + " (" + this.columns[i].sort + "), ";

      var columns = "";
      for(var i = 0; this.columns[i]; i++)
      {
        if(this.columns[i].hidden)
          columns += "(" + this.columns[i].name + ")";
        else
          if(this.columns[i].splitHide)
            columns += "{" + this.columns[i].name + "}";
          else
            columns += this.columns[i].name;

        if(!this.columns[i].column)
          columns += " (undef)";

        columns += ", ";
      }

      return this.name 
      + "<table style='font-size: 75%; font-weight: normal; "
      + "text-align: left;'>\n" 
      + "<tr><td>splits:  </td><td>" + splits + "</td></tr>\n"
      + "<tr><td>group:   </td><td>" 
      + (this.groups ? this.groups.join(", ") : "") + "</td></tr>\n"
      + "<tr><td>sort:    </td><td>" + sort + "</td></tr>\n"
      + "<tr><td>columns: </td><td>" + columns + "</td></tr>\n</table>\n"
      + "<p style='font-size: 60%; text-align: left'>" + this.cookie;
    }

    //......................................................................
    
    // load the current status from the cookie, if there are no url values
    if(!this.parseURL(inDefaultSplits, inColumns))
      this.load(inDefaultSplits, inColumns);

    gui.debug(this.debug());
  }

  //........................................................................
  //------------------------------------------------------------------- Cell

  /** A table cell for cells with a bit more data.
   *
   * @param inText   the text to be placed in the cell
   * @param inGroups the group this cell is to be put into
   * @param inSort   how to sort, if given
   * 
   */ 
   Table.Cell = function(inText, inGroups, inSort)
   {
     // the complete text of the cell
     this.text = inText;

     // the text to use for sorting (without formatting)
     this.sort = inSort;
     
     // the groups given (if any)
     this.groups = null;

     //------------------------------- init --------------------------------

     /* Initialize the cell */
     this.init = function(inGroups)
     {
       var simple = removeHTML(this.text);
     
       if(!this.sort)
         if(typeof this.text == "string")
           this.sort = simple.toLowerCase();
         else
           this.sort = this.text;
     
       if(inGroups && inGroups.length > 0)
         this.groups = inGroups.split("::");
       else  
       {
         // only use the simple group, e.g. the value itself
         this.groups = new Array();
         
         this.groups.push(simple);
       }
     }

     //..................................................................... 

     this.init(inGroups);
   }  

  //........................................................................
  //----------------------------------------------------------------- Column

  /* * The Column object.
   *
   * @param inTable  the table containing the columns
   * @param inName   the name of the column
   * @param inNumber the number of the column
   * @param inTitle  the title of the column
   * @param inImages flag if images should be shown for groups
   * @param inHide   flag if to hide this column if it is grouped
   *
   */
  function Column(inTable, inName, inNumber, inTitle, inImages, inHide)
  {
    this.table     = inTable;
    this.name      = inName;
    this.number    = inNumber;
    this.title     = inTitle;
    this.images    = inImages;
    this.splitHide = inHide;
    
    //-------------------------------- sort --------------------------------

  /** Sort the current table with the given column.
   *
   * @param inAscending true if sorting ascending, false if sorting
   *                    descending
   *
   */
  this.sort = function(inAscending)
  {
    if(inAscending == true || inAscending == false)
      this.table.sort(this, inAscending)
    else    
      this.table.sort(this, 
                      this.table.state.columns[this.name].sort != "ascending");
  }

  //........................................................................
    //-------------------------------- hide --------------------------------

  /** Hide the current column.
   *
   */
  this.hide = function()
  {
    this.table.state.columns[this.name].hidden = true;
    
    this.table.showTitle();
    this.table.showRows(this.table.currentRows);
    this.table.state.store();
  }

  //........................................................................
    //-------------------------------- show --------------------------------

  /** Show the current column.
   *
   */
  this.show = function(inColumn)
  {
    this.table.state.columns[inColumn.name].hidden = undefined;
    this.table.state.columns[inColumn.name].splitHide = undefined;

    this.table.showTitle();
    this.table.showRows(this.table.currentRows);
    this.table.state.store();
  }

  //........................................................................
    //-------------------------------- left --------------------------------

  /** Move the column to the left.
   *
   */
  this.left = function()
  {
    this.table.left(this);
  }

  //........................................................................
    //-------------------------------- right -------------------------------

  /** Move the column to the left.
   *
   */
  this.right = function()
  {
    this.table.right(this);
  }

  //........................................................................
  }

  //........................................................................  
  //--------------------------------------------------------------- Category

  /** This class represents a category when grouping values.
   *
   * @param inTable     the table this category is used in
   * @param inName      the name of the category
   * @param inParent    the parent category, if any
   * @param inImagePath the path to images to be used for bars
   * @param inSplits    the splits to use for this and sub categories
   * @param inLevel     the level of splits of this category
   * @param inDefault   the default setup
   *
   */
  function Category(inTable, inName, inParent, inImagePath, inSplits, inLevel, 
                    inDefault)
  {
    this.table      = inTable;
    this.name       = inName;
    this.parent     = inParent;
    this.rows       = new Array();
    this.splits     = inSplits;
    this.images     = inImagePath;
    this.names      = new Array;
    this.groups     = new Object;
    this.categories = new Object;
    this.current    = null;
    this.root       = inParent;
    this.hasGroup   = false;
    this.bar        = null;

    //-------------------------------- init --------------------------------

    /** Initialize the category.
     *
     */
    this.init = function(inLevel, inDefault)
    {
      // find the root category and the storage for the current bar
      if(this.parent == null)
      {
        // we are in the root
        this.root       = this;
        this.currentBar = new Object();

        // setup the root bar properties
        this.rootBar = document.createElement("div");
        
        this.rootBar.className     = "-table-bars";

      }
      else
        for(this.root = this.parent; this.root.parent; 
            this.root = this.root.parent)
          ;

      // create the bar for this level
      this.bar = document.createElement("div");
      
      this.bar.className        = "-table-groups";
      this.bar.style.whiteSpace = "nowrap";
      
      // add scrolling (after displaying everything to get sizes right)      
      var bar = this.bar;

      this.bar        = gui.scroll(this.bar, 5, 20);
      this.bar.groups = bar;
      this.bar.scroll = bar;      
      
      // hide all bars except the top level one
      if(this.parent)
        this.bar.style.display = "none";

      // add a remove icon for removing the bar
      var remove = document.createElement("div");
      
      remove.className = "-table-remove";
      remove.table     = this.table;
      remove.category  = this.category;
      remove.category  = this;
      
      remove.onclick   = function()
      {
        // compute the new split state
        this.table.state.columns[this.category.splits[0].number].splitHide = 
          undefined;

        this.table.state.splits = 
          this.category.root.splits.slice(0, -this.category.splits.length)
            .concat(this.category.splits.slice(1));
        
        this.table.state.groups = null;
        this.table.bars.parentNode.removeChild(this.table.bars);
        this.table.showTitle();
        this.table.show();
      }
      
      this.bar.appendChild(remove);

      if(!this.parent)
      {
        this.root.rootBar.appendChild(this.bar);
      }

      // setup the default
      if(inDefault && inDefault.length)
      {
        if(inDefault[0].match(/%%/))
        {
          var groups = inDefault[0].split(/%%/);
          
          // setup the new groups
          if(!this.groups[groups[0]])
            this.groups[groups[0]] = new MultiGroup(this, groups[0], inLevel);
          
          // create the name as default
          if(!this.groups[groups[0]].groups[groups[1]])
            this.groups[groups[0]].groups[groups[1]] = new Array();
        }
        else
          this.addGroup(new Group(this, inDefault[0], inLevel));          

        // now continue to add to the sub categories
        if(this.splits && this.splits.length && this.splits.length > 1)
        {
          // create a new category if necessary
          var column = this.splits[1].column;

          // create a new category if necessary
          if(!this.categories[inDefault[0]])
          {
            this.categories[inDefault[0]] = 
              new Category(this.table, inDefault[0] + "::" + column.name,
                           this, column.images ? 
                           this.table.imagePath + "/" + column.images : null, 
                           this.splits.slice(1), inLevel + 1, 
                           inDefault.slice(1));

            this.rootBar.appendChild(this.categories[inDefault[0]].bar); 
          }
        }
      }
    }

    //......................................................................
    //------------------------------ addGroup ------------------------------

    /** Add the given group to the display.
     *
     * @param inGroup the group to add
     *
     */
    this.addGroup = function(inGroup)
    {
      // fix empty group names
      if(!inGroup.name || inGroup.name == "")
        inGroup.name = "_";

      this.groups[inGroup.name] = inGroup;

      var before = this.addName(inGroup.name);

      if(before)
        this.groups[before].node.parentNode.insertBefore
          (this.groups[inGroup.name].node, this.groups[before].node);
      else
        this.bar.groups.appendChild(this.groups[inGroup.name].node)
    }

    //......................................................................
    //-------------------------------- add ---------------------------------
    
    /** Add a table row to this category.
     *
     * @param inName  the name (group) of the row to add
     * @param inRow   the row data
     * @param inLevel the level we are adding to
     *
     */
    this.add = function(inName, inRow, inLevel, inSplits, inCheckMulti)
    {
      // add to the category
      this.rows.push(inRow);

      if(!inCheckMulti && inName[0].match(/\|/) 
         || (inName.length > 1 && inName[1].match(/\|/)))
      {
        var names = inName.join("::").split(/\|/);

        // ignore the last entry, as it will always be empty
        for(var i = 0; i < names.length - 1; i++)
          this.add(names[i].split("::"), inRow, inLevel, inSplits, true);

        return;
      }

      if(!this.groups[inName[0]])
        this.addGroup(new Group(this, inName[0], inLevel));

      if(!inName[0] || inName[0] == "")
        this.groups["_"].add(inRow);
      else
        this.groups[inName[0]].add(inRow);

      // now continue to add to the sub categories
      if(inSplits && inSplits.length && inSplits.length > 0)
      {
        if(!this.categories)
          this.categories = new Object();

        var column = inSplits[0].column;

        // create a new category if necessary
        if(!this.categories[inName[0]])
        {
          this.categories[inName[0]] = 
            new Category(this.table, inName[0] + "::" + column.name,
                         this, column.images ? 
                         this.table.imagePath + "/" + column.images : null, 
                         inSplits.slice(1), inLevel + 1, null);

          this.bar.appendChild(this.categories[inName[0]].bar);          
        }

        this.categories[inName[0]].add
          (inRow[inSplits[0].column.number].groups, inRow, inLevel + 1, 
           inSplits.slice(1));
      }
    }
  
    //......................................................................
    //------------------------------- select -------------------------------

    /** Select this category for displaying.
     *
     * @param  inGroups the specific groups to select, if any
     *
     * @return the rows selected
     *
     */
    this.select = function(inGroups)
    {
      // clear the state (will be filled by Group.select())
      this.table.state.groups = new Array();

      if(inGroups)
      {
        var group;
        if(inGroups[0].match(/%%/))
        {
          var groups = inGroups[0].split(/%%/);
          
          group = this.groups[groups[0]][groups[1]];
        }
        else
          group = this.groups[inGroups[0]];

        if(inGroups.length > 1)
          return group.select(inGroups.slice(1));
        else
          return group.select();
      }
      else
      {
        // select the default (e.g. first) group
        if(this.groups[this.names[0]].select)
          return this.groups[this.names[0]].select();        
        else
        {
          // we have a grouped group
          var grouped = this.groups[this.names[0]];

          return grouped[names(grouped)[0]].select();
        }
      }
    }

    //......................................................................  
    //------------------------------ addName -------------------------------

    /** Add a name to the group of names.
     *
     * @param inName the name to add
     *
     */
    this.addName = function(inName)
    {
      for(var i in this.names)
      {
        if(inName == this.names[i])
          return null;        

        // undefined comes last
        if(inName == "$undefined$")
          continue;

        if(this.names[i] == "$undefined$" 
           || compareWithNumber(inName, this.names[i]) < 0)
        {
          var name = this.names[i];
          
          this.names.splice(i, 0, inName);

          // return the element after the current one
          return name;
        }
      }

      // not yet found...
      this.names.push(inName);

      return null;
    }

    //...................................................................... 

    //---------------------------------------------------------------- Group

    /** This is the class for an individual group in a category.
     *
     * @param inCategory the category this groups belongs to
     * @param inName     the name of the group
     * @param inLevel    the level this group is in
     * 
     */ 
    function Group(inCategory, inName, inLevel)
    {
      this.category = inCategory;
      this.name     = inName;
      this.rows     = new Array();
      this.level    = inLevel;

      this.node     = null;
      this.sub      = null;
      
      //------------------------------- init -------------------------------

      /** Initialize the group. */
      this.init = function()
      {
        // setup the display, e.g. the group picture

        // remove a leading sorting qualifier
        var name = this.name.replace(/^.*?--/, "");
        
        if(this.category.images)
        {          
          this.node = document.createElement("img");
          
          this.node.src   = this.category.images + "/" + name + ".png";
          this.node.title = name;
          this.node.alt   = name;
          
          this.node.onmouseover = function() { gui.iconHighlight(this); };
          this.node.onmouseout  = function() { gui.iconNormal(this);    };
        }
        else
        {
          this.node = document.createElement("span");
          
          this.node.className = "-table-tab";
          this.node.innerHTML = name;         
        }
        
        this.node.group   = this;
        this.node.bar     = this.category.bar;
        this.node.onclick = function() 
        { 
          // select the appropriate group
          this.group.select();
          
          return false; 
        };         
      }

      //.................................................................... 
      //------------------------------ select ------------------------------

      /** Select this specific group.
       *
       * @param  inGroups the names of the sub-groups to select
       *
       * @return the rows selected now
       *
       */
      this.select = function(inGroups)
      {
        // increase the height of the bar to keep space for the grouping
        var bar = this.node.bar;

        // make sure the bar is show
        bar.style.display = "block";

        if(this.category.hasGroup && this.node.bar.element.style.height == "")
        {
          var f   = function()
          { 
            if(bar.element.offsetHeight != 0)
            {
              bar.element.style.height = bar.element.offsetHeight + 8 + "px"; 
              gui.checkScrolling(bar.scroll);            
            }
            else
              gui.delayed(f, 250);
          };
          
          gui.delayed(f, 0);
        }
        else
          gui.delayed(function() { gui.checkScrolling(bar.scroll) }, 0);
        
        // store the current group
        this.category.current = this;
        
        // mark the current node as selected and the last as unselected
        if(this.node.tagName == 'IMG')
        {
          // an image node
          if(this.node.bar.highlight)
            gui.iconNormal(this.node.bar.highlight);

          gui.iconHighlight(this.node);
        }
        else
        {
          // a non-image (probably text) node
          if(this.node.bar.highlight)
            this.node.bar.highlight.className = "-table-tab";

          this.node.className = "-table-tab-selected -table-tab";
        }      
        
        this.node.bar.highlight = this.node;
        
        // open a group if this is in a group
        if(this.name.match("%%"))
          this.node.parentNode.previousSibling.onclick();

        if(this.category.parent)
        {
          // make sure the bar is shown and the old one not
          if(this.category.root.currentBar[this.level])
            this.category.root.currentBar[this.level].style.display = "none";

          this.category.root.currentBar[this.level] = this.node.bar;

          this.node.bar.style.display = "";
        }
        
        // store the current group in the state
        this.category.table.state.groups[this.level] = this.name;

        var sub = this.category.categories[this.name];

        if(this.category.categories && sub && sub.groups 
           && sub.names && sub.names.length > 0)
        {
          if(inGroups && inGroups.length > 0)
          {            
            // do we have grouped groups?
            if(sub.groups[inGroups[0]])
              return sub.groups[inGroups[0]].select(inGroups.slice(1));
            else
            {
              var parts = inGroups[0].split(/%%/);
              
              return sub.groups[parts[0]][parts[1]].select(inGroups.slice(1));
            }
          }
          else
          {
            // select the default (e.g. first) group
            
            // do we have a group of groups?
            if(sub.groups[sub.names[0]].name)
              return sub.groups[sub.names[0]].select();
            else
            {
              var name = names(sub.groups[sub.names[0]])[0];
              
              return sub.groups[sub.names[0]][name].select();
            }
          }
        }
        else
          // select the rows 
          this.category.table.showRows(this.rows);
        
        // store the table state (might have been called from group directly)
        this.category.table.state.store();   

        // return the rows
        return this.rows;
      }

      //....................................................................   
      //--------------------------- createGroup ----------------------------

      /** Create a group image or text for this group.
       *
       * @param inBar  the bar to put the group into
       * @param inPath the path to pictures to use
       *
       * @return the HTML node created, NOT added to the bar
       * 
       */ 
      this.createGroup = function(inBar, inPath)
      {
        return this.node; 
        // remove group identifiers
        var name = this.name.replace(/^.*?%%/, "");
        
        // remove ordering identifiers
        name  = name.replace(/^.*?--/, "");
        
        var node;
        
        if(inPath)
        {          
          node = document.createElement("img");
          
          node.src   = inPath + "/" + name + ".png";
          node.title = name;
          node.alt   = name;
          
          node.onmouseover = function() { gui.iconHighlight(this); };
          node.onmouseout  = function() { gui.iconNormal(this);    };
        }
        else
        {
          node = document.createElement("span");
          
          node.className = "-table-tab";
          node.innerHTML = name;         
        }
        
        node.group     = this;
        this.node      = node;
        node.bar       = inBar;
        node.onclick   = function() 
        { 
          // select the appropriate group
          this.group.select();
          
          return false; 
        };
        
        return node;
      }  

      //....................................................................
      //------------------------------- add --------------------------------

      /** Add a row to this group.
       *
       * @param inRow the row to add
       *
       */
      this.add = function(inRow)
      {
        // do we add to the currently displayed rows?
        if(this.category.table.currentRows == this.rows)
          this.category.table.insertRow(inRow);
        else
          // simply add to the list        
          this.rows.push(inRow);        
      }

      //.................................................................... 

      this.init();
    }

    Group.getClassName = function()
    {
      return this.toString().match(/function\s*(\w+)/)[1];
    };

    //.......................................................................
    //----------------------------------------------------------- MultiGroup

    function MultiGroup(inCategory, inName, inLevel)
    {
      this.groups = new Object();

      //------------------------------- add --------------------------------

      /** Add a row to this group.
       *
       * @param inRow  the row to add
       * @param inName the name to add for
       *
       */
      this.add = function(inRow, inName)
      {
        // add to the list
        if(!this.groups[inName])
          this.groups[inName] = new Array();

        this.groups[inName].rows.push(inRow);
      }

      //.................................................................... 
    }

    //----------------------------- derivation -----------------------------

    MultiGroup.inherit = function(superClass)
    {
      var tmpClass = function() {};
      
      tmpClass.prototype        = superClass.prototype;
      this.prototype            = new tmpClass;
      var className             = superClass.getClassName();
      this.prototype[className] = superClass;
    };

    MultiGroup.getClassName = function()
    {
      return this.toString().match(/function\s*(\w+)/)[1];
    };

    //......................................................................

    MultiGroup.inherit(Group);

    //...................................................................... 

    // initialize the category
    this.init(inLevel, inDefault);
  }

  //........................................................................  

  // initialize the table, if not yet done
  this.init();
}

// make sure the table css is loaded
gui.delayed(function () { gui.includeCSS("/css/table.css"); }, 0);
