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

import java.util.Iterator;

import javax.faces.application.Application;
import javax.faces.application.FacesMessage;
import javax.faces.component.EditableValueHolder;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.faces.convert.ConverterException;
import javax.faces.el.EvaluationException;
import javax.faces.el.MethodBinding;
import javax.faces.el.ValueBinding;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.FacesEvent;
import javax.faces.event.ValueChangeEvent;
import javax.faces.render.Renderer;
import javax.faces.validator.Validator;
import javax.faces.validator.ValidatorException;

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.util.MessageFactory;
import org.apache.myfaces.trinidad.util.LabeledFacesMessage;

/**
 * Base class for components that have a value.
 * <p>
 * @version $Name:  $ ($Revision: 547038 $) $Date: 2007-06-13 23:18:33 +0200 (Wed, 13 Jun 2007) $
 * @todo Autogen more of the properties, getters and setters
 */
abstract public class UIXEditableValueTemplate
  extends UIXValue implements EditableValueHolder
{
/**/  static public final FacesBean.Type TYPE = new FacesBean.Type(
/**/    UIXValue.TYPE);

  static public final PropertyKey VALIDATORS_KEY =
    TYPE.registerKey("validators", Validator[].class, PropertyKey.CAP_LIST);

  static public final String REQUIRED_MESSAGE_ID =
    "org.apache.myfaces.trinidad.UIXEditableValue.REQUIRED";
  static public final String CONVERSION_MESSAGE_ID =
    "org.apache.myfaces.trinidad.UIXEditableValue.CONVERSION";



  /**
   * Convenience method to reset this component's value to an
   * uninitialized state, by resetting the local value and
   * submitted values to null (ensuring that {@link #isLocalValueSet}
   * is false), and setting "valid" to true.
   */
  public void resetValue()
  {
    setValue(null);
    setSubmittedValue(null);
    setLocalValueSet(false);
    setValid(true);
  }


  // ----------------------------------------------------- Validators Methods



  public void addValidator(Validator validator)
  {
    if (validator == null)
      throw new NullPointerException();

    getFacesBean().addEntry(VALIDATORS_KEY, validator);
  }


  public Validator[] getValidators()
  {
    return (Validator[]) getFacesBean().getEntries(VALIDATORS_KEY,
                                                   Validator.class);
  }

  public void removeValidator(Validator validator)
  {
    getFacesBean().removeEntry(VALIDATORS_KEY, validator);
  }


  /**
   */
  public void validate(FacesContext context)
  {
    if (context == null)
      throw new NullPointerException();

    // Submitted value == null means "the component was not submitted
    // at all";  validation should not continue
    Object submittedValue = getSubmittedValue();
    if (submittedValue == null)
      return;

    Object newValue = null;
    try
    {
      newValue = getConvertedValue(context, submittedValue);
    }
    catch (ConverterException ce)
    {
      _addConversionErrorMessage(context, ce, submittedValue);
      setValid(false);
    }

    validateValue(context, newValue);

    // If our value is valid, store the new value, erase the
    // "submitted" value, and emit a ValueChangeEvent if appropriate
    if (isValid())
    {
      Object previous = getValue();
      setSubmittedValue(null);
      if (compareValues(previous, newValue))
      {
        setValue(newValue);
        queueEvent(new ValueChangeEvent(this, previous, newValue));
      }
    }
  }


  /**
   * In addition to to the default
   * {@link javax.faces.component.UIComponent#broadcast}
   * processing, pass the {@link ValueChangeEvent} being broadcast to the
   * method referenced by <code>valueChangeListener</code> (if any).
   *
   * @param event {@link FacesEvent} to be broadcast
   *
   * @exception AbortProcessingException Signal the JavaServer Faces
   *  implementation that no further processing on the current event
   *  should be performed
   * @exception IllegalArgumentException if the implementation class
   *  of this {@link FacesEvent} is not supported by this component
   * @exception NullPointerException if <code>event</code> is
   * <code>null</code>
   */
  @Override
  public void broadcast(FacesEvent event)
        throws AbortProcessingException
  {
    // Perform standard superclass processing
    super.broadcast(event);

    if (event instanceof ValueChangeEvent)
    {
      broadcastToMethodBinding(event, getValueChangeListener());
    }
  }


  /**
   * In addition to the standard <code>processDecodes</code> behavior
   * inherited from {@link UIXComponentBase}, calls
   * <code>validate()</code> if the the <code>immediate</code>
   * property is true.  Iif the component is invalid afterwards or
   * a <code>RuntimeException</code> is thrown, calls
   * {@link FacesContext#renderResponse}.
   */
  @Override
  public void processDecodes(FacesContext context)
  {
    setValid(true);

    // Skip processing if our rendered flag is false
    if (!isRendered())
      return;

    super.processDecodes(context);

    if (isImmediate())
      _executeValidate(context);
  }

  @Override
  public void processUpdates(FacesContext context)
  {
    super.processUpdates(context);

    // Skip processing if our rendered flag is false
    if (!isRendered())
      return;

    // Process this component itself
    updateModel(context);

    if (!isValid())
    {
      context.renderResponse();
    }
  }

  @Override
  public void processValidators(FacesContext context)
  {
    super.processValidators(context);

    // Skip processing if our rendered flag is false
    if (!isRendered())
      return;

    if (!isImmediate())
      _executeValidate(context);
  }

  // TODO Better error messages when update model fails.
  public void updateModel(FacesContext context)
  {
    if (context == null)
      throw new NullPointerException();

    if (!isValid() || !isLocalValueSet())
      return;

    ValueBinding binding = getFacesBean().getValueBinding(VALUE_KEY);
    if (binding == null)
      return;

    try
    {
      Object localValue = getLocalValue();
      binding.setValue(context, localValue);
      setValue(null);
      setLocalValueSet(false);
      if (_LOG.isFiner())
      {
        _LOG.finer("Wrote value {0} to model {1} in component {2}",
                   new Object[]{localValue,
                                binding.getExpressionString(),
                                this});
      }
    }
    catch (RuntimeException e)
    {
      // exceptions at this point can occur during normal
      // bean attribute level validation:
      if (_LOG.isFine())
      {
        _LOG.fine("Error updating binding ({0})",
                    binding.getExpressionString());
        _LOG.fine(e);
      }

      setValid(false);
      FacesMessage message = MessageFactory.getMessage(e);
      message = _wrapMessage(message);
      context.addMessage(getClientId(context), message);
    }
  }

  /**
   */
  @SuppressWarnings("unchecked")
  protected void validateValue(FacesContext context, Object newValue)
  {
    if (!isValid())
      return;

    // If our value is empty, only check the required property
    if (isEmpty(newValue))
    {
      if (isRequired())
      {
        FacesMessage message = _getRequiredFacesMessage(context);
        context.addMessage(getClientId(context), message);
        setValid(false);
      }
    }
    // If our value is not empty, call all validators
    else
    {
      Iterator<Validator> validators = (Iterator<Validator>)getFacesBean().entries(VALIDATORS_KEY);
      while (validators.hasNext())
      {
        Validator validator = validators.next();
        try
        {
          validator.validate(context, this, newValue);
        }
        catch (ValidatorException ve)
        {
          // If the validator throws an exception, we're
          // invalid, and we need to add a message
          setValid(false);
          FacesMessage message = ve.getFacesMessage();
          if (message != null)
          {
            message.setSeverity(FacesMessage.SEVERITY_ERROR);
            message = _wrapMessage(message);
            context.addMessage(getClientId(context), message);
          }
        }
      }

      MethodBinding validatorBinding = getValidator();
      if (validatorBinding != null)
      {
        try
        {
          validatorBinding.invoke(context,
                                  new Object[] { context, this, newValue});
        }
        catch (EvaluationException ee)
        {
          Throwable cause = ee.getCause();
          if (cause instanceof ValidatorException)
          {
            ValidatorException ve = (ValidatorException) cause;

            // If the validator throws an exception, we're
            // invalid, and we need to add a message
            setValid(false);
            FacesMessage message = ve.getFacesMessage();
            if (message != null)
            {
              message.setSeverity(FacesMessage.SEVERITY_ERROR);
              message = _wrapMessage(message);
              context.addMessage(getClientId(context), message);
            }
          }
          else
          {
            // Otherwise, rethrow the EvaluationException
            throw ee;
          }
        }
      }
    }
  }


  protected String getRequiredMessageKey()
  {
    return REQUIRED_MESSAGE_ID;
  }

  /**
   *
   */
  protected Object getConvertedValue(
    FacesContext context,
    Object       submittedValue) throws ConverterException
  {
    Renderer renderer = getRenderer(context);
    Object newValue = null;

    if (_LOG.isFine())
    {
      _LOG.fine("Converting from " + submittedValue + "(" +
                submittedValue.getClass() + ")");
    }

    if (renderer != null)
    {
      newValue = renderer.getConvertedValue(context, this,
                                            submittedValue);

      if (_LOG.isFine())
      {
        _LOG.fine("Renderer " + renderer + " returned value " + newValue + "(" +
                  ((newValue != null) ? newValue.getClass().getName() : "null") + ")");
      }
    }
    else if (submittedValue instanceof String)
    {
      // If there's no Renderer, and we've got a String,
      // run it through the Converter (if any)
      Converter converter = _getConverterWithType(context);
      if (converter != null)
      {
        newValue = converter.getAsObject(context, this,
                                         (String) submittedValue);
      }
      else
      {
        newValue = submittedValue;
      }
    }
    else
    {
      newValue = submittedValue;
    }

    return newValue;
  }

  /**
   * <p>Return <code>true</code> if the new value is different from the
   * previous value.</p>
   *
   * @param previous old value of this component (if any)
   * @param value new value of this component (if any)
   */
  protected boolean compareValues(Object previous, Object value)
  {
    if (isEmpty(previous)) // bug 4268807
      return !isEmpty(value);

    return !previous.equals(value);
  }

  /**
   * <p>Return <code>true</code> if the value is empty.</p>
   */
  protected boolean isEmpty(Object value)
  {
    if (value == null)
      return true;

    return ((value instanceof String) &&
            (((String) value).trim().length() == 0));
  }


  /**
   * Executes validation logic.
   */
  private void _executeValidate(FacesContext context)
  {
    try
    {
      validate(context);
    }
    catch (RuntimeException e)
    {
      context.renderResponse();
      throw e;
    }

    if (!isValid())
    {
      context.renderResponse();
    }
  }


  // We currently use 'label' for the validation failed message
  private Object _getLabel()
  {
    Object o = getAttributes().get("label");
    if (o == null)
      o = getValueBinding("label");

    return o;
  }

  private Object _getRequiredMessageDetail()
  {
    Object o = getAttributes().get("requiredMessageDetail");
      if (o == null)
       o = getValueBinding("requiredMessageDetail");

    return o;
  }

  private FacesMessage _getRequiredFacesMessage(FacesContext context)
  {
    Object customMessageDetail = _getRequiredMessageDetail();
    FacesMessage message;
    Object label = _getLabel();

    // if message is null then a custom message was not set.
    message = MessageFactory.getMessage(context,
                                        getRequiredMessageKey(),
                                        customMessageDetail,
                                        new Object[]{label},
                                        label);
    return message;
  }

  private void _addConversionErrorMessage(
     FacesContext       context,
     ConverterException ce,
     Object             value)
  {
    FacesMessage message = ce.getFacesMessage();

    if (message == null)
    {
      Object label = _getLabel();
      message = MessageFactory.getMessage(context,
                                          CONVERSION_MESSAGE_ID,
                                          new Object[]{label, value,
                                                       ce.getMessage()},
                                          label);
    }
    else
    {
      message = _wrapMessage(message);
    }

    context.addMessage(getClientId(context), message);
  }

  private Converter _getConverterWithType(FacesContext context)
  {
    Converter converter = getConverter();
    if (converter != null)
    {
      return converter;
    }

    ValueBinding valueBinding = getValueBinding("value");
    if (valueBinding == null)
    {
      return null;
    }

    Class<?> converterType = valueBinding.getType(context);
    // if converterType is null, String, or Object, assume
    // no conversion is needed
    if (converterType == null ||
        converterType == String.class ||
        converterType == Object.class)
    {
      return null;
    }

    // if getType returns a type for which we support a default
    // conversion, acquire an appropriate converter instance.
    try
    {
      Application application = context.getApplication();
      return application.createConverter(converterType);
    }
    catch (Exception e)
    {
      return null;
    }
  }

  private FacesMessage _wrapMessage(FacesMessage original)
  {
    if (original instanceof LabeledFacesMessage)
      return original;

    return new FacesMessageWrapper(original, _getLabel());
  }

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