/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 * 
 *  http://www.apache.org/licenses/LICENSE-2.0
 * 
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package org.apache.myfaces.trinidadinternal.renderkit.core.xhtml;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;

import org.apache.myfaces.trinidad.bean.FacesBean;
import org.apache.myfaces.trinidad.bean.PropertyKey;
import org.apache.myfaces.trinidad.component.UIXCollection;
import org.apache.myfaces.trinidad.component.core.data.CoreColumn;
import org.apache.myfaces.trinidad.logging.TrinidadLogger;
import org.apache.myfaces.trinidad.model.SortCriterion;
import org.apache.myfaces.trinidadinternal.agent.TrinidadAgent;
import org.apache.myfaces.trinidad.context.FormData;
import org.apache.myfaces.trinidad.context.RenderingContext;
import org.apache.myfaces.trinidadinternal.renderkit.core.xhtml.table.CellUtils;
import org.apache.myfaces.trinidadinternal.renderkit.core.xhtml.table.ColumnData;
import org.apache.myfaces.trinidadinternal.renderkit.core.xhtml.table.RenderStage;
import org.apache.myfaces.trinidadinternal.renderkit.core.xhtml.table.TableRenderingContext;
import org.apache.myfaces.trinidad.skin.Icon;
import org.apache.myfaces.trinidad.util.IntegerUtils;

/**
 * @todo Kill the now-strange "compute mode", since we can
 *       just iterate over the things.
 * @todo Support abbreviation!!!  Missing from Trinidad so far.
 * @todo Support required?  Support messageType?
 */
public class ColumnGroupRenderer extends XhtmlRenderer
{
  protected static final int SORT_NO         = 0;
  protected static final int SORT_SORTABLE   = 1;
  protected static final int SORT_ASCENDING  = 2;
  protected static final int SORT_DESCENDING = 3;

  public ColumnGroupRenderer()
  {
    super(CoreColumn.TYPE);
  }

  @Override
  protected void findTypeConstants(FacesBean.Type type)
  {
    super.findTypeConstants(type);
    _headerTextKey = type.findKey("headerText");
    _headerNoWrapKey = type.findKey("headerNoWrap");
    _noWrapKey = type.findKey("noWrap");
    _separateRowsKey = type.findKey("separateRows");
    _rowHeaderKey = type.findKey("rowHeader");
    _alignKey = type.findKey("align");
    _widthKey = type.findKey("width");
    _sortableKey = type.findKey("sortable");
    _sortPropertyKey = type.findKey("sortProperty");
    _defaultSortOrderKey = type.findKey("defaultSortOrder");
  }

  @Override
  public boolean getRendersChildren()
  {
    return true;
  }

  protected String getHeaderText(FacesBean bean)
  {
    return toString(bean.getProperty(_headerTextKey));
  }

  protected boolean getHeaderNoWrap(FacesBean bean)
  {
    Object o = bean.getProperty(_headerNoWrapKey);
    if (o == null)
      o = _headerNoWrapKey.getDefault();

    return Boolean.TRUE.equals(o);
  }

  protected boolean getNoWrap(FacesBean bean)
  {
    Object o = bean.getProperty(_noWrapKey);
    if (o == null)
      o = _noWrapKey.getDefault();

    return Boolean.TRUE.equals(o);
  }


  protected boolean getRowHeader(FacesBean bean)
  {
    Object o = bean.getProperty(_rowHeaderKey);
    if (o == null)
      o = _rowHeaderKey.getDefault();

    return Boolean.TRUE.equals(o);
  }


  protected boolean getSeparateRows(FacesBean bean)
  {
    Object o = bean.getProperty(_separateRowsKey);
    if (o == null)
      o = _separateRowsKey.getDefault();

    return Boolean.TRUE.equals(o);
  }

  protected String getWidth(FacesBean bean)
  {
    return toString(bean.getProperty(_widthKey));
  }

  protected String getFormatType(FacesBean bean)
  {
    return toString(bean.getProperty(_alignKey));
  }


  protected boolean getSortable(FacesBean bean)
  {
    Object o = bean.getProperty(_sortableKey);
    if (o == null)
      o = _sortableKey.getDefault();

    return !Boolean.FALSE.equals(o);
  }


  protected String getSortProperty(FacesBean bean)
  {
    return toString(bean.getProperty(_sortPropertyKey));
  }

  protected String getDefaultSortOrder(FacesBean bean)
  {
    if (_defaultSortOrderKey == null)
      return null;

    return toString(bean.getProperty(_defaultSortOrderKey));
  }


  static public String getDefaultHeaderStyleClass(TableRenderingContext tContext)
  {
    return ColumnData.selectFormat(tContext,
                                   SkinSelectors.AF_COLUMN_HEADER_TEXT_STYLE,
                                   SkinSelectors.AF_COLUMN_HEADER_NUMBER_STYLE,
                                   SkinSelectors.AF_COLUMN_HEADER_ICON_STYLE);
  }

  /**
   * @param tContext the column is identified by the logicalColumnIndex on
   * this context.
   * @return the CSS style class to use for a column header. This will be
   * left-aligned for text, right-aligned for numbers and center-aligned
   * for icons.
   */
  protected String getHeaderStyleClass(TableRenderingContext tContext)
  {
    return getDefaultHeaderStyleClass(tContext);
  }

  /**
   * @todo Will need to support TREE_NODE_STAGE
   */
  @Override
  protected void encodeAll(
    FacesContext        context,
    RenderingContext arc,
    UIComponent         component,
    FacesBean           bean) throws IOException
  {
    TableRenderingContext tContext =
      TableRenderingContext.getCurrentInstance();

    RenderStage rs = tContext.getRenderStage();
    int stage = rs.getStage();
    switch (stage)
    {
    case RenderStage.INITIAL_STAGE:
      _computeMode(context, tContext, component);
      break;
    case RenderStage.COLUMN_HEADER_STAGE:
      _renderHeaderMode(context, arc, tContext, component);
      break;
    // For these stages, simply render the children; we
    // need no special processing at the column group level
    case RenderStage.DATA_STAGE:
    case RenderStage.START_ROW_STAGE:
    case RenderStage.COLUMN_FOOTER_STAGE:
      _renderChildren(context, component, null /*parentNode*/);
      break;
    case RenderStage.END_STAGE:
      // Do nothing.  =-=AEW This is only legit because we happen
      // to know that ColumnRenderer does nothing here too.
      break;
    default:
      throw new AssertionError("Bad renderStage:"+stage);
    }
  }

  private void _renderHeaderMode(
    FacesContext        context,
    RenderingContext arc,
    TableRenderingContext tContext,
    UIComponent           column) throws IOException
  {
    final NodeData parentNode = getParentNode(tContext);
    final boolean areWeRoot;
    final NodeData currentNode;

    if (parentNode == null)
    {
      areWeRoot = true;
      currentNode = _getNodeList(tContext, false).getNext();
    }
    else
    {
      areWeRoot = false;
      currentNode = parentNode.get(parentNode.currentChild);
    }

    final ColumnData colData = tContext.getColumnData();
    int row = colData.getRowIndex();
    int waitUntilRow = currentNode.waitUntilRow;
    // check to see if this columnGroup's header has been rendered:
    if (waitUntilRow==0)
    {
      // this columnGroup's header has not been rendered. So we will render it
      // and skip rendering the headers of our children.
      int totalRows = colData.getHeaderRowSpan();
      int rowSpan = (totalRows - row) - currentNode.rows + 1;

      // This columnGroup may have a rowSpan > 1. So we need to indicate on
      // which row our children will start rendering:
      currentNode.waitUntilRow = rowSpan + row;

      String headerID = _renderColumnHeader(context, arc, tContext, column,
                                            rowSpan, currentNode.cols);
      if (headerID != null)
      {
        if (areWeRoot)
        {
          currentNode.headerIDs = headerID;
        }
        else
        {
          currentNode.headerIDs = parentNode.headerIDs+" "+headerID;
        }
      }
    }
    else if (row >= waitUntilRow)
    {
      // this columnGroup's header has already been rendered. And we have
      // skipped as many rows as necessary (for this columnGroup's
      // rowSpan). So now we can render the headers of our children.
      _setParentNode(tContext, currentNode);
      _renderChildren(context, column, currentNode);
      _setParentNode(tContext, parentNode);
      // and we are done. Do not increment the physicalIndex as our children
      // would have done so:
      return;
    }

    // at this point we need to increment the physical column index, since our
    // children did not render:
    // this columnGroup occupies as many physicalIndices as its colSpan:
    colData.setColumnIndex(colData.getPhysicalColumnIndex() +
                           currentNode.cols,
                           colData.getLogicalColumnIndex());
  }

  /**
   * @return the column header ID
   */
  private String _renderColumnHeader(
    FacesContext          context,
    RenderingContext   arc,
    TableRenderingContext tContext,
    UIComponent           column,
    int                   rowSpan,
    int                   colSpan)
    throws IOException
  {
    ColumnData colData = tContext.getColumnData();

    // only no-wrap header cells if specified
    boolean isNoWrap = getHeaderNoWrap(getFacesBean(column));

    // indicate to the headerNode that it is a column group header
    colData.setColumnGroupHeader(true);
    // indicate to the headerNode whether or not to permit wrapping:
    colData.setCurrentHeaderNoWrap(isNoWrap);

    final String colID =
      renderHeaderAndSpan(context, arc, tContext, column,
                          rowSpan, colSpan);
    colData.setColumnGroupHeader(false);
    colData.setCurrentHeaderNoWrap(false);

    return colID;
  }

  /**
   * @todo Add renderkit test assertion that no one test writes
   * out the same ID twice!
   */
  /*
  protected final Object getID(RenderingContext rContext, UINode column)
  {
    // we already render our ID on the <TH>. If we don't return null here,
    // then our subclasses might render our ID a second time on the <TD>
    return null;
  }
  */

  /**
   * @return the headerID
   * @todo Generate unique ID
   * @todo If we support required and message type, fix nowrap
   * @todo Refactor this function
   */
  protected final String renderHeaderAndSpan(
    FacesContext          context,
    RenderingContext   arc,
    TableRenderingContext tContext,
    UIComponent           column,
    int                   rowSpan,
    int                   colSpan)
    throws IOException
  {
    ColumnData colData = tContext.getColumnData();
    String colID;
    if (shouldRenderId(context, column) ||
        tContext.isExplicitHeaderIDMode())
      // =-=AEW THIS WILL GENERATE DIFFS FROM "unique ID" land
      colID = getClientId(context, column);
    else
      colID = null;


    int physicalIndex = colData.getPhysicalColumnIndex();
    int sortability = getSortability(tContext, column);
    boolean sortable = (sortability != SORT_NO) &&
                       supportsNavigation(arc);
                       
    if(sortable)
    {
      // the sortable script has a "state" parameter, so add this
      // to the form data if the agent does not support dynamic
      // generation of elements (on those that do, form data elements
      // can be created on the fly as necessary); see the JS
      // referenced in this.getSortingOnclick
      Object domLevel = 
        arc.getAgent().getCapabilities().get(TrinidadAgent.CAP_DOM);
      if(
        domLevel == null || 
        domLevel == TrinidadAgent.DOM_CAP_NONE || 
        domLevel == TrinidadAgent.DOM_CAP_FORM)
      {
        FormData formData = arc.getFormData();
        if(formData != null)
        {
          formData.addNeededValue(XhtmlConstants.STATE_PARAM);
        }      
      }
    }

    // we do not want to wrap if wrapping has explicitly been disabled. if we
    // are inside a columnGroup then we need to check the header format on the
    // columnGroup: bug 3201579:
    boolean isNoWrap = colData.isColumnGroupHeader()
      ? colData.getCurrentHeaderNoWrap()
    // =-=AEW It's weird that we're going back to colData instead
    // of just looking on ourselves!  When we're in a columnGroup, sure.
      : colData.getHeaderNoWrap(physicalIndex);


    String sortIconName = _getIconName(sortability);
    Icon sortIcon = arc.getIcon(sortIconName);
    boolean hasSortingIcon = (sortIcon != null) && !sortIcon.isNull();

    // we do not want to wrap if there is an icon on the header:
    // On PDA, where screen width is limited, we cannot afford not to
    // wrap.  isPDA check is used in several places in this class.
    // PDA specific logic will be moved to PDA render kit in the future.
    if (!isPDA(arc))
    {
      isNoWrap = isNoWrap || hasSortingIcon;
    }
    //       || getRequired(bean);
    //       || getMessageType(bean);

    Object width = tContext.getColumnWidth(physicalIndex);

    ResponseWriter rw = context.getResponseWriter();
    rw.startElement("th", column);
    rw.writeAttribute("id", colID, "id");

    CellUtils.renderHeaderAttrs(context, tContext,
                                null, //abbreviation (MISSING!)
                                width,
                                isNoWrap,
                                true); //isColHeader

    String styleClass = getSortableHeaderStyleClass(tContext, sortability);
    String borderStyleClass =
      CellUtils.getHeaderBorderStyle(tContext,
                                     arc,
                                     true, //isColHeader
                                     sortable);

    renderStyleClasses(context, arc, new String[]{ styleClass,
                                                   borderStyleClass});

    String style = getHeaderInlineStyle(arc);
    renderInlineStyleAttribute(context, arc, style);

    if (colSpan > 1)
      rw.writeAttribute("colspan", IntegerUtils.getString(colSpan), null);

    if (rowSpan == 0)
      rowSpan = colData.getHeaderRowSpan();
    if (rowSpan > 1)
      rw.writeAttribute("rowspan", IntegerUtils.getString(rowSpan), null);

    String sortOnclick = "";
    if (supportsScripting(arc))
    {
      sortOnclick = getSortingOnclick(arc, tContext, column, sortability);
    }
     
    //=-=AEW Review: Does this need to support any other handlers?

    //=-=AEW Apparently in PDA, we don't bother rendering
    //  the onclick on the cell, just on the icon.  I think
    //  that the only reason desktop renders it in both places
    //  was for Netscape.  If I'm right, then really this decision should
    //  be driven off an "event bubbling" agent property.
    // - HKuhn if printable mode (supportScripting is disabled),
    // then no need for rendering onclick
    if (!isPDA(arc) && supportsScripting(arc))
      rw.writeAttribute("onclick", sortOnclick, null);

    // TODO: we should pass in null for "event bubbling" systems
    renderHeaderContents(context,
                         arc,
                         tContext,
                         column,
                         sortability,
                         sortIcon,
                         sortOnclick);


    rw.endElement("th");

    return colID;
  }

  /**
   * @return an inline style String to be rendered on headers (used on
   *  special subclasses)
   */
  protected String getHeaderInlineStyle(RenderingContext arc)
  {
    return null;
  }


  /**
   */
  protected String getSortingOnclick(
    RenderingContext   arc,
    TableRenderingContext tContext,
    UIComponent           column,
    int                   sortability)
  {
    FacesBean bean = getFacesBean(column);
    String onclick  = getOnclick(bean);
    if (sortability == SORT_NO)
      return onclick;

    if (arc.getFormData() == null)
    {
      _LOG.warning("SORTING_DISABLED_TABLE_NOT_IN_FORM");
      return onclick;
    }

    String formName = arc.getFormData().getName();
    String source   = tContext.getTableId();
    String value    = getSortProperty(bean);
    // Note that "state" refers to the current state, not
    // the state will be set after clicking
    String state = findSortState(sortability, bean);
    
    StringBuffer buffer = new StringBuffer(33+
                                           formName.length() +
                                           source.length() +
                                           value.length() +
                                           state.length()
                                           );
    buffer.append("return _tableSort('");
    buffer.append(formName);
    buffer.append(tContext.isImmediate() ? "',0,'" : "',1,'");
    buffer.append(source);
    buffer.append("','");
    buffer.append(value);
    if (state != "")
    {
      buffer.append("','");
      buffer.append(state);
    }

    buffer.append("');");
    String sortJS = buffer.toString();
    if (onclick != null)
    {
      sortJS = XhtmlUtils.getChainedJS(onclick, sortJS, true);
    }

    return sortJS;
  }

  protected void renderHeaderContents(
    FacesContext          context,
    RenderingContext   arc,
    TableRenderingContext tContext,
    UIComponent           column,
    int                   sortability,
    Icon                  sortIcon,
    String                sortOnclick) throws IOException
  {
    ResponseWriter rw = context.getResponseWriter();
    UIComponent header = getFacet(column, CoreColumn.HEADER_FACET);
    if (header != null)
    {
      encodeChild(context, header);
    }
    else
    {
      String headerText = getHeaderText(getFacesBean(column));
      if (headerText != null)
        rw.writeText(headerText, "headerText");
    }

     renderSortOrderSymbol(context, arc, column, tContext,
                                    sortability, sortIcon, sortOnclick);
    
  }
  
  
  /**
   * @todo IMPLEMENT
   */
  protected void renderSortOrderSymbol(
    FacesContext       context,
    RenderingContext arc,
    UIComponent           column,
    TableRenderingContext tContext,
    int                 sortability,
    Icon                icon,
    String              sortOnclick
    ) throws IOException
  {
    if ((icon == null) || icon.isNull())
      return;

    ResponseWriter writer = context.getResponseWriter();
    boolean supportNav = supportsNavigation(arc);
    boolean NonJavaScriptBrowser = false;
    boolean renderedInput = false;
    if (supportNav)
    {
      if (isPDA(arc))
        writer.writeText(XhtmlConstants.NBSP_STRING, null);
        
      NonJavaScriptBrowser = !supportsScripting(arc);  
      if (NonJavaScriptBrowser)
      {
        renderedInput = true;
        writer.startElement("input", null);
        writer.writeAttribute("type", "submit", null);
        String source = tContext.getTableId();
        FacesBean bean = getFacesBean(column);
        String value = getSortProperty(bean);
        String state = findSortState(sortability, bean);
        String nameAttri;
        if (state != "")
        {
          nameAttri =  XhtmlUtils.getEncodedParameter
                                   (XhtmlConstants.SOURCE_PARAM)
                       + XhtmlUtils.getEncodedParameter(source)
                       + XhtmlUtils.getEncodedParameter
                                   (XhtmlConstants.VALUE_PARAM)
                       + XhtmlUtils.getEncodedParameter(value)
                       + XhtmlUtils.getEncodedParameter
                                   (XhtmlConstants.EVENT_PARAM)
                       + XhtmlUtils.getEncodedParameter
                                   (XhtmlConstants.SORT_EVENT)
                       + XhtmlUtils.getEncodedParameter
                                   (XhtmlConstants.STATE_PARAM)
                       + state;
        }
        else
        {
          nameAttri =  XhtmlUtils.getEncodedParameter
                                    (XhtmlConstants.SOURCE_PARAM)
                       + XhtmlUtils.getEncodedParameter(source)
                       + XhtmlUtils.getEncodedParameter
                                    (XhtmlConstants.EVENT_PARAM)
                       + XhtmlUtils.getEncodedParameter
                                    (XhtmlConstants.SORT_EVENT)
                       + XhtmlUtils.getEncodedParameter
                                    (XhtmlConstants.VALUE_PARAM)
                       + value;
        }

        writer.writeAttribute("name", nameAttri, null);
        if (state.equals(XhtmlConstants.SORTABLE_ASCENDING))
        {
          writer.writeAttribute("value",
                                    XhtmlConstants.NON_JS_DESC_ICON, null); 
        }
        else
        {
          writer.writeAttribute("value", 
                                    XhtmlConstants.NON_JS_ASC_ICON, null); 
        }

        writer.writeAttribute("class", 
             SkinSelectors.SORTABLE_HEADER_SORT_ICON_STYLE_CLASS, null);
      }
      else
      {
        writer.startElement("a", null);
        writer.writeURIAttribute("href", "#", null);
        writer.writeAttribute("onclick", sortOnclick, null);
      }
    }

    String altTextKey = null;
    if (sortability == SORT_ASCENDING)
    {
      altTextKey = "af_column.SORTED_ASCEND_TIP";
    }
    else if (sortability == SORT_DESCENDING)
    {
      altTextKey = "af_column.SORTED_DESCEND_TIP";
    }
    else if (sortability == SORT_SORTABLE)
    {
      altTextKey = "af_column.SORTED_SORTABLE_TIP";
    }


    String altText = arc.getTranslatedString(altTextKey);

    Object align = OutputUtils.getMiddleIconAlignment(arc);
    
    //Don't render any child element for input element
    if (!renderedInput)
    {
      // Render the icon, specifying embedded=true.  This
      // allows text-based Icons to render their style class
      // and altText directly on the anchor itself
      OutputUtils.renderIcon(context,
                             arc,
                             icon,
                             altText,
                             align,
                             true);
    }

    if (supportNav)
    {
      if(NonJavaScriptBrowser)
      {
        writer.writeAttribute("title", altText, null);
        writer.endElement("input");
      }
      else
      {
        writer.endElement("a");
      }
    }

  }

  /**
   * @return 0 if not sortable. 1 if sortable, but not sorted.
   * 2 if sorted in ascending order. 3 if sorted in descending order.
   */
  protected final int getSortability(
    TableRenderingContext tContext,
    UIComponent           column)
  {
    // no columns sortable when empty table
    if (tContext.getRowData().isEmptyTable())
      return SORT_NO;

    FacesBean bean = getFacesBean(column);
    // Check to make sure this is a sortable UIComponent column.
    // some columns are uix 2.2 fake columns for "select", "details" and other
    // special columns.
    // =-=AEW FIX
    if (column == null)
      return 0;

    // If there's no sort property, it's not sortable
    String property = getSortProperty(bean);
    if (property == null)
      return SORT_NO;

    // And if the renderer-specific "sortable" property is set to false,
    // it's not sortable
    if (!getSortable(bean))
      return SORT_NO;

    // Otherwise, look at the first sort criteria
    // =-=AEW This seems slow...
    UIXCollection table = (UIXCollection) tContext.getTable();
    List<SortCriterion> criteria = table.getSortCriteria();
    // We currently only show anything for the primary sort criterion
    if (criteria.size() > 0)
    {
      SortCriterion criterion = criteria.get(0);
      if (property.equals(criterion.getProperty()))
      {
        return criterion.isAscending() ? SORT_ASCENDING : SORT_DESCENDING;
      }
    }

    return table.isSortable(property) ? SORT_SORTABLE : SORT_NO;
  }

  protected boolean hasSortingIcon(
    RenderingContext arc,
    int                 sortability)
  {
    return sortability != SORT_NO;
  }

  /**
   * gets the icon name to use
   */
  private String _getIconName(int sortable)
  {
    switch (sortable)
    {
      case SORT_SORTABLE:
        return SkinSelectors.AF_COLUMN_UNSORTED_ICON_NAME;
      case SORT_ASCENDING:
        return SkinSelectors.AF_COLUMN_SORTED_ASCEND_ICON_NAME;
      case SORT_DESCENDING:
        return SkinSelectors.AF_COLUMN_SORTED_DESCEND_ICON_NAME;
      default:
        return null;
    }
  }

  private void _computeMode(
    FacesContext        context,
    TableRenderingContext tContext,
    UIComponent           component) throws IOException
  {
    // since we use colSpan we need headers attributes on all the table's data
    // cells:
    tContext.setExplicitHeaderIDMode(true);

    NodeData parentNode = getParentNode(tContext);
    boolean areWeRoot = (parentNode == null);

    int kids = component.getChildCount();

    NodeData currentNode = new NodeData(kids);
    if (areWeRoot)
    {
      _getNodeList(tContext, true).add(currentNode);
    }

    _setParentNode(tContext, currentNode);

    // "Render" the children to execute their "compute mode"
    _renderChildren(context, component, currentNode);

    ColumnData colData = tContext.getColumnData();
    if (areWeRoot)
    {
      colData.setHeaderRowSpan(currentNode.rows);
      int cols = currentNode.cols + colData.getColumnCount() - 1;
      colData.setColumnCount(cols);
    }
    else
    {
      int rows = currentNode.rows+1;
      if (parentNode.rows < rows)
        parentNode.rows = rows;

      parentNode.cols += currentNode.cols;

      parentNode.set(parentNode.currentChild, currentNode);
    }

    _setParentNode(tContext, parentNode);
  }

  @SuppressWarnings("unchecked")
  private void _renderChildren(FacesContext context,
                               UIComponent  component,
                               NodeData     parentNode)
    throws IOException
  {
    int i = 0;
    for(UIComponent child : (List<UIComponent>)component.getChildren())
    {
      if (child.isRendered())
      {
        // Tell the parent node - if there is one - which child we're rendering
        if (parentNode != null)
        {
          parentNode.currentChild = i;
        }
        
        encodeChild(context, child);
      }
      
      i++;
    }
  }

  protected final NodeData getParentNode(TableRenderingContext tContext)
  {
    NodeList nl = _getNodeList(tContext, false);
    return (nl == null) ? null : nl.currentNode;
  }

  private void _setParentNode(TableRenderingContext tContext,
                              NodeData parentNode)
  {
    _getNodeList(tContext, true).currentNode = parentNode;
  }

  /**
   * Returns the skinning selector for the header
   * taking into account the if the column is sortable or is sorted
   * @param tContext the TableRenderingContext
   * @param sortability the value returned by getSortability()
   * @return the skinning selector for the header
   */
  protected String getSortableHeaderStyleClass(TableRenderingContext tContext,
                                      int sortability)
  {
    ColumnData colData = tContext.getColumnData();
    // if we are a columnGroup header, then we must be centered:
    if (colData.isColumnGroupHeader())
    {
      return SkinSelectors.AF_COLUMN_HEADER_ICON_STYLE;
    }

    switch (sortability)
    {
      //not sortable column
      case SORT_NO:
        return getHeaderStyleClass(tContext);
        //sortable column (but not sorted)
      case SORT_SORTABLE:
        return ColumnData.selectFormat(
            tContext,
            SkinSelectors.AF_COLUMN_SORTABLE_HEADER_STYLE_CLASS,
            SkinSelectors.AF_COLUMN_SORTABLE_HEADER_NUMBER_STYLE_CLASS,
            SkinSelectors.AF_COLUMN_SORTABLE_HEADER_ICON_STYLE_CLASS);
        //sorted column
      default:
        return ColumnData.selectFormat(
            tContext,
            SkinSelectors.AF_COLUMN_SORTED_HEADER_STYLE_CLASS,
            SkinSelectors.AF_COLUMN_SORTED_HEADER_NUMBER_STYLE_CLASS,
            SkinSelectors.AF_COLUMN_SORTED_HEADER_ICON_STYLE_CLASS);
    }
  }

  private NodeList _getNodeList(TableRenderingContext tContext,
                                boolean create)
  {
    NodeList root =
      (NodeList) tContext.getHeaderNodesList();
    if ((root == null) && create)
    {
      root = new NodeList();
      tContext.setHeaderNodeList(root);
    }

    return root;
  }

  private static final class NodeList
  {
    private final ArrayList<NodeData> _list = new ArrayList<NodeData>(10);
    private int _index = 0;

    public NodeData currentNode = null;

    public void add(NodeData node)
    {
      _list.add(node);
    }

    public NodeData getNext()
    {
      if (_index >= _list.size())
        _index = 0;

      return _list.get(_index++);
    }
  }

  protected static final class NodeData
  {
    private final NodeData[] _kids;
    public int rows, cols, waitUntilRow = 0;
    public int currentChild;
    public String headerIDs = null;

    // for leaf nodes
    public NodeData()
    {
      rows = cols = 1;
      _kids = null;
    }

    // for parent nodes
    private NodeData(int kids)
    {
      rows = cols = 0;
      _kids = new NodeData[kids];
    }

    public void set(int index, NodeData kid)
    {
      _kids[index] = kid;
    }

    public int size()
    {
      return (_kids == null) ? 0 : _kids.length;
    }

    public NodeData get(int index)
    {
      return _kids[index];
    }
  }
  
  /**
   * @return the state of the sorting after the page submition 
   */
  private String findSortState(
     int sortability, 
     FacesBean bean )
  {
    String state;
    if (sortability == SORT_ASCENDING)
    {
      state = XhtmlConstants.SORTABLE_ASCENDING;
    }
    else if (sortability == SORT_DESCENDING)
    {
      state = XhtmlConstants.SORTABLE_DESCENDING;
    }
    else if ("descending".equals(getDefaultSortOrder(bean)))
    {
      state = XhtmlConstants.SORTABLE_ASCENDING;
    }
    else
    {
      state = "";
    }
    
    return state;
  }

  private PropertyKey _headerTextKey;
  private PropertyKey _headerNoWrapKey;
  private PropertyKey _rowHeaderKey;
  private PropertyKey _separateRowsKey;
  private PropertyKey _noWrapKey;
  private PropertyKey _alignKey;
  private PropertyKey _widthKey;
  private PropertyKey _sortPropertyKey;
  private PropertyKey _sortableKey;
  private PropertyKey _defaultSortOrderKey;

  private static final TrinidadLogger _LOG = TrinidadLogger.createTrinidadLogger(ColumnGroupRenderer.class);
}
