package org.jboss.seam.core;

import static org.jboss.seam.annotations.Install.BUILT_IN;

import java.beans.FeatureDescriptor;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.el.ELContext;
import javax.el.ELException;
import javax.el.ELResolver;
import javax.el.PropertyNotFoundException;
import javax.el.PropertyNotWritableException;
import javax.el.ValueExpression;

import org.hibernate.validator.ClassValidator;
import org.hibernate.validator.InvalidValue;
import org.jboss.seam.Component;
import org.jboss.seam.Instance;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.Install;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.annotations.intercept.BypassInterceptors;
import org.jboss.seam.contexts.Contexts;
import org.jboss.seam.el.EL;

/**
 * Caches instances of Hibernate Validator ClassValidator
 * 
 * @author Gavin King
 *
 */
@Name("org.jboss.seam.core.validators")
@BypassInterceptors
@Scope(ScopeType.APPLICATION)
@Install(precedence=BUILT_IN)
public class Validators
{
   
   //TODO: should use weak references here...
   private Map<Key, ClassValidator> classValidators = new ConcurrentHashMap<Key, ClassValidator>();
   
   class Key
   {
      private Class validatableClass;
      private java.util.Locale locale;
      
      public Key(Class validatableClass, java.util.Locale locale)
      {
         this.validatableClass = validatableClass;
         this.locale = locale;
      }
      
      @Override
      public boolean equals(Object other)
      {
         Key key = (Key) other;
         return key.validatableClass.equals(validatableClass)
               && key.locale.equals(locale);
      }
      
      @Override
      public int hashCode()
      {
         return validatableClass.hashCode() + locale.hashCode();
      }
   }

   /**
    * Get the cached ClassValidator instance. If the
    * argument is an instance of a session bean Seam 
    * component instance, the returned validator will 
    * be aware of constraints defined on the bean class.
    * Therefore this method is preferred to
    * getValidator(Class) if the argument might be a
    * session bean.
    * 
    * @param model the object to be validated
    */
   public <T> ClassValidator<T> getValidator(T model)
   {
      Class modelClass = model instanceof Instance ?
            ( (Instance) model ).getComponent().getBeanClass() :
            model.getClass();
      return getValidator( (Class<T>) modelClass );
   }
   
   /**
    * Get the cached ClassValidator instance.
    * 
    * @param modelClass the class to be validated
    */
   @SuppressWarnings("unchecked")
   public <T> ClassValidator<T> getValidator(Class<T> modelClass)
   {
      java.util.ResourceBundle bundle = SeamResourceBundle.getBundle();
      Locale none = bundle==null ? new Locale("NONE") : bundle.getLocale();
      Key key = new Key(modelClass, none);
      ClassValidator result = classValidators.get(key);
      if (result==null)
      {
         result = createValidator(modelClass);
         classValidators.put(key, result);
      }
      return result;
   }
   
   /**
    * Create a new ClassValidator for the given class,
    * using the current Seam ResourceBundle.
    * 
    * @param modelClass the class to be validated
    */
   @SuppressWarnings("unchecked")
   protected <T> ClassValidator<T> createValidator(Class<T> modelClass)
   {
      java.util.ResourceBundle bundle = SeamResourceBundle.getBundle();
      
      return bundle==null ? 
              new ClassValidator(modelClass) : 
              new ClassValidator(modelClass, bundle);
   }
   
   /**
    * Validate that the given value can be assigned to the property given by the value
    * expression.
    * 
    * @param valueExpression a value expression, referring to a property
    * @param elContext the ELContext in which to evaluate the expression
    * @param value a value to be assigned to the property
    * @return a set of potential InvalidValues, from Hibernate Validator
    */
   public InvalidValue[] validate(ValueExpression valueExpression, ELContext elContext, Object value)
   {
      ValidatingResolver validatingResolver = new ValidatingResolver( elContext.getELResolver() );
      ELContext decoratedContext = EL.createELContext(elContext, validatingResolver);
      valueExpression.setValue(decoratedContext, value);
      return validatingResolver.getInvalidValues();
   }
   
   class ValidatingResolver extends ELResolver
   {
      private ELResolver delegate;
      private InvalidValue[] invalidValues;

      public ValidatingResolver(ELResolver delegate)
      {
         this.delegate = delegate;
      }
      
      public InvalidValue[] getInvalidValues()
      {
         return invalidValues;
      }

      @Override
      public Class<?> getCommonPropertyType(ELContext context, Object value)
      {
         return delegate.getCommonPropertyType(context, value);
      }

      @Override
      public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context, Object value)
      {
         return delegate.getFeatureDescriptors(context, value);
      }

      @Override
      public Class<?> getType(ELContext context, Object x, Object y) 
            throws NullPointerException, PropertyNotFoundException, ELException
      {
         return delegate.getType(context, x, y);
      }

      @Override
      public Object getValue(ELContext context, Object base, Object property) 
            throws NullPointerException, PropertyNotFoundException, ELException
      {
         return delegate.getValue(context, base, property);
      }

      @Override
      public boolean isReadOnly(ELContext context, Object base, Object property) 
            throws NullPointerException, PropertyNotFoundException, ELException
      {
         return delegate.isReadOnly(context, base, property);
      }

      @Override
      public void setValue(ELContext context, Object base, Object property, Object value) 
            throws NullPointerException, PropertyNotFoundException, PropertyNotWritableException, ELException
      {
         if (base!=null && property!=null )
         {
            context.setPropertyResolved(true);
            invalidValues = getValidator(base).getPotentialInvalidValues( property.toString(), value );
         }
         
      }
      
   }
   
   public static Validators instance()
   {
      if ( !Contexts.isApplicationContextActive() )
      {
         throw new IllegalStateException("No active application scope");
      }
      return (Validators) Component.getInstance(Validators.class, ScopeType.APPLICATION);
   }

}
