/*
 * Copyright (c) 1998-2012 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source 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, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.ejb.session;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.annotation.security.DeclareRoles;
import javax.annotation.security.RolesAllowed;
import javax.ejb.SessionContext;
import javax.ejb.TimerService;
import javax.enterprise.inject.Disposes;
import javax.enterprise.inject.Produces;
import javax.enterprise.inject.Specializes;
import javax.enterprise.inject.spi.AnnotatedField;
import javax.enterprise.inject.spi.AnnotatedMethod;
import javax.enterprise.inject.spi.AnnotatedParameter;
import javax.enterprise.inject.spi.AnnotatedType;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.InjectionTarget;
import javax.enterprise.inject.spi.SessionBeanType;

import com.caucho.config.ConfigException;
import com.caucho.config.gen.BeanGenerator;
import com.caucho.config.inject.BeanBuilder;
import com.caucho.config.inject.InjectManager;
import com.caucho.config.inject.ManagedBeanImpl;
import com.caucho.config.inject.OwnerCreationalContext;
import com.caucho.config.j2ee.BeanName;
import com.caucho.config.j2ee.BeanNameLiteral;
import com.caucho.config.reflect.AnnotatedMethodImpl;
import com.caucho.config.reflect.AnnotatedTypeUtil;
import com.caucho.config.reflect.BaseType;
import com.caucho.ejb.cfg.EjbLazyGenerator;
import com.caucho.ejb.inject.ProcessSessionBeanImpl;
import com.caucho.ejb.inject.SessionRegistrationBean;
import com.caucho.ejb.manager.EjbManager;
import com.caucho.ejb.server.AbstractEjbBeanManager;
import com.caucho.java.gen.JavaClassGenerator;
import com.caucho.loader.DynamicClassLoader;
import com.caucho.naming.Jndi;
import com.caucho.util.L10N;

/**
 * Server container for a session bean.
 */
abstract public class AbstractSessionManager<X> extends AbstractEjbBeanManager<X> {
  private static final L10N L = new L10N(AbstractSessionManager.class);
  private final static Logger log
     = Logger.getLogger(AbstractSessionManager.class.getName());

  private EjbLazyGenerator<X> _lazyGenerator;
  
  private Class<?> _proxyImplClass;
  
  private HashMap<Class<?>, AbstractSessionContext<X,?>> _contextMap
    = new HashMap<Class<?>, AbstractSessionContext<X,?>>();

  private InjectManager _injectManager;
  private Bean<X> _bean;
  
  private String[] _declaredRoles;

  public AbstractSessionManager(EjbManager manager,
                                String ejbName,
                                String moduleName,
                                AnnotatedType<X> rawAnnType,
                                AnnotatedType<X> annotatedType,
                                EjbLazyGenerator<X> lazyGenerator)
  {
    super(manager, ejbName, moduleName, rawAnnType, annotatedType);
    
    _lazyGenerator = lazyGenerator;
    
    DeclareRoles declareRoles 
      = annotatedType.getJavaClass().getAnnotation(DeclareRoles.class);

    RolesAllowed rolesAllowed 
      = annotatedType.getJavaClass().getAnnotation(RolesAllowed.class); 
    
    if (declareRoles != null && rolesAllowed != null) {
      _declaredRoles = new String[declareRoles.value().length +
                                  rolesAllowed.value().length];

      System.arraycopy(declareRoles.value(), 0, 
                       _declaredRoles, 0, 
                       declareRoles.value().length);

      System.arraycopy(rolesAllowed.value(), 0, 
                       _declaredRoles, declareRoles.value().length, 
                       rolesAllowed.value().length);
    }
    else if (declareRoles != null) {
      _declaredRoles = declareRoles.value();
    }
    else if (rolesAllowed != null) {
      _declaredRoles = rolesAllowed.value();
    }
  }

  @Override
  protected String getType()
  {
    return "session:";
  }

  @Override
  public Bean<X> getDeployBean()
  {
    return _bean;
  }
  
  public Class<?> getProxyImplClass()
  {
    return _proxyImplClass;
  }
  
  @Override
  public InjectManager getInjectManager()
  {
    return _injectManager;
  }
  
  protected EjbLazyGenerator<X> getLazyGenerator()
  {
    return _lazyGenerator;
  }
  
  @Override
  public ArrayList<AnnotatedType<? super X>> getLocalApi()
  {
    return _lazyGenerator.getLocalApi();
  }
  
  @Override
  public ArrayList<AnnotatedType<? super X>> getRemoteApi()
  {
    return _lazyGenerator.getRemoteApi();
  }

  @Override
  public AnnotatedType<X> getLocalBean()
  {
    return _lazyGenerator.getLocalBean();
  }
  
  @SuppressWarnings("unchecked")
  protected <T> AbstractSessionContext<X,T> getSessionContext(Class<T> api)
  {
    return (AbstractSessionContext<X,T>) _contextMap.get(api);
  }

  /**
   * Initialize the server during the config phase.
   */
  @Override
  public void init() throws Exception
  {
    Thread thread = Thread.currentThread();
    ClassLoader oldLoader = thread.getContextClassLoader();

    try {
      thread.setContextClassLoader(getClassLoader());

      super.init();
      
      _injectManager = InjectManager.create();
      
      registerInjection();

      for (AnnotatedType<? super X> localApi : _lazyGenerator.getLocalApi()) {
        createContext(localApi.getJavaClass());
      }
      
      AnnotatedType<X> localBean = _lazyGenerator.getLocalBean();
      if (localBean != null)
        createContext(localBean.getJavaClass());
      
      for (AnnotatedType<? super X> remoteApi : _lazyGenerator.getRemoteApi()) {
        createContext(remoteApi.getJavaClass());
      }

      bindContext();
    } finally {
      thread.setContextClassLoader(oldLoader);
    }
    
    registerCdiBeans();

    log.fine(this + " initialized");
  }
  
  @Override
  public void bind()
  {
    Thread thread = Thread.currentThread();
    ClassLoader loader = thread.getContextClassLoader();
    
    try {
      thread.setContextClassLoader(getClassLoader());
      
      boolean isAutoCompile = true;

      if (_proxyImplClass == null) {
        BeanGenerator<X> beanGen = createBeanGenerator();

        String fullClassName = beanGen.getFullClassName();
        
        JavaClassGenerator javaGen = getLazyGenerator().getJavaClassGenerator();
      
        if (javaGen.preload(fullClassName) != null) {
        }
        else if (isAutoCompile) {
          beanGen.introspect();
          
          javaGen.generate(beanGen);
        }
      
        javaGen.compilePendingJava();
      
        _proxyImplClass = generateProxyClass(fullClassName, javaGen);
      }
     
      for (AbstractSessionContext<X,?> cxt : _contextMap.values()) {
        cxt.bind();
      }
    } catch (Exception e) {
      throw ConfigException.create(e);
    } finally {
      thread.setContextClassLoader(loader);
    }
  }

  /**
   * Creates the bean generator for the session bean.
   */
  protected BeanGenerator<X> createBeanGenerator()
  {
    throw new UnsupportedOperationException();
  }
  
  private <T> void createContext(Class<T> api)
  {
    if (_contextMap.get(api) != null)
      throw new IllegalStateException(String.valueOf(api));
    
    AbstractSessionContext<X,T> context = createSessionContext(api);
    
    InjectManager injectManager = context.getInjectManager();
    
    BeanBuilder<SessionContext> factory
    = injectManager.createBeanFactory(SessionContext.class);

    context.setDeclaredRoles(_declaredRoles);

    // XXX: separate additions?
    if (injectManager.getBeans(SessionContext.class).size() == 0)
      injectManager.addBeanDiscover(factory.singleton(context));
   
    _contextMap.put(context.getApi(), context);
    
    try {
      String beanName = getAnnotatedType().getJavaClass().getName();
      
      Jndi.bindDeep("java:comp/EJBContext", context);
      Jndi.bindDeep("java:comp/" + beanName + "/ejbContext", context);
      Jndi.bindDeep("java:comp/" + beanName + "/sessionContext", context);
    } catch (Exception e) {
      log.log(Level.FINER, e.toString(), e);
    }
    
    try {
      TimerService timer = context.getTimerService();
      
      BeanBuilder<TimerService> timerBuilder
        = injectManager.createBeanFactory(TimerService.class);
      
      if (injectManager.getBeans(TimerService.class).size() == 0)
        injectManager.addBeanDiscover(timerBuilder.singleton(timer));
    } catch (Exception e) {
      log.log(Level.ALL, e.toString(), e);
    }
    
    /*
    if (_sessionContext == null) {
    }
    */
  }

  private Class<?> generateProxyClass(String skeletonName,
                                      JavaClassGenerator javaGen)
    throws ClassNotFoundException
  {
    Class<?> proxyImplClass;
    
    Class<?> ejbClass = getAnnotatedType().getJavaClass();
  
    if (! isPublic(ejbClass)
        && (ejbClass.getClassLoader() instanceof DynamicClassLoader)) {
      // ejb/1103
      proxyImplClass = javaGen.loadClassParentLoader(skeletonName, ejbClass);
    }
    else {
      proxyImplClass = javaGen.loadClass(skeletonName);
    }
    
    try {
      Method method = proxyImplClass.getMethod("__caucho_getException");
      
      RuntimeException exn = (RuntimeException) method.invoke(null);

      if (exn != null)
        throw exn;
    } catch (RuntimeException exn) {
      throw exn;
    } catch (Exception exn) {
      throw new RuntimeException(exn);
    }
    // contextImplClass.getDeclaredConstructors();
    
    return proxyImplClass;
  }
  
  private boolean isPublic(Class<?> cl)
  {
    if (! Modifier.isPublic(cl.getModifiers()))
      return false;
    
    Class<?> superClass = cl.getSuperclass();
    if (superClass != null && ! Modifier.isPublic(superClass.getModifiers()))
      return false;
    
    // ejb/5092 - CDI TCK
    for (Class<?> ifClass : cl.getInterfaces()) {
      if (! Modifier.isPublic(ifClass.getModifiers()))
        return false;
    }
    
    return true;
  }
  
  @Override
  public <T> T getLocalProxy(Class<T> api)
  {
    OwnerCreationalContext owner = new OwnerCreationalContext(null);
    
    return getSessionContext(api).createProxy(owner);
  }
  
  protected <T> AbstractSessionContext<X,T>
  createSessionContext(Class<T> api)
  {
    throw new UnsupportedOperationException(getClass().getName());
  }
  
  protected <T> SessionProxyFactory<T>
  createProxyFactory(AbstractSessionContext<X,T> context)
  {
    try {
      if (_proxyImplClass == null)
        bind();
      
      Class<?> []param = new Class[] { getClass(), getContextClass() };
    
      Constructor<?> ctor = _proxyImplClass.getConstructor(param);
      
      return (SessionProxyFactory<T>) ctor.newInstance(this, context);
    } catch (InvocationTargetException e) {
      Throwable cause = e.getCause();
      
      if (cause instanceof RuntimeException)
        throw (RuntimeException) cause;
      else
        throw new IllegalStateException(cause);
    } catch (RuntimeException e) {
      throw e;
    } catch (Exception e) {
      throw new IllegalStateException(e);
    }
  }
  
  protected Class<?> getContextClass()
  {
    throw new UnsupportedOperationException(getClass().getName());
  }

  private void registerCdiBeans()
  {
    ArrayList<AnnotatedType<? super X>> localApiList = getLocalApi();
    ArrayList<AnnotatedType<? super X>> remoteApiList = getRemoteApi();
 
    AnnotatedType<X> rawAnnType = getRawAnnotatedType();
    AnnotatedType<X> annType = getAnnotatedType();
    
    AnnotatedType<X> extAnnType = createExternalAnnotatedType(annType, localApiList);

    InjectManager moduleBeanManager = InjectManager.create();

    ManagedBeanImpl<X> mBean 
      = new ManagedBeanImpl<X>(getInjectManager(), getAnnotatedType(), true);
    mBean.introspect();
    
    InjectionTarget<X> target = mBean.getInjectionTarget();
    target = moduleBeanManager.processInjectionTarget(target, getRawAnnotatedType());
    mBean.setInjectionTarget(target);
    
    Class<?> baseApi = annType.getJavaClass();
      
    Set<Type> apiList = new LinkedHashSet<Type>();

    AnnotatedType<X> baseType = getLocalBean();
    
    if (baseType != null) {
      BaseType sourceApi = moduleBeanManager.createSourceBaseType(baseType.getBaseType());
        
      apiList.addAll(sourceApi.getTypeClosure(moduleBeanManager));
    }
      
    if (localApiList != null) {
      for (AnnotatedType<? super X> api : localApiList) {
        baseApi = api.getJavaClass();
        
        BaseType sourceApi = moduleBeanManager.createSourceBaseType(api.getJavaClass());
          
        apiList.addAll(sourceApi.getTypeClosure(moduleBeanManager));
      }
    }
      
    apiList.add(Object.class);
    
    // ioc/024p
    /*
    if (remoteApiList != null) {
      for (AnnotatedType<? super X> api : remoteApiList) {
        if (baseApi == null)
          baseApi = api.getJavaClass();
        
        BaseType sourceApi = moduleBeanManager.createSourceBaseType(api.getJavaClass());
          
        apiList.addAll(sourceApi.getTypeClosure(moduleBeanManager));
      }
    }
    */
    
    if (baseApi == null)
      throw new NullPointerException();

    _bean = (Bean<X>) createBean(mBean, baseApi, apiList, extAnnType);
      
    // CDI TCK requires the rawAnnType, not the processed one
    ProcessSessionBeanImpl process
      = new ProcessSessionBeanImpl(moduleBeanManager,
                                   _bean,
                                   rawAnnType,
                                   getEJBName(),
                                   getSessionBeanType());

    moduleBeanManager.addBean(_bean, process);
    
    if (! moduleBeanManager.isSpecialized(annType.getJavaClass())) {
      moduleBeanManager.addProduces(_bean, extAnnType);
    }

    for (AnnotatedType<?> localApi : getLocalApi()) {
      registerLocalSession(moduleBeanManager, localApi.getJavaClass());
    }

    for (AnnotatedType<?> remoteApi : getRemoteApi()) {
      registerLocalSession(moduleBeanManager, remoteApi.getJavaClass());
    }
    
    if (getLocalBean() != null) {
      registerLocalSession(moduleBeanManager, getLocalBean().getJavaClass());

    }
  }
  
  private AnnotatedType<X> 
  createExternalAnnotatedType(AnnotatedType<X> baseType,
                              ArrayList<AnnotatedType<? super X>> apiList)
  {
    ExtAnnotatedType<X> extAnnType = new ExtAnnotatedType<X>(baseType);
    
    for (AnnotatedField<? super X> field : baseType.getFields()) {
      if (field.isStatic())
        extAnnType.addField(field);
    }
    
    for (AnnotatedMethod<? super X> method : baseType.getMethods()) {
      AnnotatedMethod<? super X> extMethod = mergeMethod(method, apiList);
      
      if (extMethod != null) {
        extAnnType.addMethod(extMethod);
      }
      else if (method.isAnnotationPresent(Produces.class)
               && ! baseType.isAnnotationPresent(Specializes.class)) {
        // TCK: conflict
        // ioc/07fa, ioc/07a4
        throw new ConfigException(L.l("{0}.{1} is an invalid @Produces EJB method because the method is not in a @Local interface.",
                                      method.getDeclaringType().getJavaClass().getName(),
                                      method.getJavaMember().getName()));
      }
      else if (isDisposes(method)) {
        throw new ConfigException(L.l("{0}.{1} is an invalid @Disposes EJB method because the method is not in a @Local interface.",
                                      method.getDeclaringType().getJavaClass().getName(),
                                      method.getJavaMember().getName()));
      }
    }
    
    return extAnnType;
  }
  
  private boolean isDisposes(AnnotatedMethod<? super X> method)
  {
    for (AnnotatedParameter<? super X> param : method.getParameters()) {
      if (param.isAnnotationPresent(Disposes.class))
        return true;
    }
    
    return false;
  }
  
  private AnnotatedMethod<? super X>
  mergeMethod(AnnotatedMethod<? super X> method,
              ArrayList<AnnotatedType<? super X>> apiList)
  {
    // ioc/07g3
    if (apiList.size() == 0) {
      return method;
    }
    
    for (AnnotatedType<? super X> api : apiList) {
      AnnotatedMethod<? super X> apiMethod
        = AnnotatedTypeUtil.findMethod(api, method);
      
      if (apiMethod != null) {
        AnnotatedMethodImpl<? super X> extMethod
          = new AnnotatedMethodImpl(apiMethod.getDeclaringType(),
                                    method,
                                    apiMethod.getJavaMember(),
                                    toArray(apiMethod.getAnnotations()),
                                    null);

        return extMethod;
      }
    }
    
    return null;
  }
  
  private Annotation []toArray(Set<Annotation> annSet)
  {
    Annotation []ann = new Annotation[annSet.size()];
    annSet.toArray(ann);
    
    return ann;
  }
  
  private <T> void registerLocalSession(InjectManager beanManager, 
                                        Class<T> localApi)
  {
    AbstractSessionContext<X,T> context = getSessionContext(localApi);
      
    BeanName beanName = new BeanNameLiteral(getEJBName());

    SessionRegistrationBean<X,T> regBean
      = new SessionRegistrationBean<X,T>(beanManager, context, _bean, beanName);
      
    beanManager.addBeanImpl(regBean, regBean.getAnnotated());
  }

  protected Bean<X> getBean()
  {
    return _bean;
  }

  abstract protected <T> Bean<T>
  createBean(ManagedBeanImpl<X> mBean,
             Class<T> api,
             Set<Type> apiList,
             AnnotatedType<X> extAnnType);
  
  protected SessionBeanType getSessionBeanType()
  {
    return SessionBeanType.STATELESS;
  }

  @Override
  public void destroy()
  {
    for (AbstractSessionContext<X,?> context : _contextMap.values()) {
      try {
        context.destroy();
      } catch (Exception e) {
        log.log(Level.WARNING, e.toString(), e);
      }
    }
  }
}
