/*
 * JBoss, the OpenSource J2EE webOS
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jboss.ejb3.injection;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.persistence.EntityManagerFactory;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContexts;
import org.jboss.aop.Advisor;
import org.jboss.ejb3.Container;
import org.jboss.ejb3.Ejb3Deployment;
import org.jboss.ejb3.entity.InjectedEntityManager;
import org.jboss.ejb3.entity.InjectedEntityManagerFactory;
import org.jboss.ejb3.entity.ManagedEntityManagerFactory;
import org.jboss.logging.Logger;
import org.jboss.naming.Util;

/**
 * Searches bean class for all @Inject and create Injectors
 *
 * @author <a href="mailto:bill@jboss.org">Bill Burke</a>
 * @version $Revision: 1.1.2.4 $
 */
public class PersistenceContextHandler
{
   private static final Logger log = Logger.getLogger(PersistenceContextHandler.class);

   private static void loadResourcesAnnotation(Container container) throws Exception
   {
      PersistenceContexts resources = (PersistenceContexts) ((Advisor) container).resolveAnnotation(PersistenceContexts.class);
      if (resources == null) return;
      for (PersistenceContext ref : resources.value())
      {
         String encName = ref.name();
         if (encName == null || encName.equals(""))
         {
            throw new RuntimeException("JBoss requires name() for class level @PersistenceContext");
         }

         ManagedEntityManagerFactory factory = getManagedEntityManagerFactory(ref, container);
         bindEntityManagerToEnc(container, factory, encName, container.getBeanClass());
      }
      PersistenceContext pc = (PersistenceContext) ((Advisor) container).resolveAnnotation(PersistenceContext.class);
      if (pc != null)
      {
         String encName = pc.name();
         if (encName == null || encName.equals(""))
         {
            throw new RuntimeException("JBoss requires name() for class level @PersistenceContext");
         }

         ManagedEntityManagerFactory factory = getManagedEntityManagerFactory(pc, container);
         bindEntityManagerToEnc(container, factory, encName, container.getBeanClass());
      }

   }

   public static ManagedEntityManagerFactory getManagedEntityManagerFactory(PersistenceContext ref, Container container)
           throws NamingException
   {
      ManagedEntityManagerFactory factory;
      if (ref.unitName() == null || ref.unitName().equals(""))
      {
         factory = container.getManagedEntityManagerFactory();
      }
      else
      {
         InitialContext ctx = container.getInitialContext();
         try
         {
            factory = (ManagedEntityManagerFactory) ctx.lookup(Ejb3Deployment.MANAGED_ENTITY_FACTORY_CONTEXT + "/" + ref.unitName());
         }
         catch (NamingException e)
         {
            throw new RuntimeException("unable to find: " + Ejb3Deployment.MANAGED_ENTITY_FACTORY_CONTEXT + "/" + ref.unitName(), e);
         }
      }
      return factory;
   }


   public static EntityManagerFactory getEntityManagerFactory(PersistenceContext ref, Container container)
           throws NamingException
   {
      EntityManagerFactory factory;
      if (ref.name() == null || ref.name().equals(""))
      {
         factory = container.getManagedEntityManagerFactory().getEntityManagerFactory();
         return new InjectedEntityManagerFactory(factory, container.getInitialContextProperties());
      }
      else
      {
         InitialContext ctx = container.getInitialContext();
         factory = (EntityManagerFactory) ctx.lookup(Ejb3Deployment.ACTUAL_ENTITY_MANAGER_FACTORY_CONTEXT + "/" + ref.name());
         return new InjectedEntityManagerFactory(factory,Ejb3Deployment.ACTUAL_ENTITY_MANAGER_FACTORY_CONTEXT + "/" + ref.name(), container.getInitialContextProperties());
      }
   }


   public static List loadInjectors(Container container) throws Exception
   {
      Class clazz = container.getBeanClass();
      Method[] methods = clazz.getMethods();
      ArrayList list = new ArrayList();
      Context resourceCtx = Util.createSubcontext(container.getInitialContext(), Container.ENC_CTX_NAME + "/env");
      for (int i = 0; i < methods.length; i++)
      {
         PersistenceContext ref = (PersistenceContext) ((Advisor) container).resolveAnnotation(methods[i], PersistenceContext.class);
         if (ref != null)
         {
            if (!methods[i].getName().startsWith("set")) throw new RuntimeException("@PersistenceContext can only be used with a set method: " + methods[i]);
            if (methods[i].getParameterTypes().length != 1) throw new RuntimeException("@PersistenceContext can only be used with a set method of one parameter: " + methods[i]);
            String encName = ref.name();
            if (encName == null || encName.equals(""))
            {
               encName = methods[i].getName().substring(3);
            }
            ManagedEntityManagerFactory factory = getManagedEntityManagerFactory(ref, container);
            list.add(new EntityManagerMethodInjector(methods[i], container, factory, ref.type()));
            bindEntityManagerToEnc(container, factory, encName, methods[i]);
         }
      }
      loadResourcesAnnotation(container);
      loadFieldInjectors(clazz, container, list, resourceCtx);
      return list;
   }

   private static void bindEntityManagerToEnc(Container container, ManagedEntityManagerFactory factory, String encName, Object where)
           throws NamingException
   {
      Context entityCtx = Util.createSubcontext(container.getInitialContext(), Container.ENC_CTX_NAME + "/env");
      InjectedEntityManager entityManager = new InjectedEntityManager(factory, container.getInitialContextProperties());
      try
      {
         entityCtx.bind(encName, entityManager);
      }
      catch (javax.naming.NameAlreadyBoundException e)
      {
         throw new RuntimeException("Error trying to bind EntityManager to name " + encName + " declared on " + where, e);
      }
   }

   private static void loadFieldInjectors(Class clazz, Container container, ArrayList list, Context resourceCtx) throws Exception
   {
      if (clazz == null || clazz.equals(Object.class)) return;
      loadFieldInjectors(clazz.getSuperclass(), container, list, resourceCtx);
      Field[] fields = clazz.getDeclaredFields();
      for (int i = 0; i < fields.length; i++)
      {
         PersistenceContext ref = (PersistenceContext) ((Advisor) container).resolveAnnotation(fields[i], PersistenceContext.class);
         if (ref != null)
         {
            fields[i].setAccessible(true);
            String encName = ref.name();
            if (encName == null || encName.equals(""))
            {
               encName = fields[i].getName();
            }
            ManagedEntityManagerFactory factory = getManagedEntityManagerFactory(ref, container);
            list.add(new EntityManagerFieldInjector(fields[i], container, factory, ref.type()));
            bindEntityManagerToEnc(container, factory, encName, fields[i]);
         }
      }
   }
}
