package org.jboss.seam.ui.validator;

import javax.el.ELException;
import javax.el.ValueExpression;
import javax.faces.FacesException;
import javax.faces.application.Application;
import javax.faces.application.FacesMessage;
import javax.faces.component.EditableValueHolder;
import javax.faces.component.NamingContainer;
import javax.faces.component.StateHolder;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.faces.convert.ConverterException;
import javax.faces.render.Renderer;
import javax.faces.validator.Validator;
import javax.faces.validator.ValidatorException;

import org.jboss.seam.faces.FacesMessages;
import org.jboss.seam.log.LogProvider;
import org.jboss.seam.log.Logging;
import org.jboss.seam.ui.component.UIDecorate;

/**
 * Validates two fields are equal
 * 
 * @author pmuir
 * @author Daniel Roth
 * 
 */
public class EqualityValidator implements Validator, StateHolder
{

   private static LogProvider log = Logging.getLogProvider(EqualityValidator.class);

   public static final String MESSAGE_ID = "org.jboss.seam.ui.validator.NOT_EQUAL";

   public static final String VALIDATOR_ID = "org.jboss.seam.ui.validator.Equality";

   private enum ValidOperation
   {
      EQUAL, NOT_EQUAL, GREATER, GREATER_OR_EQUAL, LESS, LESS_OR_EQUAL;
   }

   private String forId;
   private String message;
   private String messageId;
   private ValidOperation operator = ValidOperation.EQUAL; // Default

   public EqualityValidator()
   {
      this.message = "Value does not equal that in '#0'";
      this.messageId = MESSAGE_ID;
   }

   public EqualityValidator(String forId)
   {
      this();
      setFor(forId);
   }

   public EqualityValidator(String forId, String message, String messageId, String operator)
   {
      this(forId);
      if (message != null)
      {
         setMessage(message);
      }
      if (messageId != null)
      {
         setMessageId(messageId);
      }
      if (operator != null && !"".equals(operator))
      {
         if (ValidOperation.valueOf(operator.toUpperCase()) != null)
            setOperator(ValidOperation.valueOf(operator.toUpperCase()));
         else
            throw new IllegalStateException("Illegal operator. " + "Supported are: " + validOperatorsAsString());
      }

   }

   private String validOperatorsAsString()
   {
      StringBuffer buff = new StringBuffer();
      for (ValidOperation op : ValidOperation.values())
      {
         buff.append(op.name()).append(" ");
      }
      return buff.toString();
   }

   public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException
   {
      if (getFor() == null)
      {
         throw new FacesException("Must specify a component to validate equality against");
      }
      UIComponent otherComponent = findOtherComponent(component);

      Object other = new OtherComponent(context, otherComponent).getValue();
      if (value == null && other == null)
      {
         // Thats fine
      }
      else if (value != null)
      {
         switch (operator)
         {
         case EQUAL:
            if (!value.equals(other))
            {
               throwValidationException(value, otherComponent, other);
            }
            break;
         case NOT_EQUAL:
            if (value.equals(other))
            {
               throwValidationException(value, otherComponent, other);
            }
            break;
         case GREATER:
            if (!(compare(value, other) > 0))
            {
               throwValidationException(value, otherComponent, other);
            }
            break;
         case GREATER_OR_EQUAL:
            if (!(compare(value, other) >= 0))
            {
               throwValidationException(value, otherComponent, other);
            }
            break;
         case LESS:
            if (!(compare(value, other) < 0))
            {
               throwValidationException(value, otherComponent, other);
            }
            break;
         case LESS_OR_EQUAL:
            if (!(compare(value, other) <= 0))
            {
               throwValidationException(value, otherComponent, other);
            }
            break;
         }
      }
   }

   private UIComponent findOtherComponent(UIComponent component)
   {
      UIComponent otherComponent = component.findComponent(getFor());

      /**
       * If s:decorate is used, otherComponent will be null We have to look it
       * up ourselves
       */
      if (otherComponent == null)
      {
         UIComponent decorateParent = null;
         UIComponent parent = component.getParent();
         while (decorateParent == null && parent != null)
         {
            if (parent instanceof NamingContainer && !(parent instanceof UIDecorate))
            {
               decorateParent = parent;
            }
            parent = parent.getParent();
         }
         if (decorateParent != null)
            otherComponent = findChildComponent(decorateParent);

      }
      return otherComponent;
   }

   private UIComponent findChildComponent(UIComponent parent)
   {
      UIComponent ret = null;
      for (UIComponent child : parent.getChildren())
      {
         if (child.getId().equals(getFor()))
            ret = child;
         else
            ret = findChildComponent(child);
         if (ret != null)
            break;
      }
      return ret;

   }

   private int compare(Object value, Object other) throws IllegalArgumentException
   {
      try
      {
         Comparable c1 = (Comparable) value;
         return c1.compareTo(other);
      }
      catch (Exception e)
      {
         throw new IllegalArgumentException("Values are not comparable", e);
      }

   }

   private void throwValidationException(Object value, UIComponent otherComponent, Object other)
   {
      throw new ValidatorException(FacesMessages.createFacesMessage(FacesMessage.SEVERITY_ERROR, getMessageId(), getMessage(), otherComponent.getId(), value, other));
   }

   public String getFor()
   {
      return forId;
   }

   public void setFor(String forId)
   {
      this.forId = forId;
   }

   public String getMessage()
   {
      return message;
   }

   public void setMessage(String message)
   {
      this.message = message;
   }

   public String getMessageId()
   {
      return messageId;
   }

   public void setMessageId(String messageId)
   {
      this.messageId = messageId;
   }

   public boolean isTransient()
   {
      return false;
   }

   public void restoreState(FacesContext context, Object state)
   {
      Object[] fields = (Object[]) state;
      forId = (String) fields[0];
      message = (String) fields[1];
      messageId = (String) fields[2];
      operator = ValidOperation.valueOf((String) fields[3]);
   }

   public Object saveState(FacesContext context)
   {
      Object[] state = new Object[4];
      state[0] = forId;
      state[1] = message;
      state[2] = messageId;
      state[3] = operator.toString();
      return state;
   }

   public void setTransient(boolean newTransientValue)
   {
      // No-op
   }

   /**
    * Simple data structure to hold info on the "other" component
    * 
    * @author pmuir
    * 
    */
   private class OtherComponent
   {

      private FacesContext context;
      private UIComponent component;
      private EditableValueHolder editableValueHolder;

      private Renderer renderer;
      private Converter converter;

      public OtherComponent(FacesContext facesContext, UIComponent component)
      {
         this.component = component;
         this.context = facesContext;
         if (!(component instanceof EditableValueHolder))
         {
            throw new IllegalStateException("forId must reference an EditableValueHolder (\"input\") component");
         }
         editableValueHolder = (EditableValueHolder) component;
         initRenderer();
         initConverter();
      }

      private void initRenderer()
      {
         if (renderer == null)
         {
            String rendererType = component.getRendererType();
            if (rendererType != null)
            {
               renderer = context.getRenderKit().getRenderer(component.getFamily(), rendererType);
               if (null == renderer)
               {
                  log.trace("Can't get Renderer for type " + rendererType);
               }
            }
            else
            {
               if (log.isTraceEnabled())
               {
                  String id = component.getId();
                  id = (null != id) ? id : component.getClass().getName();
                  log.trace("No renderer-type for component " + id);
               }
            }
         }
      }

      private void initConverter()
      {
         converter = editableValueHolder.getConverter();
         if (converter != null)
         {
            return;
         }

         ValueExpression valueExpression = component.getValueExpression("value");
         if (valueExpression == null)
         {
            return;
         }

         Class converterType;
         try
         {
            converterType = valueExpression.getType(context.getELContext());
         }
         catch (ELException e)
         {
            throw new FacesException(e);
         }

         // if converterType is null, String, or Object, assume
         // no conversion is needed
         if (converterType == null || converterType == String.class || converterType == Object.class)
         {
            return;
         }

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

      private Object getConvertedValue(Object newSubmittedValue) throws ConverterException
      {

         Object newValue;

         if (renderer != null)
         {
            newValue = renderer.getConvertedValue(context, component, newSubmittedValue);
         }
         else if (newSubmittedValue instanceof String)
         {
            // If there's no Renderer, and we've got a String, run it
            // through
            // the Converter (if any)
            if (converter != null)
            {
               newValue = converter.getAsObject(context, component, (String) newSubmittedValue);
            }
            else
            {
               newValue = newSubmittedValue;
            }
         }
         else
         {
            newValue = newSubmittedValue;
         }
         return newValue;
      }

      public Object getValue()
      {
         /**
          * If conversion already is done, return value
          */
         if (editableValueHolder.isLocalValueSet())
         {
            return editableValueHolder.getValue();
         }

         /**
          * Convert submittet value
          */
         Object submittedValue = editableValueHolder.getLocalValue();
         if (submittedValue == null)
         {
            return null;
         }

         Object newValue = null;

         try
         {
            newValue = getConvertedValue(submittedValue);
         }
         catch (ConverterException ce)
         {
            // Any errors will be attached by JSF
            return null;
         }

         return newValue;
      }

   }

   public ValidOperation getOperator()
   {
      return operator;
   }

   public void setOperator(ValidOperation operator)
   {
      this.operator = operator;
   }
}
