/*
 *  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.List;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.convert.Converter;
import javax.faces.convert.ConverterException;

import javax.faces.model.SelectItem;
import javax.faces.model.SelectItemGroup;

import org.apache.myfaces.trinidad.logging.TrinidadLogger;

import org.apache.myfaces.trinidad.bean.FacesBean;
import org.apache.myfaces.trinidad.bean.PropertyKey;

import org.apache.myfaces.trinidad.component.UIXSelectOne;

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.uix.SelectItemSupport;
import org.apache.myfaces.trinidad.util.IntegerUtils;

/**
 */
abstract public class SimpleSelectOneRenderer extends FormInputRenderer
{
  public SimpleSelectOneRenderer(FacesBean.Type type)
  {
    super(type);
  }

  @Override
  protected void findTypeConstants(FacesBean.Type type)
  {
    super.findTypeConstants(type);
    _valuePassThruKey = type.findKey("valuePassThru");
  }

  @Override
  protected Object getSubmittedValue(
    FacesContext context,
    UIComponent  component,
    String       clientId)
  {
    Object submittedValue = super.getSubmittedValue(context,
                                                    component,
                                                    clientId);
    boolean valuePassThru = getValuePassThru(getFacesBean(component));

    if (submittedValue == null && valuePassThru)
      submittedValue = "";

    return submittedValue;
  }

  /**
   * Return the value to output for an item.
   */
  static public Object getItemValue(
    FacesContext context,
    UIComponent  component,
    SelectItem   item,
    Converter    converter,
    boolean      valuePassThru,
    int          index)
  {
    if (!valuePassThru)
    {
      return IntegerUtils.getString(index);
    }
    else
    {
      Object itemValue = item.getValue();
      if ((itemValue != null) && (converter != null))
      {
        itemValue = converter.getAsString(context,
                                          component,
                                          itemValue);
      }

      return itemValue;
    }
  }

  /**
   * @todo Move to utility class?
   */
  static public boolean encodeOption(
    FacesContext        context,
    RenderingContext arc,
    UIComponent         component,
    SelectItem          item,
    Converter           converter,
    boolean             valuePassThru,
    int                 index,
    boolean             isSelected) throws IOException
  {
    if (item == null)
      return false;

    if (item.isDisabled())
    {
      if (!Boolean.TRUE.equals(arc.getAgent().getCapabilities().get(
                          TrinidadAgent.CAP_SUPPORTS_DISABLED_OPTIONS)))
        return false;
    }

    Object itemValue = getItemValue(context,
                                    component,
                                    item,
                                    converter,
                                    valuePassThru,
                                    index);

    ResponseWriter writer = context.getResponseWriter();

    writer.startElement("option", null);

    if (item.isDisabled())
      writer.writeAttribute("disabled", Boolean.TRUE, null);

    // Never write out null, because that will result in the label
    // getting submitted, instead of null.
    if (itemValue == null)
      itemValue="";
    writer.writeAttribute("value", itemValue, null);

    if (isSelected)
      writer.writeAttribute("selected", Boolean.TRUE, null);

    // For reasons that aren't especially clear to me, we're getting
    // passed the empty string for our title.
    String description = item.getDescription();
    if ((description != null) && !"".equals(description))
      writer.writeAttribute("title", description, null);

    writer.writeText(item.getLabel(), null);

    writer.endElement("option");

    return true;
  }

  //
  // DECODE BEHAVIOR
  //
  @Override
  public Object getConvertedValue(
    FacesContext context,
    UIComponent  component,
    Object       submittedValue) throws ConverterException
  {
    boolean valuePassThru = getValuePassThru(getFacesBean(component));

    if (!valuePassThru)
    {
      return _convertIndexedSubmittedValue(context, component, submittedValue);
    }
    else
    {
      return super.getConvertedValue(context, component, submittedValue);
    }
  }


  /**
   * Call this method only when the valuePassThru attribute on the selectOne
   * component is not set to true.
   * This indicates that the client-side value
   * is an index. We need to convert that index into its real value.
   * @param component
   * @param submittedValue the submittedValue. Since this method is only
   *  called when the valuePassThru attribute on the selectOne component is
   *  not true, then the submittedValue in this case is an index into a List.
   * @return the Object value at that index specified in submittedValue,
   *    or null.
   */
  private Object _convertIndexedSubmittedValue(
    FacesContext context,
    UIComponent  component,
    Object       submittedValue)
  {
    FacesBean bean = getFacesBean(component);
    Converter converter = getConverter(bean);
    if ( converter == null)
      converter = getDefaultConverter(context, bean);

    List<SelectItem> selectItems = getSelectItems(component, converter, true);
    
    int index = __getIndex(submittedValue, selectItems);
    if (index < 0)
      return null;

    SelectItem item = selectItems.get(index);
    if (item != null)
      return item.getValue();
    else
      return null;
  }


  //
  // ENCODE BEHAVIOR
  //
  /*
   */
  @Override
  protected void encodeAllAsElement(
    FacesContext        context,
    RenderingContext    arc,
    UIComponent         component,
    FacesBean           bean) throws IOException
  {
    Converter converter = getConverter(bean);
    if ( converter == null)
      converter = getDefaultConverter(context, bean);
    boolean valuePassThru = getValuePassThru(bean);

    if (isAutoSubmit(bean))
      AutoSubmitUtils.writeDependencies(context, arc);

    // Only add in validators and converters when we're in valuePassThru
    // mode; otherwise, there's not enough on the client to even consider
    FormData fData = arc.getFormData();
    if (fData != null)
    {
      ((CoreFormData) fData).addOnSubmitConverterValidators(component,
                      valuePassThru ? converter : null,
                      valuePassThru ? getValidators(bean) : null,
                      getClientId(context, component),
                      isImmediate(bean),
                      getRequired(bean),
                      getRequiredMessageKey());
    }

    List<SelectItem> selectItems = getSelectItems(component, converter, false);
    
    int selectedIndex = _getSelectedIndex(context,
                                          component,
                                          bean,
                                          selectItems,
                                          converter,
                                          valuePassThru);

    ResponseWriter writer = context.getResponseWriter();
    boolean simple = getSimple(bean);
    if (simple)
    {
      writer.startElement("span", component);
      // put the outer style class here, like af_selectOneRadio, styleClass,
      // inlineStyle, 'state' styles like p_AFDisabled, etc.
      renderRootDomElementStyles(context, arc, component, bean);
    }

    encodeElementContent(context,
                         arc,
                         component,
                         bean,
                         selectItems,
                         selectedIndex,
                         converter,
                         valuePassThru);



    if (isHiddenLabelRequired(arc))
      renderShortDescAsHiddenLabel(context, arc, component, bean);

    if (simple)
    {
      writer.endElement("span");
    }
  }

  abstract   protected void encodeElementContent(
    FacesContext        context,
    RenderingContext arc,
    UIComponent         component,
    FacesBean           bean,
    List<SelectItem>    selectItems,
    int                 selectedIndex,
    Converter           converter,
    boolean             valuePassThru) throws IOException;

  @Override
  protected void renderNonElementContent(
    FacesContext        context,
    RenderingContext    arc,
    UIComponent         component,
    FacesBean           bean) throws IOException
  {
    // http://issues.apache.org/jira/browse/ADFFACES-151
    // Getting default converter for null value leads to exception but
    // if value of component is null than there is no need to perform
    // this method
    if (getValue(bean) == null)
      return;

    Converter converter = getConverter(bean);
    if ( converter == null)
      converter = getDefaultConverter(context, bean);
    boolean valuePassThru = getValuePassThru(bean);

    // =-=AEW If needed, this could be made more efficient
    // by iterating through the list instead of getting
    // all the items
    List<SelectItem> selectItems = getSelectItems(component, converter, false);
    
    int selectedIndex = _getSelectedIndex(context,
                                          component,
                                          bean,
                                          selectItems,
                                          converter,
                                          valuePassThru);

    // If an item is selected, get its label.
    String text;
    if (selectedIndex >= 0)
    {
      SelectItem item = selectItems.get(selectedIndex);
      text = item.getLabel();
    }
    else
    {
      text = getUnselectedLabel(bean);
    }

    context.getResponseWriter().writeText(text, null);
  }

  @Override
  protected String getRequiredMessageKey()
  {
    return UIXSelectOne.REQUIRED_MESSAGE_ID;
  }

  protected List<SelectItem> getSelectItems(
    UIComponent component,
    Converter converter)
  {
    return getSelectItems(component, converter, false);
  }

  protected List<SelectItem> getSelectItems(
    UIComponent component,
    Converter converter,
    boolean filteredItems)
  {
    return SelectItemSupport.getSelectItems(component, converter, filteredItems);
  }

  protected boolean getValuePassThru(FacesBean bean)
  {
    Object o = bean.getProperty(_valuePassThruKey);
    if (o == null)
      o = _valuePassThruKey.getDefault();
    assert(o != null);
    return Boolean.TRUE.equals(o);
  }

  /**
   * @todo Move up to all SelectOnes?
   */
  protected String getUnselectedLabel(FacesBean bean)
  {
    return null;
  }

  /**
   * Convert a stringified index into an index, with range-checking.
   */
  static int __getIndex(
      Object submittedValue, 
      List<SelectItem> selectItems)
  {
    if ("".equals(submittedValue))
      return -1;

    try
    {
      int index = Integer.parseInt(submittedValue.toString());

      if (( -1 < index) && (countSelectItems(selectItems) > index))
      {
        return index;
      }
      else
      {
        // TODO Don't throw exception: message!
        throw new IndexOutOfBoundsException(_LOG.getMessage(
          "SELECTONE_SUBMITTEDVALUE_INDEX_OUTSIDE_BOUNDS", new Object[]{index, (selectItems.size() - 1)}));
      }
    }
    catch (NumberFormatException ne)
    {
      // TODO Don't throw exception: message!
      throw new NumberFormatException(_LOG.getMessage(
        "SELECTONE_CANNOT_CONVERT_SUBMITTEDVALUE_INDEX_INTO_INTEGER", new Object[]{submittedValue.toString(), ne}));
    }
  }

  private static int countSelectItems(List<SelectItem> selectItems)
  {
    int count = 0;
    for(SelectItem item : selectItems)
    {
      count += calcItems( item );
    }

    return count;
  }

  private static int calcItems(SelectItem item)
  {
    if(item instanceof SelectItemGroup)
    {
      int count = 0;
      SelectItem[] items;
      items = ((SelectItemGroup)item).getSelectItems();

      for(int i = 0; i < items.length; i++)
      {
        count += calcItems( items[i] );
      }

      return count;
    }

    return 1;
  }

  //
  // Find the selected item in the list
  //
  private int _findIndex(
      Object value, 
      List<SelectItem> selectItems)
  {
    int size = selectItems.size();
    int result;
    for (int i = 0; i < size; i++)
    {
      SelectItem item = selectItems.get(i);
      if (item == null)
        continue;

      result = resolveIndex(item, value, i);
      if(result >= 0)
      {
        return result;
      }
    }

    return -1;
  }

  private int resolveIndex(SelectItem item, Object value, int index)
  {
    if(item instanceof SelectItemGroup)
    {
      int result;
      for(SelectItem subItem : ((SelectItemGroup)item).getSelectItems())
      {
        result = resolveIndex( subItem, value, index++ );

        if(result >= 0)
        {
          return result;
        }
      }
    }
    else
    {
      if (value == null)
      {
        Object itemValue = item.getValue();
        // =-=AEW Treat the empty string as if it were null
        if ((itemValue == null) || "".equals(itemValue))
          return index;
      }
      else
      {
        if (value.equals(item.getValue()) || (value.getClass().isEnum() && item.getValue() != null && value.toString().equals( item.getValue().toString() )))
          return index;
      }
    }

    return -1;
  }

  protected String getAutoSubmitScript(
    RenderingContext arc,
    FacesBean           bean)
  {
    String source = LabelAndMessageRenderer.__getCachedClientId(arc);
    boolean immediate = isImmediate(bean);
    return AutoSubmitUtils.getSubmitScript(arc, source, XhtmlConstants.AUTOSUBMIT_EVENT, immediate);
  }



  private int _getSelectedIndex(
    FacesContext        context,
    UIComponent         component,
    FacesBean           bean,
    List<SelectItem>    selectItems,
    Converter           converter,
    boolean             valuePassThru)
  {
    Object submittedValue = getSubmittedValue(bean);
    // In passthru mode, if there's a submitted value, we just
    // have to turn it into an int and range-check it
    if ((submittedValue != null) && !valuePassThru)
    {
      return __getIndex(submittedValue, selectItems);
    }
    // Figure out the current value, whether it's submitted or not
    else
    {
      Object value;
      if (submittedValue == null)
      {
        value = getValue(bean);
      }
      else
      {
        // submittedValue: run it through the converter if there is one
        if (converter != null)
        {
          try
          {
            value = converter.getAsObject(context,
                                          component,
                                          submittedValue.toString());
          }
          // This shouldn't happen unless we got sent a bogus value;
          // log a warning and move on
          catch (ConverterException ce)
          {
            _LOG.warning(ce);
            value = null;
          }
        }
        else
          value = submittedValue;
      }

      int index = _findIndex(value, selectItems);
      if ((value != null) && (index < 0))
      {
        if (_LOG.isWarning())
          _LOG.warning("CANNOT_FIND_SELECTED_ITEM_MATCHING_VALUE", new Object[]{value, component});
      }

      return index;
    }
  }

  private PropertyKey _valuePassThruKey;

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