/*
 *  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 java.util.Map;

import javax.faces.component.UIComponent;
import javax.faces.component.UISelectItem;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.model.SelectItem;

import org.apache.myfaces.trinidad.bean.FacesBean;
import org.apache.myfaces.trinidad.bean.PropertyKey;
import org.apache.myfaces.trinidad.component.UIXPanel;
import org.apache.myfaces.trinidad.component.UIXSelectItem;
import org.apache.myfaces.trinidad.component.UIXSelectRange;
import org.apache.myfaces.trinidad.component.core.data.CoreSelectRangeChoiceBar;
import org.apache.myfaces.trinidad.context.Agent;
import org.apache.myfaces.trinidad.context.FormData;
import org.apache.myfaces.trinidad.context.PartialPageContext;
import org.apache.myfaces.trinidad.context.RenderingContext;
import org.apache.myfaces.trinidad.context.RequestContext;
import org.apache.myfaces.trinidad.event.RangeChangeEvent;
import org.apache.myfaces.trinidad.logging.TrinidadLogger;
import org.apache.myfaces.trinidad.skin.Icon;
import org.apache.myfaces.trinidad.util.IntegerUtils;
import org.apache.myfaces.trinidadinternal.util.Range;

public class SelectRangeChoiceBarRenderer extends XhtmlRenderer
{
  public SelectRangeChoiceBarRenderer()
  {
    this(CoreSelectRangeChoiceBar.TYPE);
  }

  public SelectRangeChoiceBarRenderer(FacesBean.Type type)
  {
    super(type);
  }

  @Override
  protected void findTypeConstants(FacesBean.Type type)
  {
    super.findTypeConstants(type);
    _rowsKey = type.findKey("rows");
    _firstKey = type.findKey("first");
    _immediateKey = type.findKey("immediate");
    _showAllKey = type.findKey("showAll");
    _varKey = type.findKey("var");
  }

  @SuppressWarnings("unchecked")
  @Override
  public void decode(FacesContext context, UIComponent component)
  {
    Map<String, String> parameters =
      context.getExternalContext().getRequestParameterMap();

    Object event = parameters.get(XhtmlConstants.EVENT_PARAM);

    // get the goto event parameter values and queue a RangeChangeEvent.
    if (XhtmlConstants.GOTO_EVENT.equals(event))
    {
      Object source = parameters.get(XhtmlConstants.SOURCE_PARAM);
      String id = component.getClientId(context);
      if (id.equals(source))
      {
        UIXSelectRange choiceBar = (UIXSelectRange)component;
        Object valueParam = parameters.get(XhtmlConstants.VALUE_PARAM);
        RangeChangeEvent rce = _createRangeChangeEvent(choiceBar, valueParam);
        rce.queue();

        if (choiceBar.isImmediate())
          context.renderResponse();

        RequestContext.getCurrentInstance().addPartialTarget(component);
      }
    }
  }

  private RangeChangeEvent _createRangeChangeEvent(
    UIXSelectRange choiceBar,
    Object         valueParam)
  {
    // get the variables needed to calculate oldStart, oldEnd,
    // newStart, newEnd.
    int rowCount = choiceBar.getRowCount();
    int rows = choiceBar.getRows();

    FacesBean bean = getFacesBean(choiceBar);
    boolean isShowAll = getShowAll(bean);

    // calculate oldStart and oldEnd
    int increment = (isShowAll && rowCount > -1) ? rowCount : rows;
    int oldStart = choiceBar.getFirst();
    int oldEnd = oldStart + increment;


    // calculate newStart and newEnd

    // initialize showAll to its default state. We will change
    // this later if the event's value is "all".
    if (isShowAll)
      bean.setProperty(_showAllKey, Boolean.FALSE);

    int newStart = -1;
    int newEnd = -1;

    if (valueParam != null)
    {
      String newStartString = valueParam.toString();

      // We get "all" if the user selected the "Show All" option.
      // If so, set showAll to true and set newStart and newEnd to
      // be the entire range.
      if (newStartString.equals(XhtmlConstants.VALUE_SHOW_ALL))
      {
        bean.setProperty(_showAllKey, Boolean.TRUE);
        newStart = 0;
        newEnd = rowCount;
      }
      else
      {
        try
        {
          newStart = Integer.parseInt(newStartString) - 1;
          newEnd = newStart + rows;
        }
        catch (NumberFormatException nfe)
        {
          // Shouldn't happen with a legit request
          _LOG.severe(nfe);
        }
      }
    }

    return new RangeChangeEvent(choiceBar, oldStart, oldEnd,
                                newStart, newEnd);
  }

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

  /**
   * Always render an ID, needed for proper PPR.
   */
  @Override
  protected boolean shouldRenderId(
    FacesContext context,
    UIComponent  component)
  {
    return true;
  }

  protected int getRows(UIComponent component, FacesBean bean)
  {
    Object o = bean.getProperty(_rowsKey);
    if (o == null)
      o = _rowsKey.getDefault();

    return toInt(o);
  }



  protected int getFirst(UIComponent component, FacesBean bean)
  {
    Object o = bean.getProperty(_firstKey);
    if (o == null)
      o = _firstKey.getDefault();

    return toInt(o);
  }


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

    return Boolean.TRUE.equals(o);
  }


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

    return Boolean.TRUE.equals(o);
  }

  protected String getVar(FacesBean bean)
  {
    return toString(bean.getProperty(_varKey));
  }

  protected UIComponent getRangeLabel(UIComponent component)
  {
    return getFacet(component, CoreSelectRangeChoiceBar.RANGE_LABEL_FACET);
  }

  //
  // HOOKS FOR SUBCLASSES
  // These methods exist entirely for subclasses to override behavior.
  //
  protected int getRowCount(UIComponent component)
  {
    return ((UIXSelectRange) component).getRowCount();
  }

  protected int getRowIndex(UIComponent component)
  {
    return ((UIXSelectRange) component).getRowIndex();
  }

  protected void setRowIndex(UIComponent component, int index)
  {
    ((UIXSelectRange) component).setRowIndex(index);
  }

  protected boolean isRowAvailable(UIComponent component)
  {
    return ((UIXSelectRange) component).isRowAvailable();
  }

  protected boolean isRowAvailable(UIComponent component, int rowIndex)
  {
    return ((UIXSelectRange) component).isRowAvailable(rowIndex);
  }

  protected Object getRowData(UIComponent component)
  {
    return ((UIXSelectRange) component).getRowData();
  }

  protected String getSource()
  {
    return null;
  }

  protected boolean showAllSupported()
  {
    return true;
  }

  //
  // END OF HOOKS
  //

  /**
   */
  @Override
  protected void encodeAll(
    FacesContext        context,
    RenderingContext arc,
    UIComponent         component,
    FacesBean           bean) throws IOException
  {
    int rowIndex = getRowIndex(component);
    try
    {
      int blockSize = getRows(component, bean);
      if (blockSize < 0)
       blockSize = toInt(_rowsKey.getDefault());

      // =-=AEW The old rendering code was written with "value" as one-indexed;
      // "first" is zero-indexed.  Rewrite the rendering code to deal.
      long currentValue = getFirst(component, bean) + 1;
      if (currentValue < 1)
        currentValue = 1;

      long minValue = 1;

      // @todo: =-=jmw ... get maxValue from the model. If no model, then use the
      // maximum attribute. Not sure we want to implement this feature.
      long maxValue = getRowCount(component);
      if (maxValue <= 0)
        maxValue = XhtmlConstants.MAX_VALUE_UNKNOWN;

      // get name
      String id = getClientId(context, component);
      // For the source, just pass the ID as long as this is being
      // used on its own - but give a hook for subclasses to their thing.
      String source = getSource();
      if (source == null)
        source = id;

      if (arc.getFormData() == null)
        return;

      String formName = arc.getFormData().getName();
      if (formName == null)
        return;

      int  nextRecords = 0;
      int  prevRecords = 0;
      long backValue = 0;
      long nextValue = 0;

      if (blockSize > 0)
      {
        // determine how many records user can go forward
        long lNextRecords = blockSize;

        if (maxValue != XhtmlConstants.MAX_VALUE_UNKNOWN)
        {
          // if we know the total records, align the current value to the
          // start of its block. This makes the choice-rendering style
          // not show extra back navigation records on the min block,
          // which it would if not aligned.

          // =-=AEW Revisit bug 3052637 for JSF
          /** no don't do this. see bug: 3052637
              currentValue -= minValue;
              currentValue /= blockSize;
              currentValue *= blockSize;
              currentValue += minValue;
          */

          lNextRecords = maxValue - (currentValue + blockSize - 1);
        }

        // determine how many records user can go back
        long lPrevRecords = currentValue - minValue;

        // trim
        nextRecords = (lNextRecords > blockSize)
          ? blockSize
          : (int) lNextRecords;

        prevRecords = (lPrevRecords > blockSize)
          ? blockSize
          : (int) lPrevRecords;

        backValue = currentValue - prevRecords;
        nextValue = currentValue + blockSize;
      }


      boolean validate = !getImmediate(bean);

      boolean showDisabledNavigation = disabledNavigationShown();
      boolean hasBackRecords = (prevRecords > 0);
      boolean hasNextRecords = (nextRecords > 0);

      if (hasNextRecords && (maxValue == XhtmlConstants.MAX_VALUE_UNKNOWN))
      {
        // make sure the next range exists in the data model.
        hasNextRecords = isRowAvailable(component, (int)nextValue-1);
      }

      boolean showBackButton = hasBackRecords || showDisabledNavigation;
      boolean showNextButton = hasNextRecords || showDisabledNavigation;
      if (!supportsNavigation(arc))
      {
        showBackButton = false;
        showNextButton = false;
      }

      boolean showAllActive = getShowAll(bean);

      if (showAllActive)
      {
        prevRecords = 0;
        nextRecords = 0;
      }

      String prevOnClick = null;
      String nextOnClick = null;


      if (hasBackRecords || hasNextRecords)
      {
        addHiddenFields(arc);
        // Render script submission code.
        ProcessUtils.renderNavSubmitScript(context, arc);
        ProcessUtils.renderNavChoiceSubmitScript(context, arc);
      }

      // use form submit
      if (supportsScripting(arc))
      {
        if (hasBackRecords && !showAllActive)
        {
          prevOnClick = ProcessUtils.getSubmitScriptCall(formName,
                                                         source,
                                                         backValue,
                                                         validate);
        }

        if (hasNextRecords && !showAllActive)
        {
          nextOnClick =  ProcessUtils.getSubmitScriptCall(formName,
                                                          source,
                                                          nextValue,
                                                          validate);
        }
      }

      // ready to render
      ResponseWriter writer = context.getResponseWriter();
      boolean renderAsTable = __renderAsTable(component);


      // The following strange code is part of the work around for
      // bug 2275703.  IE has problems re-laying out a TableBean
      // after a partial page replacement.  In particular, pieces
      // of the table's top navigation bar, such as the previous link
      // or icon, or sometimes the entire navigation bar,  may shift to
      // the left.  In some cases, pieces of the navigation bar (the previous
      // icon) may disappear during re-layout!  There doesn't seem to be
      // any clean way to avoid this apparent IE bug.  However, we explicitly
      // perform a partial replacement of the navigation bar's previous icon
      // *after* the entire table has been replaced, everything seems to lay
      // out just fine.
      //
      // So, if we are rendering a TableBean's navigation bar with
      // PPR enabled on IE, then we generate an ID for the nav bar's
      // previous icon, and we add this to the list of rendered partial
      // targets during the partial page render.  This forces the icon
      // to be replaced as part of the partial page update, and fixes
      // our layout problems.
      String iconID = null;
      if (PartialPageUtils.isPPRActive(context) &&
          isIE(arc))
      {
        iconID = id + "-i";
      }

      // we only want to render the baseID, if needed, once. Then we
      // render the subIDs. So we need to keep track of this.
      boolean renderedId = false;

      // If the request is from a desktop browser we don't need to wrap up
      // with a div tag
      boolean isDesktop = false;

      // if we need to render standalone, create a table and table row...
      if (renderAsTable)
      {
        isDesktop = (arc.getAgent().getType().equals(Agent.TYPE_DESKTOP));
        // Few mobile browsers doesn't support PPR for Table element
        // so lets wrap it up with a div tag
        if(!isDesktop )
        {
          writer.startElement("div", component);
          writer.writeAttribute("id", id, "id");
        }
        writer.startElement("table", component);
        OutputUtils.renderLayoutTableAttributes(context, arc, "0", null);

        // =-=AEW Where do these attrs get written out when
        // we're inside a PanelPageButton?
        renderAllAttributes(context, arc, bean);

        // We should always render the ID, but we particularly need
        // to make sure that the ID is rendered if the NavBar is being
        // used to navigate a TableBean, since we explicitly target
        // TableBean NavBars when using PPR to re-render TableBeans...
        if(isDesktop )
        {
          writer.writeAttribute("id", id, "id");
        }
        renderedId = true;

        writer.startElement("tr", null);
      }

      if (showBackButton)
      {
        Icon prevIcon = getIcon(arc, false, (prevOnClick != null));

        if (!prevIcon.isNull())
        {
          // We assign an id to the left arrow so that we can target it as
          // a partial target to work around bug 2275703 - see note above.
          if (iconID != null)
          {
            writer.startElement("td", component);
            writer.writeAttribute("id", iconID, null);

            // If the navigation bar that we are currently rendering
            // is included in a partial page response, add the icon
            // id to the list of partial targets.
            // =-=AEW Not sure this is still necessary
            PartialPageContext pprContext = arc.getPartialPageContext();
            if ((pprContext != null) &&
                pprContext.isInsidePartialTarget())
            {
              pprContext.addRenderedPartialTarget(iconID);
            }
          }
          else
          {
            // not in PPR mode, so just render the td (and id if not in a table)
            _renderStartTableCell(writer, id, renderedId);
            renderedId = true;
          }


          writer.writeAttribute("valign", "middle", null);
          _renderArrow(context, arc, prevIcon, false, prevOnClick);
          writer.endElement("td");

          _renderSpacerCell(context, arc);
        }

        _renderStartTableCell(writer, id, renderedId);
        renderedId = true;
        writer.writeAttribute("valign", "middle", null);
        writer.writeAttribute("nowrap", Boolean.TRUE, null);
        
        _renderLink(context,
                    arc, 
                    false, 
                    prevOnClick,
                    prevRecords, 
                    id, 
                    source, 
                    backValue);
                    
        writer.endElement("td");
        _renderSpacerCell(context, arc);
      }

      _renderStartTableCell(writer, id, renderedId);
      renderedId = true;
      writer.writeAttribute("valign", "middle", null);
      writer.writeAttribute("nowrap", Boolean.TRUE, null);

      _renderChoice(context,
                    arc,
                    component,
                    id,
                    source,
                    formName,
                    minValue,
                    currentValue,
                    blockSize,
                    maxValue,
                    validate);

      writer.endElement("td");

      if (showNextButton)
      {
        _renderSpacerCell(context, arc);

        _renderStartTableCell(writer, id, true);
        writer.writeAttribute("valign", "middle", null);
        writer.writeAttribute("nowrap", Boolean.TRUE, null);
        
        _renderLink(context,
                    arc, 
                    true, 
                    nextOnClick,
                    nextRecords, 
                    id,
                    source,
                    nextValue);
                  
        writer.endElement("td");

        Icon nextIcon = getIcon(arc, true, (nextOnClick != null));
        if (!nextIcon.isNull())
        {
          _renderSpacerCell(context, arc);

          _renderStartTableCell(writer, id, true);
          writer.writeAttribute("valign", "middle", null);
          _renderArrow(context, arc, nextIcon, true, nextOnClick);
          writer.endElement("td");
        }
      }

      if (renderAsTable)
      {
        writer.endElement("tr");
        writer.endElement("table");
      }

      if (renderAsTable && !isDesktop )
      {
        writer.endElement("div");
      }

    }
    // Make sure we always restore the row index correctly
    finally
    {
      setRowIndex(component, rowIndex);
    }
  }


  /**
   * render form value needed values and javascript code.
   */
  public static void addHiddenFields(RenderingContext arc)
  {
    FormData fData = arc.getFormData();
    fData.addNeededValue(XhtmlConstants.EVENT_PARAM);
    fData.addNeededValue(XhtmlConstants.SOURCE_PARAM);
    fData.addNeededValue(XhtmlConstants.PARTIAL_PARAM);
    fData.addNeededValue(XhtmlConstants.VALUE_PARAM);
  }

  private void _renderChoice(
   FacesContext        context,
   RenderingContext arc,
   UIComponent         component,
   String              id,
   String              source,
   String              form,
   long                minValue,
   long                currentValue,
   int                 blockSize,
   long                maxValue,
   boolean             validate) throws IOException
  {
    UIComponent rangeLabel = getRangeLabel(component);
    boolean firstRowAvailable = isRowAvailable(component, 0);

    ResponseWriter writer = context.getResponseWriter();
    // if there is no blockSize to step by, or there are no items in the
    // table, then we don't render a choice
    if ((blockSize <= 0) || (!firstRowAvailable) ||
          ((maxValue < minValue) &&
           (maxValue != XhtmlConstants.MAX_VALUE_UNKNOWN)))
    {
      writer.writeText(XhtmlConstants.NBSP_STRING, null);
    }
    else
    {
      List<SelectItem> items =
        new ArrayList<SelectItem>((int) _MAX_VISIBLE_OPTIONS);

      int selectedIndex = _getItems(context, arc, component, items,
                                    minValue, maxValue, currentValue,
                                    blockSize, rangeLabel);
      int count = items.size();
      if (count > 1)
      {
        String choiceTip = arc.getTranslatedString(_CHOICE_TIP_KEY);
        String choiceId = XhtmlUtils.getCompositeId(id, _CHOICE_ID_SUFFIX);
        String onChange = ProcessUtils.getChoiceOnChangeFormSubmitted(
                             form, source, validate);
        boolean javaScriptSupport = supportsScripting(arc);

        writer.startElement("select", null);
        writer.writeAttribute("title", choiceTip, null);
        renderStyleClass(context, arc,
                         SkinSelectors.AF_FIELD_TEXT_STYLE_CLASS);

        
        if (onChange != null && javaScriptSupport)
        {
          // set the onchange handler
          writer.writeAttribute("onchange", onChange, null);
          // set the onfocus handler to save the initial value
          writer.writeAttribute("onfocus", _CHOICE_FORM_ON_FOCUS, null);
        }

        writer.writeAttribute("id", choiceId, null);
        
        // For Non-JavaScript browsers, render the name attribute thus it
        // would enable the browsers to include the name and value of this 
        // element in its payLoad.
     
        if (!javaScriptSupport)
        {
          writer.writeAttribute("name", choiceId, null);
        }

        _writeSelectItems(context, items, selectedIndex);

        writer.endElement("select");

        if (HiddenLabelUtils.supportsHiddenLabels(arc))
        {
          HiddenLabelUtils.outputHiddenLabelIfNeeded(context,
                                             arc,
                                             choiceId,
                                             choiceTip,
                                             null);
        }
        
        
        // For Non-JavaScript browsers, render a input element(type= submit) to 
        // submit the page. Encode the name attribute with the parameter name 
        // and value thus it would enable the browsers to include the name of 
        // this element in its payLoad if it submits the page.
        
        if (!javaScriptSupport)
        {
          String nameAttri =  XhtmlUtils.getEncodedParameter
                                          (XhtmlConstants.MULTIPLE_VALUE_PARAM)
                              + XhtmlUtils.getEncodedParameter(choiceId)
                              + XhtmlUtils.getEncodedParameter
                                          (XhtmlConstants.SOURCE_PARAM)
                              + XhtmlUtils.getEncodedParameter(source)
                              + XhtmlUtils.getEncodedParameter
                                          (XhtmlConstants.EVENT_PARAM)
                              + XhtmlConstants.GOTO_EVENT;

          writer.startElement("span", null);
          writer.startElement("input", null);
          writer.writeAttribute("value",XhtmlConstants.NO_JS_PARAMETER_KEY_BUTTON , null);
          writer.writeAttribute("type","submit", null);
          writer.writeAttribute("name", nameAttri, null);
          writer.endElement("input");
          writer.endElement("span");
        }
        else
        {
          writer.startElement("script", null);
          renderScriptDeferAttribute(context, arc);
          renderScriptTypeAttribute(context, arc);
          writer.writeText("_setSelectIndexById(\"", null);
          writer.writeText(choiceId, null);
          writer.writeText("\",", null);
          writer.writeText(IntegerUtils.getString(selectedIndex), null);
          writer.writeText(")", null);
          writer.endElement("script");
        }
      }
      else if (count == 1)
      {
        writer.startElement("span", null);
        renderStyleClass(context, arc,
                         SkinSelectors.AF_FIELD_TEXT_STYLE_CLASS);
        writer.writeText(items.get(0).getLabel(), null);
        writer.endElement("span");
      }
    }
  }

  private void _writeSelectItems(
    FacesContext     context,
    List<SelectItem> items,
    int              selectedIndex) throws IOException
  {
    ResponseWriter writer = context.getResponseWriter();
    int count = items.size();
    for (int i = 0; i < count; i++)
    {
      SelectItem item = items.get(i);
      writer.startElement("option", null);
      writer.writeAttribute("value", item.getValue(), null);
      if (i == selectedIndex)
        writer.writeAttribute("selected", Boolean.TRUE, null);
      writer.writeText(item.getLabel(), null);
      writer.endElement("option");
    }
  }

  /**
   * create each of the choice options and add them onto the List.
   * @return the number of options added
   */
  private int _getItems(
    FacesContext        context,
    RenderingContext arc,
    UIComponent         component,
    List<SelectItem>    items,
    long                minValue, long maxValue, long value,
    int                 blockSize,
    UIComponent         rangeLabel)
  {
    int selectedIndex = -1;

    boolean maxUnknown = (maxValue == XhtmlConstants.MAX_VALUE_UNKNOWN);

    // Zero-indexed block index.
    long blockIndex = (value - minValue + blockSize - 1L) / blockSize;

    // sometimes a record set won't start on a multiple of blockSize. So
    // remember to add any offset:
    // this can safely be an int because it is an index into the blockSize,
    // which is itself an int:
    int offset = (int) (value - (minValue + (blockIndex * blockSize)));
    if (offset < 0)
      offset = offset + blockSize;

    // Total number of blocks (again, zero-indexed)
    long maxBlockIndex;
    if (maxUnknown)
      maxBlockIndex = blockIndex + 1;
    else
    {
      maxBlockIndex = (maxValue - minValue - offset) / blockSize;
      if (offset > 0)
        maxBlockIndex++;
    }

    // Calculate the first block that should be shown.  The order goes:
    // Group 0:            0-28 + More
    // Group 1:Previous + 29-56 + More
    // Group 2:Previous + 57-84 + More
    // etc..
    long firstBlockIndex;

    // If everything is visible, or we're in the first group, start at zero.
    if ((maxBlockIndex <= (_MAX_VISIBLE_OPTIONS - 1L)) ||
        (blockIndex <= (_MAX_VISIBLE_OPTIONS - 2L)))
      firstBlockIndex = 0;
    else
      firstBlockIndex = ((blockIndex - 1L) / (_MAX_VISIBLE_OPTIONS - 2L)) *
                         (_MAX_VISIBLE_OPTIONS - 2L);

    // And we always show a total of 30 groups (or straight to the end)
    long lastBlockIndex = firstBlockIndex + (_MAX_VISIBLE_OPTIONS - 1L);
    if (lastBlockIndex > maxBlockIndex)
      lastBlockIndex = maxBlockIndex;

    boolean showAllActive = getShowAll(getFacesBean(component));

    // Add "Show All" option if showAll was set to true OR
    // when there are less than 30 groups (maxBlockIndex
    // start as zero, hence "29") and only allow it when there's
    // more than 1 visible item!
    if (showAllActive ||
        (!maxUnknown && (lastBlockIndex > firstBlockIndex) &&
         (maxBlockIndex <= (_MAX_VISIBLE_OPTIONS - 1L))
         ))
    {
      // Omit show all if it's not supported
      if (showAllSupported())
      {
        items.add(_createShowAllSelectItem(arc,
                                           maxValue));
        if (showAllActive)
          selectedIndex = 0;
      }
    }

    for (blockIndex = firstBlockIndex;
         blockIndex <= lastBlockIndex;
         blockIndex++)
    {
      long blockStart = minValue + (blockIndex * blockSize);

      // if there is an offset, then adjust accordingly. for example, if the
      // offset is 7 (and the blockSize is 10), then the new blockStarts are:
      // 1-7, 8-17, 18-27, etc ...
      if (offset > 0)
        blockStart += (offset - blockSize);

      final int currentRecordSize;
      // check to see if this is the very first record set in a table using an
      // offset:
      if (blockStart < minValue)
      {
        // treat this specially. this is the 1-7 case from the example above:
        blockStart = minValue;
        currentRecordSize = offset;
      }
      else
      {
        currentRecordSize = blockSize;
      }


      // return immediately if the start of the next range is not available.
      if (maxUnknown)
      {
        if (!isRowAvailable(component, (int)blockStart - 1))
          return selectedIndex;
      }

      String text;
      // Need "Previous..."
      if ((blockIndex == firstBlockIndex) &&
          (blockIndex != 0))
      {
        text = arc.getTranslatedString(_PREVIOUS_TEXT_KEY);
      }

      // Need "More..." (on the last block, either 'cause
      // the total number of blocks is unknown or we've shown enough blocks
      // However, don't show More... if the total number of blocks is unknown,
      // and we checked and found out that the start of the next block doesn't
      // exist.
      else if ((blockIndex == lastBlockIndex) &&
               (maxUnknown || (lastBlockIndex < maxBlockIndex)))
      {
        text = arc.getTranslatedString(_MORE_TEXT_KEY);
      }
      else
      {
        text = null;
      }

      // =-=AEW I don't understand this next line...
      long currValue = showAllActive ? minValue - 1 : value;// Don't select

      SelectItem item = _createNavigationItem(context,
                                              arc,
                                              component,
                                              blockStart,
                                              currentRecordSize,
                                              maxValue,
                                              text,
                                              rangeLabel);
      if ((currValue >= blockStart) &&
          (currValue <  (blockStart + currentRecordSize)))
      {
        selectedIndex = items.size();
      }

      items.add(item);
    }

    return selectedIndex;
  }

  private SelectItem _createShowAllSelectItem(
    RenderingContext arc,
    long                maxValue)
  {
    String[] parameters = new String[]{IntegerUtils.getString(maxValue)};
    String showAllText = XhtmlUtils.getFormattedString(
                                     arc.getTranslatedString(_SHOW_ALL_KEY),
                                     parameters);

    return new SelectItem(XhtmlConstants.VALUE_SHOW_ALL,
                          showAllText);
  }

  // create a choice option when max value is known
  private SelectItem _createNavigationItem(
    FacesContext        context,
    RenderingContext arc,
    UIComponent         component,
    long             blockStart,
    int              blockSize,
    long             maxValue,
    String           text,
    UIComponent      rangeLabel
    )
  {
    // if text isn't null, it is More or Previous.
    if (text == null)
      text = _getRangeString(context,
                             arc,
                             component,
                             blockStart,
                             blockSize,
                             maxValue,
                             rangeLabel);

    return new SelectItem(IntegerUtils.getString(blockStart),
                          text);
  }


  /**
   * Returns true if disabled navigation items should be shown
   */
  protected boolean disabledNavigationShown()
  {
    return true;
  }



  // create one of the text links for navigation
  private void _renderLink(
    FacesContext        context,
    RenderingContext arc,
    boolean             isNext,
    String              onclick,
    int                 records,
    String              id,
    String              source,
    long                 value ) throws IOException
  {

    String text = getBlockString(arc, isNext, records);
    boolean isEnabled = ((onclick != null) && (records > 0));
    ResponseWriter writer = context.getResponseWriter();
   
    // if we have more than one record and browser is js capable then
    // render as a link
    if (isEnabled)
    {
      writer.startElement("a", null);
      writer.writeURIAttribute("href", "#", null);
      writer.writeAttribute("onclick", onclick, null);

      // The navBar needs its initial focus to be on the Next button,
      // according to the BLAF. Render a special id on the Next button
      // if this navBar is to have the initial focus. (unless it needs
      // initial focus, the Next button does not have an id on it)
      if (isNext)
      {
        String linkID = _getIDForFocus(arc, id);
        writer.writeAttribute("id", linkID, null);
      }

      renderStyleClass(context, arc, SkinSelectors.NAV_BAR_ALINK_STYLE_CLASS);
      writer.writeText(text, null);
      writer.endElement("a");
    }
    // if we don't have any record then just render as <span> element
    else if (records < 1)
    {
      writer.startElement("span", null);
      renderStyleClass(context, arc, SkinSelectors.NAV_BAR_ILINK_STYLE_CLASS);
      writer.writeText(text, null);
      writer.endElement("span");
    }
   
    // For Non-JavaScript browsers, render a submit element
    // (<input type = "submit"/> ). Encode the the name attribute with the 
    // parameter name and value thus it would enable the browsers to 
    // include the name of this element in its payLoad if it submits the
     
    else
    {
      String nameAttri = XhtmlUtils.getEncodedParameter
                                       (XhtmlConstants.SOURCE_PARAM)
                         + XhtmlUtils.getEncodedParameter(source)
                         + XhtmlUtils.getEncodedParameter
                                       (XhtmlConstants.EVENT_PARAM)
                         + XhtmlUtils.getEncodedParameter
                                       (XhtmlConstants.GOTO_EVENT)
                         + XhtmlUtils.getEncodedParameter
                                       (XhtmlConstants.VALUE_PARAM)
                         + IntegerUtils.getString(value);

      writer.startElement("input", null);
      writer.writeAttribute("type", "submit", null);
      writer.writeAttribute("name", nameAttri, null);
      writer.writeAttribute("value", text, "text");
      // This style makes a button to appear as a link
      writer.writeAttribute("style",
        "border:none;background:inherit;text-decoration:underline;",null);
      renderStyleClass(context, arc, SkinSelectors.NAV_BAR_ALINK_STYLE_CLASS);
      writer.endElement("input");
    }

 }


  /**
   */
  protected Icon getIcon(
    RenderingContext arc,
    boolean             isNext,
    boolean             isEnabled
    )
  {
    // get the image location
    String iconName;

    if (isNext)
    {
      if (isEnabled)
      {
        iconName   = SkinSelectors.AF_SELECT_RANGE_CHOICE_BAR_NEXT_ICON_NAME;
      }
      else
      {
        iconName   = SkinSelectors.AF_SELECT_RANGE_CHOICE_BAR_NEXT_DISABLED_ICON_NAME;
      }
    }
    else
    {
      if (isEnabled)
      {
        iconName   = SkinSelectors.AF_SELECT_RANGE_CHOICE_BAR_PREV_ICON_NAME;
      }
      else
      {
        iconName   = SkinSelectors.AF_SELECT_RANGE_CHOICE_BAR_PREV_DISABLED_ICON_NAME;
      }
    }

    return arc.getIcon(iconName);
  }

  protected String getIconTitleKey(
    boolean             isNext,
    boolean             isEnabled
    )
  {
    if (isNext)
    {
      return (isEnabled) ? _NEXT_DESC_KEY : _DISABLED_NEXT_DESC_KEY;
    }
    else
    {
      return (isEnabled) ? _PREVIOUS_DESC_KEY : _DISABLED_PREVIOUS_DESC_KEY;
    }
  }

  /**
   * @todo GENERIC FIX: need to use renderURIAttribute() in Icon
   *  code to output the Icon URL.  But that'll break a zillion
   *  renderkit tests.
   */
  private void _renderArrow(
    FacesContext        context,
    RenderingContext arc,
    Icon                icon,
    boolean             isNext,
    String              onclick) throws IOException
  {
    ResponseWriter writer = context.getResponseWriter();
    if (onclick != null)
    {
      writer.startElement("a", null);
      writer.writeURIAttribute("href", "#", null);
      writer.writeAttribute("onclick", onclick, null);
    }

    boolean isEnabled = (onclick != null);
    String titleKey = getIconTitleKey(isNext, isEnabled);
    String title = arc.getTranslatedString(titleKey);
    OutputUtils.renderIcon(context, arc, icon, title, null);

    if (onclick != null)
      writer.endElement("a");
  }


  /**
   * Gets the string to use for next/previous links
   * in a table navigation bar.
   */
  protected String getBlockString(
    RenderingContext arc,
    boolean             isNext,
    int                 numRecords
    )
  {
    // check to make sure that we have some records in this direction:
    if (numRecords > 0)
    {
      String pattern = (isNext)
      ? arc.getTranslatedString("af_selectRangeChoiceBar.NEXT")
      : arc.getTranslatedString("af_selectRangeChoiceBar.PREVIOUS");
      String value = IntegerUtils.getString(numRecords);

      return XhtmlUtils.getFormattedString(pattern, new String[]{value});
    }
    else
    {
      // since we don't have any records, we are going to display some
      // disabled text. see bug 1740486.
      String text = (isNext)
        ? arc.getTranslatedString("af_selectRangeChoiceBar.DISABLED_NEXT")
        : arc.getTranslatedString("af_selectRangeChoiceBar.DISABLED_PREVIOUS");
      return text;
    }
  }

  //
  /**
   * get the string for the current range
   * @todo We probably shouldn't use the same substitution string
   *  when we know the max and when we don't.  We should have two:
   *   {0}-{1} of {2}
   *   {0}-{1}
   *  (and not bother with the "of" substitution)
   */
  @SuppressWarnings("unchecked")
  private String _getRangeString(
    FacesContext        context,
    RenderingContext arc,
    UIComponent      component,
    long             start,
    int              visibleItemCount,
    long             total,
    UIComponent      rangeLabel
    )
  {

    // how many records do we really see now?
    long currVisible = (total == XhtmlConstants.MAX_VALUE_UNKNOWN)
                           ? visibleItemCount
                           : total - start + 1;

    if (currVisible > visibleItemCount)
      currVisible = visibleItemCount;


    // getItemLabel from selectItem in the facet if it exists.
    if ((rangeLabel != null) &&
        ((rangeLabel instanceof UISelectItem) ||
         (rangeLabel instanceof UIXSelectItem)))
    {
      Range range = new Range();
      // getting data is zero-indexed, whereas start is 1-indexed.
      setRowIndex(component, (int)start-1);
      Object startRow = getRowData(component);
      range.setStart(startRow);

      // get end row. If the end row doesn't exist, find the
      // last row that does exist. The row indices are zero-indexed.
      int endIndex = (int)(start + currVisible - 2);

      endIndex = _setToExistingEndRow(component, (int)start -1, endIndex);
      setRowIndex(component, endIndex);

      // ok, we are sure we have an existing end row set, so set the end
      // parameter on the range object.
      range.setEnd(getRowData(component));

      Object old = null;
      String var = getVar(getFacesBean(component));
      if (var != null)
      {
        Map<String, Object> requestMap =
          context.getExternalContext().getRequestMap();
        old = requestMap.put(var, range);
      }

      String label = (rangeLabel instanceof UISelectItem)
        ? ((UISelectItem) rangeLabel).getItemLabel()
        : toString(((UIXSelectItem) rangeLabel).getAttributes().get("label"));

      if (var != null)
      {
        Map<String, Object> requestMap =
          context.getExternalContext().getRequestMap();

        if (old == null)
          requestMap.remove(var);
        else
          requestMap.put(var, old);
      }


      return label;
    }
    else
    {
      // formatter for generating the page string
      String startParam = IntegerUtils.getString(start);


      int endIndex = (int)(start + currVisible - 2);
      endIndex = _setToExistingEndRow(component, (int)start -1, endIndex);
      String endParam = IntegerUtils.getString(endIndex+1);
      String   pattern = null;
      String[] parameters = null;

      if ((total == XhtmlConstants.MAX_VALUE_UNKNOWN))
      {
        pattern = arc.getTranslatedString(_MULTI_RANGE_NO_TOTAL_FORMAT_STRING);
        parameters = new String[]
        {
          startParam,
          endParam
        };

      }
      else
      {

        pattern = arc.getTranslatedString(_MULTI_RANGE_TOTAL_FORMAT_STRING);
        parameters = new String[]
        {
          startParam,
          endParam,
          IntegerUtils.getString(total)
        };

      }

      return XhtmlUtils.getFormattedString(pattern, parameters);
    }
  }

  /**
   * Find the highest end row in the range from startRowIndex to endRowIndex
   * inclusive that exists, and make that row current
   * (by calling selectRange.setRowIndex).
   * @param startRowIndex. the start index for the first row in this range.
   * @param endRowIndex the initial end row. that is, the row index for the
   * last row in this range.
   * @return the index of the highest end row that exists.
   */
  private int _setToExistingEndRow(
    UIComponent  component,
    int          startRowIndex,
    int          endRowIndex )
  {
    boolean rowAvailable = isRowAvailable(component, endRowIndex);

    // make sure that the end row exists. If it doesn't, then loop
    // back from the end until we see that it does exist.
    while (!rowAvailable && endRowIndex >= startRowIndex)
    {
      endRowIndex--;
      rowAvailable = isRowAvailable(component, endRowIndex);
    }

    return endRowIndex;
  }


  // don't render as a table in certain locations like a page button bar
  static boolean __renderAsTable(
    UIComponent component
    )
  {
    UIComponent parent = XhtmlUtils.getStructuralParent(component);
    if ((parent instanceof UIXPanel) &&
        ("org.apache.myfaces.trinidad.ButtonBar".equals(parent.getRendererType()) ||
         "org.apache.myfaces.trinidad.rich.ButtonBar".equals(parent.getRendererType())))
    {
      return false;
    }

    return true;
  }


  /**
   * Writes the separator between two elements
   */
  protected void renderItemSpacer(
   FacesContext context,
   RenderingContext arc) throws IOException
  {
    if (isPDA(arc))
    {
      context.getResponseWriter().writeText(XhtmlConstants.NBSP_STRING, null);
    }
    else
    {
      renderSpacer(context, arc, "5", "1");
    }
  }


  /**
   * Writes the separator between two elements
   */
  private void _renderSpacerCell(
    FacesContext       context,
    RenderingContext arc
    ) throws IOException
  {
    ResponseWriter writer = context.getResponseWriter();

    writer.startElement("td", null);
    renderItemSpacer(context, arc);
    writer.endElement("td");
  }

  /**
   * render the "td".
   */
  private void _renderStartTableCell(
    ResponseWriter writer,
    String         id,
    boolean        alreadyRenderedId) throws IOException
  {
    writer.startElement("td", null);
    if (!alreadyRenderedId)
    {
      writer.writeAttribute("id", id, null);
    }
  }

  private String _getIDForFocus(
    RenderingContext arc,
    String              baseId
    )
  {
    // The navBar needs its initial focus to be on the Next button,
    // according to the BLAF. Render a special id on the Next button
    // if this navBar is to have the initial focus. (unless it needs
    // initial focus, the Next button does not have an id on it)
    // We get body's initialFocus attribute off of the rendering context.
    // If this is equal to the navBar's id, then we make up a new id
    // for the Next button.

    Object initialFocusID =
      arc.getProperties().get(XhtmlConstants.INITIAL_FOCUS_CONTEXT_PROPERTY);

    String id = null;
    if ((initialFocusID != null) && initialFocusID.equals(baseId))
    {
      // make up an id to use for the initial focus.
      String focus = "-focus";
      StringBuilder buffer = new StringBuilder(baseId.length()+
                                               focus.length());
      buffer.append(baseId);
      buffer.append(focus);
      id = buffer.toString();
      // set the new id on the rendering context so that the body
      // renderer can write it out to a script variable.
      // A side-effect is that the initialFocusID in subsequent calls will
      // never equal the navBar's id.
      arc.getProperties().put(XhtmlConstants.INITIAL_FOCUS_CONTEXT_PROPERTY, id);
    }


    return id;
  }

  //
  // Private variables
  //


  private PropertyKey _rowsKey;
  private PropertyKey _firstKey;
  private PropertyKey _showAllKey;
  private PropertyKey _immediateKey;
  private PropertyKey _varKey;

  // resource keys
  static private final String _PREVIOUS_DESC_KEY =
  "af_selectRangeChoiceBar.PREVIOUS_TIP";
  static private final String _NEXT_DESC_KEY =
  "af_selectRangeChoiceBar.NEXT_TIP";
  static private final String _DISABLED_PREVIOUS_DESC_KEY =
  "af_selectRangeChoiceBar.PREV_DISABLED_TIP";
  static private final String _DISABLED_NEXT_DESC_KEY =
  "af_selectRangeChoiceBar.NEXT_DISABLED_TIP";
  static private final String _CHOICE_TIP_KEY =
  "af_selectRangeChoiceBar.CHOICE_TIP";
  static private final String _MULTI_RANGE_NO_TOTAL_FORMAT_STRING =
  "af_selectRangeChoiceBar.CHOICE_FORMAT_NO_TOTAL";
  static private final String _MULTI_RANGE_TOTAL_FORMAT_STRING =
  "af_selectRangeChoiceBar.CHOICE_FORMAT_TOTAL";
  static private final String _PREVIOUS_TEXT_KEY =
  "af_selectRangeChoiceBar.PREVIOUS_OPTION";
  static private final String _MORE_TEXT_KEY     =
  "af_selectRangeChoiceBar.MORE_OPTION";
  static private final String _SHOW_ALL_KEY     =
  "af_selectRangeChoiceBar.SHOW_ALL";

  /**
   * @todo This should be pulled from a skin property
   */
  static private final long   _MAX_VISIBLE_OPTIONS = 30L;

  // on focus handler for the form case.  We save off the old value so that
  // we can restore it if the validation chokes
  private static final String _CHOICE_FORM_ON_FOCUS =
  "this._lastValue = this.selectedIndex";

  static private final String _CHOICE_ID_SUFFIX = "c";

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