package org.jboss.seam.wicket;

import static org.jboss.seam.ScopeType.STATELESS;
import static org.jboss.seam.ScopeType.UNSPECIFIED;

import java.lang.annotation.Annotation;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.wicket.Page;
import org.apache.wicket.util.string.Strings;
import org.jboss.seam.Component;
import org.jboss.seam.Namespace;
import org.jboss.seam.RequiredException;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.Begin;
import org.jboss.seam.annotations.Conversational;
import org.jboss.seam.annotations.End;
import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.Logger;
import org.jboss.seam.annotations.Out;
import org.jboss.seam.annotations.RaiseEvent;
import org.jboss.seam.annotations.bpm.BeginTask;
import org.jboss.seam.annotations.bpm.EndTask;
import org.jboss.seam.annotations.bpm.StartTask;
import org.jboss.seam.annotations.security.Restrict;
import org.jboss.seam.annotations.security.RoleCheck;
import org.jboss.seam.contexts.Contexts;
import org.jboss.seam.core.Expressions;
import org.jboss.seam.core.Init;
import org.jboss.seam.log.Log;
import org.jboss.seam.log.LogProvider;
import org.jboss.seam.log.Logging;
import org.jboss.seam.security.Identity;
import org.jboss.seam.wicket.annotations.NoConversationPage;
import org.jboss.seam.wicket.ioc.BijectedAttribute;
import org.jboss.seam.wicket.ioc.BijectedField;
import org.jboss.seam.wicket.ioc.BijectedMethod;
import org.jboss.seam.wicket.ioc.BijectionInterceptor;
import org.jboss.seam.wicket.ioc.ConversationInterceptor;
import org.jboss.seam.wicket.ioc.EventInterceptor;
import org.jboss.seam.wicket.ioc.InjectedAttribute;
import org.jboss.seam.wicket.ioc.InjectedField;
import org.jboss.seam.wicket.ioc.StatelessInterceptor;

public class WicketComponent<T>
{
   
   private final class InjectedLogger extends InjectedField<Logger>
   {
      private Log logInstance;
      
      InjectedLogger(Field field, Logger annotation)
      {
         super(field, annotation);
         String category = getAnnotation().value();
         if ("".equals(category))
         {
            logInstance = Logging.getLog(getType());
         }
         else
         {
            logInstance = Logging.getLog(category);
         }
      }

      Log getLogInstance()
      {
         return logInstance;
      }
      
      public void set(Object bean)
      {
         super.set(bean, logInstance);
      }
   }

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

   private Class<? extends T> type;
   
   private Class<?> enclosingType;
   private String enclosingInstanceVariableName;
   
   private List<BijectedAttribute<In>> inAttributes = new ArrayList<BijectedAttribute<In>>();
   private List<BijectedAttribute<Out>> outAttributes = new ArrayList<BijectedAttribute<Out>>();
   private List<InjectedLogger> loggerFields = new ArrayList<InjectedLogger>();
   
   private Set<AccessibleObject> conversationManagementMembers = new HashSet<AccessibleObject>();
   
   private List<StatelessInterceptor<T>> interceptors = new ArrayList<StatelessInterceptor<T>>();
   
   private Set<String> restrictions;
   
   boolean anyMethodHasRaiseEvent = false;
   
   private Class<? extends Page> noConversationPage;
   
   public Class<?> getType()
   {
      return type;
   }
   
   public static <T> WicketComponent<T> getInstance(Class<? extends T> type)
   {
      String name = getContextVariableName(type);
      if (Contexts.getApplicationContext().isSet(name))
      {
         return (WicketComponent) Contexts.getApplicationContext().get(name);
      }
      else
      {
         return null;
      }
   }
   

   private void initInterceptors()
   {
      // TODO Add a check to see whether we really need this
      interceptors.add(new BijectionInterceptor());
      if (!conversationManagementMembers.isEmpty())
      {
         interceptors.add(new ConversationInterceptor());
      }
      if (anyMethodHasRaiseEvent)
      {
         interceptors.add(new EventInterceptor());
      }
   }
   
   public List<StatelessInterceptor<T>> getInterceptors()
   {
      return interceptors;
   }
   
   public static String getContextVariableName(Class<?> type)
   {
      return type.getName() + ".wicketComponent";
   }
   
   public String getName()
   {
      return getContextVariableName(type);
   }

   public WicketComponent(Class<? extends T> type)
   {
      this.type = type;
      this.enclosingType = type.getEnclosingClass();
      if (this.enclosingType != null)
      {
         log.debug("Class: " + type + ", enclosed by " + enclosingType);
      }
      else
      {
         log.debug("Class: " + type);
      }
      
      scan();
      
      initInterceptors();
      
      Contexts.getApplicationContext().set(getName(), this);
   }
   
   private void scan()
   {
      Class clazz = type;
      scanClassEnclosureHierachy();
      while (clazz != Object.class) {
         for (Method method : clazz.getDeclaredMethods())
         {
            add(method);
         }
         for (Field field : clazz.getDeclaredFields())
         {
            add(field);
         }
         for(Constructor<T> constructor : clazz.getDeclaredConstructors())
         {
            add(constructor);
         } 
         clazz = clazz.getSuperclass();
      }
   }
   
   private void scanClassEnclosureHierachy()
   {     
      Class cls = type;
      int i = 0;
      while (cls != null)
      {
         for (Annotation annotation : cls.getAnnotations())
         {
            if (annotation instanceof Restrict)
            {
               Restrict restrict = (Restrict) annotation;
               if (restrictions == null) restrictions = new HashSet<String>();
               
               if ( Strings.isEmpty(restrict.value()) )
               {
                  throw new IllegalStateException("@Restrict on " + cls.getName() + " must specify an expression");
               }
               
               restrictions.add(restrict.value());
            }
            
            if (annotation.annotationType().isAnnotationPresent(RoleCheck.class))
            {
               if (restrictions == null) restrictions = new HashSet<String>();
               restrictions.add("#{identity.hasRole('" + 
                     annotation.annotationType().getSimpleName().toLowerCase() + "')}");
            }
            
            if (annotation instanceof NoConversationPage)
            {
               NoConversationPage noConversationPage = (NoConversationPage) annotation;
               this.noConversationPage = noConversationPage.value();
            }
         }
         
         cls = cls.getEnclosingClass();
         i++;
      }
      
      if (i > 1)
      {
         this.enclosingInstanceVariableName = "this$" + (i - 2);
      }
   }
   
   public void checkRestrictions()
   {
      if (Identity.isSecurityEnabled() && restrictions != null)      
      {
         for (String restriction : restrictions)
         {
            Identity.instance().checkRestriction(restriction);
         }
      }
   }

   public void outject(T target)
   {
      for (BijectedAttribute<Out> out : outAttributes)
      {
         Object value = out.get(target);
         if (value==null && out.getAnnotation().required())
         {
            throw new RequiredException("@Out attribute requires non-null value: " + out.toString());
         }
         else
         {
            Component component = null;
            if (out.getAnnotation().scope()==UNSPECIFIED)
            {
               component = Component.forName(out.getContextVariableName());
               if (value!=null && component!=null)
               {
                  if (!component.isInstance(value))
                  {
                     throw new IllegalArgumentException("attempted to bind an @Out attribute of the wrong type to: " + out.toString());
                  }
               }
            }
            else if (out.getAnnotation().scope()==STATELESS)
            {
               throw new IllegalArgumentException("cannot specify explicit scope=STATELESS on @Out: " + out.toString());
            }
         
            ScopeType outScope = component == null ? out.getAnnotation().scope() : component.getScope();
            
            if (outScope ==  null)
            {
               throw new IllegalArgumentException("cannot determine scope to outject to on @Out: " + out.toString());
            }
         
            if (outScope.isContextActive())
            {
               if (value==null)
               {
                  outScope.getContext().remove(out.getContextVariableName());
               }
               else
               {
                  outScope.getContext().set(out.getContextVariableName(), value);
               }
            }
         }
      }
   }
   

   public void disinject(T target)
   {
      for ( InjectedAttribute<In> in : inAttributes )
      {
         if ( !in.getType().isPrimitive() )
         {
            in.set(target, null);
         }
      }
         
   }
   
   public void inject(T instance)
   {
      for ( BijectedAttribute<In> in : inAttributes )
      {
         in.set( instance,  getValue(in, instance) );
      }
   }
   
   private void add(Constructor<T> constructor)
   {
      if ( constructor.isAnnotationPresent(Begin.class) || 
            constructor.isAnnotationPresent(org.jboss.seam.wicket.annotations.Begin.class) || 
            constructor.isAnnotationPresent(End.class) || 
            constructor.isAnnotationPresent(StartTask.class) ||
            constructor.isAnnotationPresent(BeginTask.class) ||
            constructor.isAnnotationPresent(EndTask.class) ) 
       {
          conversationManagementMembers.add(constructor);
       }
   }
   
   private void add(Method method)
   {
      if ( method.isAnnotationPresent(In.class) )
      {
         final In in = method.getAnnotation(In.class);
         inAttributes.add( new BijectedMethod(method, in)
         {

            @Override
            protected String getSpecifiedContextVariableName()
            {
               return in.value();
            }
            
         });
      }
      if ( method.isAnnotationPresent(Out.class) )
      {
         final Out out = method.getAnnotation(Out.class);
         outAttributes.add( new BijectedMethod(method, out)
         {

            @Override
            protected String getSpecifiedContextVariableName()
            {
               return out.value();
            }
            
         });
      }
      
      if ( method.isAnnotationPresent(Begin.class) || 
            method.isAnnotationPresent(org.jboss.seam.wicket.annotations.Begin.class) || 
            method.isAnnotationPresent(End.class) || 
            method.isAnnotationPresent(StartTask.class) ||
            method.isAnnotationPresent(BeginTask.class) ||
            method.isAnnotationPresent(EndTask.class) ||
            method.isAnnotationPresent(Conversational.class)) 
       {
          conversationManagementMembers.add(method);
       }
      
      if (method.isAnnotationPresent(RaiseEvent.class))
      {
         anyMethodHasRaiseEvent = true;
      }
   }
   
   private void add(Field field)
   {
      if ( field.isAnnotationPresent(In.class) )
      {
         final In in = field.getAnnotation(In.class);
         inAttributes.add( new BijectedField(field, in)
         {

            @Override
            protected String getSpecifiedContextVariableName()
            {
               return in.value();
            }
            
         });
      }
      if ( field.isAnnotationPresent(Out.class) )
      {
         final Out out = field.getAnnotation(Out.class);
         outAttributes.add( new BijectedField(field, out)
         {

            @Override
            protected String getSpecifiedContextVariableName()
            {
               return out.value();
            }
            
         });
      }
      if ( field.isAnnotationPresent(Logger.class) )
      {
         final Logger logger = field.getAnnotation(Logger.class);
         InjectedLogger loggerField = new InjectedLogger(field, logger);   
         
         if ( Modifier.isStatic( field.getModifiers() ) )
         {
            loggerField.set(null);
         }
         else
         {
            loggerFields.add(loggerField);
         }
      }
   }
   
   private Object getValue(BijectedAttribute<In> in, Object bean)
   {
      Object result;
      String name = in.getContextVariableName();
      if ( name.startsWith("#") )
      {
         if ( log.isDebugEnabled() )
         {
            log.trace("trying to inject with EL expression: " + name);
         }
         result = Expressions.instance().createValueExpression(name).getValue();
      }
      else if ( in.getAnnotation().scope()==UNSPECIFIED )
      {
         if ( log.isDebugEnabled() )
         {
            log.trace("trying to inject with hierarchical context search: " + name);
         }
         boolean create = in.getAnnotation().create() && !org.jboss.seam.contexts.Lifecycle.isDestroying();
         result = getInstanceInAllNamespaces(name, create);
      }
      else
      {
         if ( in.getAnnotation().create() )
         {
            throw new IllegalArgumentException(
                  "cannot combine create=true with explicit scope on @In: " +
                  in.toString()
               );
         }
         if ( in.getAnnotation().scope()==STATELESS )
         {
            throw new IllegalArgumentException(
                  "cannot specify explicit scope=STATELESS on @In: " +
                  in.toString()
               );
         }
         
         log.trace("trying to inject from specified context: " + name);
         
         if ( in.getAnnotation().scope().isContextActive() )
         {
            result = in.getAnnotation().scope().getContext().get(name);
         }
         else 
         {
            result = null;
         }
      }
      if ( result==null && in.getAnnotation().required() )
      {
         throw new RequiredException( "@In attribute requires non-null value: " + type + '.' + name );
      }
      else
      {
         return result;
      }
   }
   
   private static Object getInstanceInAllNamespaces(String name, boolean create)
   {
      Object result;
      result = Component.getInstance(name, create);
      if (result==null)
      {
         for ( Namespace namespace: Init.instance().getGlobalImports() )
         {
            result = namespace.getComponentInstance(name, create);
            if (result!=null) break; 
         }
      }
      return result;
   }
   
   public void initialize(T bean)
   {
      injectLog(bean);
   }
   
   private void injectLog(T bean)
   {
      for (InjectedLogger injectedLogger : loggerFields)
      {
         injectedLogger.set(bean);
      }
   }
   
   public Class<?> getEnclosingType()
   {
      return enclosingType;
   }
   
   public String getEnclosingInstanceVariableName()
   {
      return enclosingInstanceVariableName;
   }
   
   @Override
   public String toString()
   {
      return "WicketComponent(" + type + ")";
   }
   
   public boolean isConversationManagementMethod(AccessibleObject member)
   {
      return member!=null && 
            conversationManagementMembers.contains(member);
   }
   
   public Class<? extends Page> getNoConversationPage()
   {
      return noConversationPage;
   }
   
}
