// Copyright (C) 2007-2012 Bristle Software, Inc.
// 
// This program 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 1, or (at your option)
// any later version.
// 
// This program 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 this program; if not, write to the Free Software
// Foundation, Inc.

/******************************************************************************
* com.bristle.jslib.Table.js
*******************************************************************************
* Purpose:
*       This file contains utility routines that manipulate HTML tables.
* Usage:
*       - The typical scenario for using this file from an HTML file is:
*         <script language='JavaScript' src='com.bristle.jslib.Table.js'></script>
*         Call the various functions that reside here.
* Assumptions:
*       - The file "com.bristle.jslib.Util.js" has already been loaded.
* Effects:
*       - None.
* Anticipated Changes:
* Notes:
* Implementation Notes:
* Portability Issues:
* Revision History:
*   $Log$
******************************************************************************/

// Create the "namespace" to hold the functions in this file.
com.bristle.jslib.Table = {};

/******************************************************************************
* Get the count of TRs in the specified TBODY.
*??Could probably rewrite to use tbody.rows.length, which is now supported
*??by all browsers.
*
*@param tbody The TBODY to count.
******************************************************************************/
com.bristle.jslib.Table.getTRCount =
function(tbody)
{
    if (!com.bristle.jslib.Util.isAnHTMLElement(tbody, "TBODY"))
    {
        throw new com.bristle.jslib.Exception.Exception
            (com.bristle.jslib.Exception.intEXC_NOT_AN_HTML_TBODY_ELEMENT
            ,"The specified object is not an HTML TBODY element"
            ,"com.bristle.jslib.Table.getTRCount"
            );
    }

    var intTRCount = 0;
    var intNodeCount = tbody.childNodes.length;
    for (var i = 0; i < intNodeCount; i++)
    {
        if (tbody.childNodes[i].nodeName.toUpperCase() == "TR")
        {
            intTRCount++;
        }
    }
    return intTRCount;
}

/******************************************************************************
* Return the 0-based column index of the specified cell (HTML TH or TD element) 
* in its enclosing HTML table
*
*@param thCell   The TH or TD element
******************************************************************************/
com.bristle.jslib.Table.getColumnIndexByCell =
function(thCell)
{
    if (!com.bristle.jslib.Util.isAnHTMLElement(thCell, "TH")
        &&
        !com.bristle.jslib.Util.isAnHTMLElement(thCell, "TD"))
    {
        throw new com.bristle.jslib.Exception.Exception
            (com.bristle.jslib.Exception.intEXC_NOT_AN_HTML_TH_OR_TD_ELEMENT
            ,"The specified object is not an HTML TH or TD element"
            ,"com.bristle.jslib.Table.getColumnIndexByCell"
            );
    }

    var trRow = com.bristle.jslib.Util.getParentElement(thCell, "TR");
    var arrCols = trRow.cells; 
    var intColCount = arrCols.length; 
    var intCol = 0;
    while (intCol < intColCount && thCell != arrCols[intCol]) intCol++;
    return intCol;
}

/******************************************************************************
* Sort the rows of the specified HTML table body by the value of the 
* specified column, using the optionally specified compare function, and
* optionally reversing the order specified by the specified or default
* compare function.
*
*@param tbody         The TBODY containing the rows to sort
*@param intCol        The 0-based index of the column to sort by
*@param funcCompare   The function to call for comparing 2 strings when sorting.
*                     Must take 2 string params, and return 0 for 1st = 2nd, 
*                     -1 for 1st < 2nd, and 1 for 1st > 2nd.
*                     Optional, defaulting to alphabetic order.
*@param blnReverse    True or false, specifying whether to reverse the sort
*                     order.
*                     Optional.  Default = false
*@param blnIgnoreCase True or false, specifying whether to ignore upper/lower
*                     case distinctions when sorting via the default compare 
*                     function.
*                     Optional.  Default: true
*@param blnTrimWhitespace
*                     True or false, specifying whether to ignore leading
*                     and trailing whitespace when sorting via the default 
*                     compare function.
*                     Optional.  Default: true
******************************************************************************/
com.bristle.jslib.Table.blnDEFAULT_COMPARE_FUNCTION = null;
com.bristle.jslib.Table.blnREVERSE_SORT_ORDER       = true;
com.bristle.jslib.Table.blnIGNORE_CASE              = true;
com.bristle.jslib.Table.blnTRIM_WHITESPACE          = true;
com.bristle.jslib.Table.sortTableByColumnIndex =
function(tbody
        ,intCol
        ,funcCompare
        ,blnReverse
        ,blnIgnoreCase
        ,blnTrimWhitespace
        )
{
    if (!com.bristle.jslib.Util.isAnHTMLElement(tbody, "TBODY"))
    {
        throw new com.bristle.jslib.Exception.Exception
            (com.bristle.jslib.Exception.intEXC_NOT_AN_HTML_TBODY_ELEMENT
            ,"The specified object is not an HTML TBODY element"
            ,"com.bristle.jslib.Table.sortTableByColumnIndex"
            );
    }

    var arrRows = tbody.rows;
    var intRowCount = arrRows.length;
    if (intRowCount == 0) return;
    
    // Create an Array of TR elements for ease of sorting by the built-in
    // Array.sort().  Each Array element is another Array, containing the 
    // contents of the specified column and a reference to the original TR.
    var arrSortableRows = new Array();
    for (var i = 0; i < intRowCount; i++)
    {
        var trRow   = arrRows[i];
        var arrCols = trRow.cells; 
        var tdCol = (intCol >= arrCols.length) 
                    ? null 
                    : arrCols[intCol];
        var strSortKey = (tdCol == null) 
                         ? "" 
                         : com.bristle.jslib.Util.getTextContent(tdCol);
        arrSortableRows[i] = new Array (strSortKey, trRow);
    }

    if (com.bristle.jslib.Util.isMissingNullUndefinedOrEmptyString(blnIgnoreCase))
    {
        blnIgnoreCase = true;
    }

    if (com.bristle.jslib.Util.isMissingNullUndefinedOrEmptyString(funcCompare))
    {
        funcCompare = 
        function (str1, str2)
        {
            if (blnIgnoreCase)
            {
                str1 = str1.toUpperCase();
                str2 = str2.toUpperCase();
            }
            if (blnTrimWhitespace)
            {
                str1 = com.bristle.jslib.Util.trimAllWhitespace(str1);
                str2 = com.bristle.jslib.Util.trimAllWhitespace(str2);
            }
            var intRC = (str1 == str2 
                         ? 0
                         : str1 < str2 
                           ? -1
                           : 1
                        );
            return intRC; 
        };
    }
    else if (typeof(funcCompare) == "function")
    {
        // Nothing to do.  Use specified function.
    }
    else
    {
        throw new com.bristle.jslib.Exception.Exception
            (com.bristle.jslib.Exception.intEXC_NOT_A_FUNCTION
            ,"The specified object is not a JavaScript function"
            ,"com.bristle.jslib.Table.sortTableByColumnIndex"
            );
    }
    
    var funcNonRecursiveCompare = funcCompare;
    if (blnReverse)
    {        
        // Note: Can't refer to funcCompare within itself.  Even in a 
        //       "closure", this is an infinite recursive loop.
        funcNonRecursiveCompare =
        function (str1, str2)
        {
            return (- funcCompare(str1,str2)); 
        };
    }
    
    // Sort arrSortableRows by comparing the first element of each nested 
    // Array, which is the content of the specified column for that row.
    var funcArrayElementCompare = 
    function (sortableRow1, sortableRow2)
    {
        return (funcNonRecursiveCompare(sortableRow1[0], sortableRow2[0])); 
    };
    arrSortableRows.sort(funcArrayElementCompare); 

    // Arrange the table rows to match the sorted array.
    for (var i = 0; i < intRowCount; i++)
    {
        // Note: appendChild() moves the row from its current location in
        //       the table to the end of the table.
        tbody.appendChild(arrSortableRows[i][1]);
    }
}

/******************************************************************************
* Sort the rows of the first HTML table body of the HTML table that contains 
* the specified cell (HTML TH or TD element), ordering by the values of the 
* table cells in each row that are in the same column as the specified cell,
* using the optionally specified compare function, and optionally reversing 
* the order specified by the specified or default compare function.
* Optionally, also manages the sort direction icons displayed in the column
* headers, toggling the sort direction of the column and updating all icons.
*
* Assumptions:
* - The table contains only simple columns.  Using different patterns of 
*   the COLSPAN property on TD or TH elements in different rows may cause 
*   unexpected results.
* - Operates on the smallest enclosing table.  No support for sorting 
*   outer ancestor tables.
* - Operates on the first TBODY of the table.  No support for sorting other
*   TBODY elements of the table.
* - When managing sort direction icons of the column headers, assumes that
*   the IMG element for the icon is the first IMG child element of the TH
*   or TD element.  No support for treating later IMG child elements or 
*   more deeply nested IMG elements as the sort direction icon.*
*
*@param thCell        The TH or TD element
*@param funcCompare   The function to call for comparing 2 strings when sorting.
*                     Must take 2 string params, and return 0 for 1st = 2nd, 
*                     -1 for 1st < 2nd, and 1 for 1st > 2nd.
*                     Optional, defaulting to alphabetic order.
*@param blnReverse    True or false, specifying whether to reverse the sort
*                     order.  
*                     If not specified, the values of strUrlForward, 
*                     strUrlReverse, and strUrlNoSort are required.  In that
*                     case, they are used to determine which column, if any, 
*                     the table is currently sorted by.  If the table is 
*                     currently sorted by the specified column, the sort 
*                     order is reversed.  Otherwise, the table is sorted 
*                     forward by the column.  After any change in sort 
*                     order, the sort direction icons of all columns are 
*                     updated to reflect the current status, with one icon 
*                     showing the strUrlForward or strUrlReverse image, and 
*                     all others showing the strUrlNoSort image.
*@param blnIgnoreCase True or false, specifying whether to ignore upper/lower
*                     case distinctions when sorting via the default compare 
*                     function.
*                     Optional.  Default: true
*@param blnTrimWhitespace
*                     True or false, specifying whether to ignore leading
*                     and trailing whitespace when sorting via the default 
*                     compare function.
*                     Optional.  Default: true
*@param strUrlForward URL of image to show in a sort direction icon when the 
*                     table is sorted in forward order by that column.
*@param strUrlReverse URL of image to show in a sort direction icon when the 
*                     table is sorted in reverse order by that column.
*@param strUrlNoSort  URL of image to show in a sort direction icon when the 
*                     table is not sorted by that column.
******************************************************************************/
com.bristle.jslib.Table.blnUNSPECIFIED_SORT_ORDER = null;
com.bristle.jslib.Table.sortTableByColumnCell =
function(thCell
        ,funcCompare
        ,blnReverse
        ,blnIgnoreCase
        ,blnTrimWhitespace
        ,strUrlForward
        ,strUrlReverse
        ,strUrlNoSort
        )
{
    if (!com.bristle.jslib.Util.isAnHTMLElement(thCell, "TH")
        &&
        !com.bristle.jslib.Util.isAnHTMLElement(thCell, "TD"))
    {
        throw new com.bristle.jslib.Exception.Exception
            (com.bristle.jslib.Exception.intEXC_NOT_AN_HTML_TH_OR_TD_ELEMENT
            ,"The specified object is not an HTML TH or TD element"
            ,"com.bristle.jslib.Table.sortTableByColumnCell"
            );
    }

    var tableToSort = com.bristle.jslib.Util.getParentElement(thCell, "TABLE");
    var tbodyToSort = tableToSort.tBodies[0];

    var intCol = com.bristle.jslib.Table.getColumnIndexByCell(thCell);

    if (com.bristle.jslib.Util.isMissingNullUndefinedOrEmptyString(blnReverse))
    {
        var imgDirectionIcon 
            = com.bristle.jslib.Util.getFirstChildElement(thCell, "IMG");
        if (imgDirectionIcon == null)
        {
            blnReverse = false;
        }
        else
        {
            blnReverse = com.bristle.jslib.Util.endsWith
                                (imgDirectionIcon.src, strUrlForward);
            imgDirectionIcon.src = blnReverse ? strUrlReverse : strUrlForward;
            var trRow = com.bristle.jslib.Util.getParentElement(thCell, "TR");
            for (var cell = com.bristle.jslib.Util.getFirstChildElement
                                                                (trRow, "TH");
                 cell != null;
                 cell = com.bristle.jslib.Util.getNextSiblingElement
                                                                (cell, "TH")
                )
            {
                if (cell != thCell)
                {
                    var img = com.bristle.jslib.Util.getFirstChildElement
                                                                (cell, "IMG");
                    if (   img != null
                        && !com.bristle.jslib.Util.endsWith
                                                (img.src, strUrlNoSort)
                       )
                    {
                        img.src = strUrlNoSort;
                    }
                }
            }
            for (var cell = com.bristle.jslib.Util.getFirstChildElement
                                                                (trRow, "TD");
                 cell != null;
                 cell = com.bristle.jslib.Util.getNextSiblingElement(cell, "TD")
                )
            {
                if (cell != thCell)
                {
                    var img = com.bristle.jslib.Util.getFirstChildElement
                                                                (cell, "IMG");
                    if (   img != null
                        && !com.bristle.jslib.Util.endsWith
                                                (img.src, strUrlNoSort)
                       )
                    {
                        img.src = strUrlNoSort;
                    }
                }
            }
        }
    }
    com.bristle.jslib.Table.sortTableByColumnIndex
                (tbodyToSort
                ,intCol
                ,funcCompare
                ,blnReverse
                ,blnIgnoreCase
                ,blnTrimWhitespace);
}

