/*
  * JBoss, Home of Professional Open Source
  * Copyright 2005, JBoss Inc., and individual contributors as indicated
  * by the @authors tag. See the copyright.txt in the distribution for a
  * full listing of individual contributors.
  *
  * This is free software; you can redistribute it and/or modify it
  * under the terms of the GNU Lesser General Public License as
  * published by the Free Software Foundation; either version 2.1 of
  * the License, or (at your option) any later version.
  *
  * This software is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  * Lesser General Public License for more details.
  *
  * You should have received a copy of the GNU Lesser General Public
  * License along with this software; if not, write to the Free
  * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
  * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
  */
package org.jboss.ejb3.injection;

import org.jboss.ejb3.Container;
import org.jboss.ejb3.EJBContainer;
import org.jboss.ejb3.dd.EjbLocalRef;
import org.jboss.ejb3.dd.EjbRef;
import org.jboss.ejb3.dd.Injectable;
import org.jboss.ejb3.interceptor.InterceptorInjector;
import org.jboss.logging.Logger;
import org.jboss.annotation.IgnoreDependency;

import javax.annotation.EJB;
import javax.annotation.EJBs;
import javax.naming.Context;
import javax.naming.NameNotFoundException;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;

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

   public static void loadDependencies(Injectable xml, EJBContainer container, Class clazz, boolean isContainer)
   {
      if (xml != null)
      {
         loadEjbLocalRefXmlDependencies(xml.getEjbLocalRefs(), container);
         loadEjbRefXmlDependencies(xml.getEjbRefs(), container);
      }
      loadClassDependencies(container, clazz, isContainer);
      HashSet<String> visitedMethods = new HashSet<String>();
      loadMethodDependencies(visitedMethods, clazz, container, isContainer);
      loadFieldDependencies(clazz, container, isContainer);

   }

   private static void loadEjbLocalRefXmlDependencies(Collection<EjbLocalRef> refs, EJBContainer container)
   {
      for (EjbLocalRef ref : refs)
      {
         if (ref.isIgnoreDependency())
         {
            container.getEjbRefDependencies().put("env/" + ref.getEjbRefName(), "env/" + ref.getEjbRefName());
            continue;
         }
         
         EJBContainer refcon = null;
         String link = ref.getEjbLink();
         
         try
         {
            if (ref.getLocal() != null)
            {
               Class refClass = container.getClassloader().loadClass(ref.getLocal());
            
               if (link != null && !"".equals(link))
               {
                  refcon = container.getDeployment().getEjbContainer(link, refClass);
                  if (refcon == null)
                     log.warn("unable to find <ejb-local-ref> of interface " + ref.getLocal() + " and ejbLink of " + link + " in ejb-jar.xml of " + container.getEjbName());
               }
               else
               {
                  try
                  {
                     refcon = container.getDeployment().getEjbContainer(refClass);
                     if (refcon == null)
                        log.warn("unable to find <ejb-local-ref> of interface " + ref.getLocal() + " and ejbLink of " + link + " in ejb-jar.xml of " + container.getEjbName());
                  }
                  catch (NameNotFoundException e)
                  {
                     throw new RuntimeException("could not find <ejb-local-ref> by local interface " + ref.getLocal() + " only in ejb-jar.xml of " + container.getEjbName() + " " + e.getMessage());
                  }
               }
            }
         }
         catch (ClassNotFoundException e)
         {
            throw new RuntimeException("could not find ejb-local-ref's local interface " + ref.getLocal() + " in ejb-jar.xml of " + container.getEjbName());
         }
         
         if (ref.getMappedName() == null || ref.getMappedName().equals(""))
         {
            if (container.getEjbRefDependencies().containsKey("env/" + ref.getEjbRefName())) return;
        
            if (refcon != null)
            {
               addDependency("env/" + ref.getEjbRefName(), refcon, container);
            }
         }
         else
         {
            // put it in list just in case there is an @EJB annotation without a mappedName
            container.getEjbRefDependencies().put("env/" + ref.getEjbRefName(), "env/" + ref.getEjbRefName());
            
            if (refcon != null)
            {
               addDependency(ref.getEjbRefName(), refcon, container);
            }
         }
      }
   }

   private static void loadEjbRefXmlDependencies(Collection<EjbRef> refs, EJBContainer container)
   {
      for (EjbRef ref : refs)
      {
         if (ref.isIgnoreDependency())
         {
            container.getEjbRefDependencies().put("env/" + ref.getEjbRefName(), "env/" + ref.getEjbRefName());
            continue;
         }
         
         EJBContainer refcon = null;
         String link = ref.getEjbLink();
         
         try
         {
            if (ref.getRemote() != null)
            {
               Class refClass = container.getClassloader().loadClass(ref.getRemote());
               
               if (link != null && !"".equals(link))
               {
                  refcon = container.getDeployment().getEjbContainer(link, refClass);
                  
                  if (refcon == null)
                     log.warn("unable to find <ejb-ref> of interface " + ref.getRemote() + " and ejbLink of " + link + " in ejb-jar.xml of " + container.getEjbName());
               }
               else
               {
                  try
                  {
                     refcon = container.getDeployment().getEjbContainer(refClass);
                     if (refcon == null)
                        log.warn("unable to find <ejb-ref> of interface " + ref.getRemote() + " and ejbLink of " + link + " in ejb-jar.xml of " + container.getEjbName());
                  }
                  catch (NameNotFoundException e)
                  {
                     log.warn("could not find <ejb-ref> by remote` interface " + ref.getRemote() + " only in ejb-jar.xml of " + container.getEjbName() + ":" + e.getMessage());
                  }
               }
            }
         }
         catch (ClassNotFoundException e)
         {
            throw new RuntimeException("could not find ejb-ref's remote interface " + ref.getRemote() + " in ejb-jar.xml of " + container.getEjbName());
         }
      
         if (ref.getMappedName() == null || ref.getMappedName().equals(""))
         {
            if (container.getEjbRefDependencies().containsKey("env/" + ref.getEjbRefName())) return;
            
            if (refcon != null)
            {
               addDependency("env/" + ref.getEjbRefName(), refcon, container);
            }
         }
         else
         {
            // put it in list just in case there is an @EJB annotation without a mappedName
            container.getEjbRefDependencies().put("env/" + ref.getEjbRefName(), "env/" + ref.getEjbRefName());
            
            if (refcon != null)
            {
               addDependency(ref.getEjbRefName(), refcon, container);
            }
         }
      }
   }

   private static void loadClassDependencies(EJBContainer container, Class clazz, boolean isContainer)
   {
      EJBs resources = (EJBs) InjectionUtil.getAnnotation(
              EJBs.class, container, clazz, isContainer);
      if (resources != null)
      {
         for (EJB ref : resources.value())
         {
            loadRefDependency(ref, container, null);
         }
      }
      EJB ejb = (EJB) InjectionUtil.getAnnotation(
              EJB.class, container, clazz, isContainer);
      if (ejb != null) loadRefDependency(ejb, container, null);
   }

   private static void loadRefDependency(EJB ref, EJBContainer container, AccessibleObject member)
   {
      String encName = ref.name();
      Class memberType = null;
      if (member != null)
      {
         if (member instanceof Method)
         {
            encName = InjectionUtil.getEncName((Method)member);
            memberType = ((Method)member).getParameterTypes()[0];
         }
         else
         {
            encName = InjectionUtil.getEncName((Field)member);
            memberType = ((Field)member).getType();
         }
      }
      else if (encName == null || encName.equals(""))
      {
         throw new RuntimeException("JBoss requires name() for class level @EJB");
      }
      // skip if already loaded by XML
      if (container.getEjbRefDependencies().containsKey("env/" + encName)) return;
      EJBContainer refContainer = getEjbContainer(ref, container, memberType);
      if (refContainer != null) addDependency(encName, refContainer, container);
   }

   public static void addDependency(String refName, EJBContainer refcon, EJBContainer container)
   {
      container.getEjbRefDependencies().put(refName, refName);
      container.getDependencyPolicy().addDependency(refcon.getObjectName().getCanonicalName());
   }

   private static void loadMethodDependencies(HashSet<String> visitedMethods, Class clazz, EJBContainer container, boolean isContainer)
   {
      if (clazz == null || clazz.equals(Object.class))
      {
         return;
      }
      Method[] methods = clazz.getDeclaredMethods();
      for (int i = 0; i < methods.length; i++)
      {
         EJB ref = (EJB) InjectionUtil.getAnnotation(EJB.class, container, methods[i], isContainer);
         if (ref != null)
         {
            if (container.resolveAnnotation(methods[i], IgnoreDependency.class) != null) continue;
            if (ref.mappedName() == null || ref.mappedName().equals("")) continue;
            if (!Modifier.isPrivate(methods[i].getModifiers()))
            {
               if (visitedMethods.contains(methods[i].getName())) continue;
               else visitedMethods.add(methods[i].getName());
            }
            if (!methods[i].getName().startsWith("set"))
               throw new RuntimeException("@EJB can only be used with a set method: " + methods[i]);
            String encName = ref.name();
            if (encName == null || encName.equals(""))
            {
               encName = InjectionUtil.getEncName(methods[i]);
            }
            else
            {
               encName = "env/" + encName;
            }
            if (container.getEjbRefDependencies().containsKey(encName)) continue;
            loadRefDependency(ref, container, methods[i]);
         }
      }
      loadMethodDependencies(visitedMethods, clazz.getSuperclass(), container, isContainer);
   }

   private static void loadFieldDependencies(Class clazz, EJBContainer container, boolean isContainer)
   {
      if (clazz == null || clazz.equals(Object.class)) return;
      loadFieldDependencies(clazz.getSuperclass(), container, isContainer);
      Field[] fields = clazz.getDeclaredFields();
      for (Field field : fields)
      {
         EJB ref = (EJB) InjectionUtil.getAnnotation(EJB.class, container, field, isContainer);
         if (ref != null)
         {
            if (container.resolveAnnotation(field, IgnoreDependency.class) != null) continue;
            if (ref.mappedName() == null || ref.mappedName().equals(""))
            {
               loadRefDependency(ref, container, field);
               continue;
            }
            String encName = ref.name();
            if (encName == null || encName.equals(""))
            {
               encName = InjectionUtil.getEncName(field);
            }
            else
            {
               encName = "env/" + encName;
            }

            if (container.getEjbRefDependencies().containsKey(encName)) continue;
            loadRefDependency(ref, container, field);
         }
      }
   }

   public static void loadInjectors(Container container) throws Exception
   {
      EJBContainer ejb = (EJBContainer) container;
      Class clazz = container.getBeanClass();
      loadInjectors(clazz, ejb.getXml(), ejb, ejb.getEncInjections(), true);
   }

   public static void loadInjectors(InterceptorInjector injector) throws Exception
   {
      loadInjectors(injector.getClazz(), injector.getXml(), (EJBContainer) injector.getContainer(), injector.getEncInjections(), false);
   }

   private static void loadInjectors(Class clazz, Injectable xml, EJBContainer container, HashMap<AccessibleObject, Injector> encInjections, boolean isContainer) throws Exception
   {
      if (xml != null)
      {
         loadEjbLocalXml(xml.getEjbLocalRefs(), container, clazz, encInjections);
         loadEjbRefXml(xml.getEjbRefs(), container, clazz, encInjections);
      }

      Context ctx = container.getEnc();
      loadClassLevelEnc(clazz, container, ctx, isContainer);

      HashSet<String> visitedMethods = new HashSet<String>();
      loadMethodInjectors(visitedMethods, clazz, container, encInjections, isContainer);
      loadFieldInjectors(clazz, container, encInjections, isContainer);
   }


   private static void loadEjbLocalXml(Collection<EjbLocalRef> refs, EJBContainer container, Class clazz, HashMap<AccessibleObject, Injector> injectors)
   {

      for (EjbLocalRef ref : refs)
      {
         if (container.hasEncEntry("env/" + ref.getEjbRefName())) continue;

         String jndiName = ref.getMappedName();

         if (jndiName == null || jndiName.equals(""))
         {
            if (ref.getLocal() == null) continue;  // we have just a jboss.xml listing only, no ejb-jar.xml
            String link = ref.getEjbLink();
            Class refClass = null;
            try
            {
               refClass = container.getClassloader().loadClass(ref.getLocal());
            }
            catch (ClassNotFoundException e)
            {
               throw new RuntimeException("could not find ejb-ref's remote interface " + ref.getLocal() + " in ejb-jar.xml of " + container.getEjbName());
            }
            if (link != null && !"".equals(link))
            {
               jndiName = container.getDeployment().getEjbJndiName(link, refClass);
            }
            else
            {
               try
               {
                  jndiName = container.getDeployment().getEjbJndiName(refClass);
               }
               catch (NameNotFoundException e)
               {
                  throw new RuntimeException("could not find <ejb-ref> by remote interface " + ref.getLocal() + " only " + e.getMessage());
               }
            }
         }

         container.addEncLinkRefEntry("env/" + ref.getEjbRefName(), jndiName);

         if (ref.getInjectionTarget() != null)
         {
            // todo, get injection target class
            AccessibleObject ao = InjectionUtil.findInjectionTarget(clazz, ref.getInjectionTarget());
            if (ao instanceof Field)
            {
               injectors.put(ao, new JndiFieldInjector((Field) ao, "env/" + ref.getEjbRefName(), container.getEnc()));
            }
            else
            {
               injectors.put(ao, new JndiMethodInjector((java.lang.reflect.Method) ao, "env/" + ref.getEjbRefName(), container.getEnc()));
            }
         }
      }
   }

   private static void loadEjbRefXml(Collection<EjbRef> refs, EJBContainer container, Class clazz, HashMap<AccessibleObject, Injector> injectors)
   {
      for (EjbRef ref : refs)
      {
         if (container.hasEncEntry("env/" + ref.getEjbRefName())) continue;

         String jndiName = ref.getMappedName();

         if (jndiName == null || jndiName.equals(""))
         {
            if (ref.getRemote() == null) continue;  // we have just a jboss.xml listing only, no ejb-jar.xml
            String link = ref.getEjbLink();
            Class refClass = null;
            try
            {
               refClass = container.getClassloader().loadClass(ref.getRemote());
            }
            catch (ClassNotFoundException e)
            {
               throw new RuntimeException("could not find ejb-ref's remote interface " + ref.getRemote() + " in ejb-jar.xml of " + container.getEjbName());
            }
            if (link != null && !"".equals(link))
            {
               jndiName = container.getDeployment().getEjbJndiName(link, refClass);
            }
            else
            {
               try
               {
                  jndiName = container.getDeployment().getEjbJndiName(refClass);
               }
               catch (NameNotFoundException e)
               {
                  throw new RuntimeException("could not find <ejb-ref> by remote interface " + ref.getRemote() + " only " + e.getMessage());
               }
            }
         }

         container.addEncLinkRefEntry("env/" + ref.getEjbRefName(), jndiName);

         if (ref.getInjectionTarget() != null)
         {
            // todo, get injection target class
            AccessibleObject ao = InjectionUtil.findInjectionTarget(clazz, ref.getInjectionTarget());
            if (ao instanceof Field)
            {
               injectors.put(ao, new JndiFieldInjector((Field) ao, "env/" + ref.getEjbRefName(), container.getEnc()));
            }
            else
            {
               injectors.put(ao, new JndiMethodInjector((java.lang.reflect.Method) ao, "env/" + ref.getEjbRefName(), container.getEnc()));
            }
         }
      }
   }


   public static EJBContainer getEjbContainer(EJB ref, EJBContainer container, Class memberType)
   {
      EJBContainer rtn = null;

      if (ref.mappedName() != null && !"".equals(ref.mappedName()))
      {
         return null;
      }

      if (ref.beanName().equals("") && memberType == null)
         throw new RuntimeException("For EJB " + container.getEjbName() + "not enough information for @EJB.  Please fill out the beanName and/or businessInterface attributes");

      Class businessInterface = memberType;
      if (!ref.businessInterface().getName().equals(Object.class.getName()))
      {
         businessInterface = ref.businessInterface();
      }

      if (ref.beanName().equals(""))
      {
         try
         {
            rtn = container.getDeployment().getEjbContainer(businessInterface);
         }
         catch (NameNotFoundException e)
         {
            log.warn("For EJB " + container.getEjbName() + " could not find jndi binding based on interface only for @EJB(" + businessInterface.getName() + ") " + e.getMessage());
         }
      }
      else
      {
         rtn = container.getDeployment().getEjbContainer(ref.beanName(), businessInterface);
      }

      return rtn;
   }

   public static String getJndiName(EJB ref, EJBContainer container, Class memberType)
   {
      String jndiName;
      
      if (ref.mappedName() != null && !"".equals(ref.mappedName()))
      {
         return ref.mappedName();
      }

      if (ref.beanName().equals("") && memberType == null)
         throw new RuntimeException("For EJB " + container.getEjbName() + "not enough information for @EJB.  Please fill out the beanName and/or businessInterface attributes");

      Class businessInterface = memberType;
      if (!ref.businessInterface().getName().equals(Object.class.getName()))
      {
         businessInterface = ref.businessInterface();
      }

      if (ref.beanName().equals(""))
      {
         try
         {
            jndiName = container.getDeployment().getEjbJndiName(businessInterface);
         }
         catch (NameNotFoundException e)
         {
            throw new RuntimeException("For EJB " + container.getEjbName() + " could not find jndi binding based on interface only for @EJB(" + businessInterface.getName() + ") " + e.getMessage());
         }
         if (jndiName == null)
         {
            throw new RuntimeException("For EJB " + container.getEjbName() + " could not find jndi binding based on interface only for @EJB(" + businessInterface.getName() + ")");
         }
      }
      else
      {
         jndiName = container.getDeployment().getEjbJndiName(ref.beanName(), businessInterface);
         if (jndiName == null)
         {
            throw new RuntimeException("For EJB " + container.getEjbName() + "could not find jndi binding based on beanName and business interface for @EJB(" + ref.beanName() + ", " + businessInterface.getName() + ")");
         }
      }

      return jndiName;
   }

   private static void loadClassLevelEnc(Class clazz, EJBContainer container, Context ctx, boolean isContainer) throws Exception
   {
      EJBs ref = (EJBs) InjectionUtil.getAnnotation(EJBs.class, container, clazz, isContainer);
      if (ref != null)
      {
         EJB[] ejbs = ref.value();

         for (EJB ejb : ejbs)
         {
            loadClassAnnotation(ejb, clazz, container);
         }
      }
      EJB ejbref = (EJB) InjectionUtil.getAnnotation(EJB.class, container, clazz, isContainer);
      if (ejbref != null) loadClassAnnotation(ejbref, clazz, container);

   }

   private static void loadClassAnnotation(EJB ejb, Class clazz, EJBContainer container)
   {
      String encName = ejb.name();
      if (encName == null || encName.equals(""))
      {
         throw new RuntimeException("JBoss requires the name of the @EJB in the @EJBs: " + clazz);
      }
      encName = "env/" + encName;
      if (!container.hasEncEntry(encName))
      {
         String jndiName = getJndiName(ejb, container, null);
         container.addEncLinkRefEntry(encName, jndiName);
      }
   }

   private static void loadMethodInjectors(HashSet<String> visitedMethods, Class clazz, EJBContainer container, HashMap<AccessibleObject, Injector> injectors, boolean isContainer) throws Exception
   {
      if (clazz == null || clazz.equals(Object.class))
      {
         return;
      }
      Method[] methods = clazz.getDeclaredMethods();
      for (int i = 0; i < methods.length; i++)
      {
         EJB ref = (EJB) InjectionUtil.getAnnotation(EJB.class, container, methods[i], isContainer);
         if (ref != null)
         {
            if (!Modifier.isPrivate(methods[i].getModifiers()))
            {
               if (visitedMethods.contains(methods[i].getName())) continue;
               else visitedMethods.add(methods[i].getName());
            }
            if (!methods[i].getName().startsWith("set"))
               throw new RuntimeException("@EJB can only be used with a set method: " + methods[i]);
            String encName = ref.name();
            if (encName == null || encName.equals(""))
            {
               encName = InjectionUtil.getEncName(methods[i]);
            }
            else
            {
               encName = "env/" + encName;
            }
            EJBContainer ejb = (EJBContainer) container;
            if (!ejb.hasEncEntry(encName))
            {
               String jndiName = getJndiName(ref, container, methods[i].getParameterTypes()[0]);
               ejb.addEncLinkRefEntry(encName, jndiName);
            }
            if (!injectors.containsKey(methods[i]))
            {
               injectors.put(methods[i], new JndiMethodInjector(methods[i], encName, container.getEnc()));
            }
         }
      }
      loadMethodInjectors(visitedMethods, clazz.getSuperclass(), container, injectors, isContainer);
   }

   private static void loadFieldInjectors(Class clazz, EJBContainer container, HashMap<AccessibleObject, Injector> injectors, boolean isContainer) throws Exception
   {
      if (clazz == null || clazz.equals(Object.class)) return;
      loadFieldInjectors(clazz.getSuperclass(), container, injectors, isContainer);
      Field[] fields = clazz.getDeclaredFields();
      for (Field field : fields)
      {
         EJB ref = (EJB) InjectionUtil.getAnnotation(EJB.class, container, field, isContainer);
         if (ref != null)
         {
            String encName = ref.name();
            if (encName == null || encName.equals(""))
            {
               encName = InjectionUtil.getEncName(field);
            }
            else
            {
               encName = "env/" + encName;
            }

            EJBContainer ejb = (EJBContainer) container;
            if (!ejb.hasEncEntry(encName))
            {
               String jndiName = getJndiName(ref, container, field.getType());
               ejb.addEncLinkRefEntry(encName, jndiName);
            }
            if (!injectors.containsKey(field))
            {
               injectors.put(field, new JndiFieldInjector(field, encName, container.getEnc()));
            }
         }
      }
   }
}
