/*
 * 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.config.gen;

import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.enterprise.inject.InjectionException;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.Decorator;
import javax.enterprise.inject.spi.InjectionPoint;
import javax.enterprise.inject.spi.InterceptionType;
import javax.enterprise.inject.spi.Interceptor;

import com.caucho.config.ConfigException;
import com.caucho.config.inject.CreationalContextImpl;
import com.caucho.config.inject.DecoratorBean;
import com.caucho.config.inject.DelegateProxyBean;
import com.caucho.config.inject.DependentCreationalContext;
import com.caucho.config.inject.InjectManager;
import com.caucho.config.inject.InterceptorRuntimeBean;
import com.caucho.config.reflect.AnnotatedTypeUtil;
import com.caucho.util.L10N;

/**
 * Utilities
 */
public class CandiUtil {
  private static final L10N L = new L10N(CandiUtil.class);
  private static final Logger log = Logger.getLogger(CandiUtil.class.getName());

  public static final Object []NULL_OBJECT_ARRAY = new Object[0];
  
  private static final Method _dummyPostConstruct;
  private static final Method _dummyPreDestroy;

  private CandiUtil()
  {
  }
  
  public static Object invoke(Method method, Object bean, Object ...args)
  {
    try {
      return method.invoke(bean, args);
    } catch (InvocationTargetException e) {
      if (e.getCause() instanceof RuntimeException)
        throw (RuntimeException) e.getCause();
      else
        throw new RuntimeException(method.getName() + ": " + e, e.getCause());
    } catch (Exception e) {
      throw new RuntimeException(method.getName() + ": " + e, e);
    }
  }

  public static int []createInterceptors(InjectManager manager,
                                         ArrayList<InterceptorRuntimeBean<?>> staticBeans,
                                         ArrayList<Interceptor<?>> beans,
                                         int []staticIndexList,
                                         InterceptionType type,
                                         Annotation ...bindings)
  {
    // ioc/05ah
    if (manager.isChildManager()) {
      manager = manager.getParent();
    }
    
    ArrayList<Integer> indexList = new ArrayList<Integer>();
    
    List<Interceptor<?>> interceptors;
    
    if (bindings != null && bindings.length > 0) {
      interceptors = manager.resolveInterceptors(type, bindings);
    }
    else
      interceptors = new ArrayList<Interceptor<?>>();

    if (staticIndexList != null) {
      for (int i = 0; i < staticIndexList.length; i++) {
        int staticIndex = staticIndexList[i];
        InterceptorRuntimeBean<?> staticBean = staticBeans.get(staticIndex);
      
        addStaticBean(staticBean, indexList, beans, type);
      }
    }

    for (int i = 0; i < interceptors.size(); i++) {
      Interceptor<?> interceptor = interceptors.get(i);

      int index = beans.indexOf(interceptor);

      if (index >= 0)
        indexList.add(index);
      else {
        indexList.add(beans.size());
        beans.add(interceptor);
      }
    }

    int []indexArray = new int[indexList.size()];
    for (int i = 0; i < indexList.size(); i++) {
      indexArray[i] = indexList.get(i);
    }
    
    return indexArray;
  }
  
  private static void 
  addStaticBean(InterceptorRuntimeBean<?> staticBean,
                ArrayList<Integer> indexList,
                ArrayList<Interceptor<?>> beans,
                InterceptionType type)
  {
    if (staticBean == null)
      return;
    
    if (! beans.contains(staticBean))
      beans.add(staticBean);
    
    if (staticBean.isAllowParent(type))
      addStaticBean(staticBean.getParent(), indexList, beans, type);

    if (staticBean.intercepts(type)) {
      int index = beans.indexOf(staticBean);
      indexList.add(index);
    }
  }

  public static void createInterceptors(InjectManager manager,
                                        ArrayList<Interceptor<?>> beans,
                                        Annotation ...bindings)
  {
    if (bindings == null || bindings.length == 0)
      return;
    
    createInterceptors(manager, beans, InterceptionType.AROUND_INVOKE, bindings);
    createInterceptors(manager, beans, InterceptionType.POST_CONSTRUCT, bindings);
    createInterceptors(manager, beans, InterceptionType.PRE_DESTROY, bindings);
  }
  
  public static void createInterceptors(InjectManager manager,
                                        ArrayList<Interceptor<?>> beans,
                                        InterceptionType type,
                                        Annotation ...bindings)
  {
    List<Interceptor<?>> interceptors;

    interceptors = manager.resolveInterceptors(type, bindings);

    for (Interceptor<?> bean : interceptors) {
       int index = beans.indexOf(bean);

      if (index < 0)
        beans.add(bean);
    }
  }

  public static void validatePassivating(Class<?> cl, 
                                         ArrayList<Interceptor<?>> beans)
  {
    for (Interceptor<?> interceptor : beans) {
      validatePassivating(cl, interceptor, "interceptor");
    }
  }

  public static void validatePassivatingDecorators(Class<?> cl, 
                                                   List<Decorator<?>> beans)
  {
    for (Decorator<?> decorator : beans) {
      validatePassivating(cl, decorator, "decorator");
    }
  }
  
  public static void validatePassivating(Class<?> cl,
                                         Bean<?> bean,
                                         String typeName)
  {
    Class<?> beanClass = bean.getBeanClass();

    // ioc/05ai
    if (! Serializable.class.isAssignableFrom(beanClass)) {
      ConfigException exn
      = new ConfigException(L.l("{0}: {1} is an invalid {2} because it is not serializable.",
                                cl.getName(),
                                bean,
                                typeName));

      throw exn;
      // InjectManager.create().addDefinitionError(exn);
    }

    for (InjectionPoint ip : bean.getInjectionPoints()) {
      if (ip.isTransient() || ip.isDelegate())
        continue;
      
      Class<?> type = getRawClass(ip.getType());
      
      if (type.isInterface())
        continue;

      if (! Serializable.class.isAssignableFrom(type)) {
        ConfigException exn
          = new ConfigException(L.l("{0}: {1} is an invalid {4} because its injection point '{2}' of type {3} is not serializable.",
                                    cl.getName(),
                                    bean,
                                    ip.getMember().getName(),
                                    ip.getType(),
                                    typeName));

        throw exn;
      }
    }
  }
  
  public static Class<?> getRawClass(Type type)
  {
    if (type instanceof Class<?>)
      return (Class<?>) type;
    else if (type instanceof ParameterizedType)
      return (Class<?>) ((ParameterizedType) type).getRawType();
    else
      return Object.class;
  }

  public static Interceptor<?> []createMethods(ArrayList<Interceptor<?>> beans,
                                               InterceptionType type,
                                               int []indexChain)
  {
    Interceptor<?> []methods = new Interceptor<?>[indexChain.length];

    for (int i = 0; i < indexChain.length; i++) {
      int index = indexChain[i];

      methods[i] = beans.get(index);
    }

    return methods;
  }

  public static Method []createDecoratorMethods(List<Decorator<?>> decorators,
                                                String methodName,
                                                Class<?> ...paramTypes)
  {
    Method []methods = new Method[decorators.size()];
    
    for (int i = 0; i < decorators.size(); i++) {
      Decorator<?> decorator = decorators.get(i);
      Class<?> beanClass = decorator.getBeanClass();
      
      try {
        methods[decorators.size() - i - 1] = beanClass.getMethod(methodName, paramTypes);
        methods[decorators.size() - i - 1].setAccessible(true);
      } catch (Exception e) {
        log.log(Level.FINEST, e.toString(), e);
      }
    }

    return methods;
  }

  public static Method getMethod(Class<?> cl,
                                 String methodName,
                                 Class<?> ...paramTypes)
    throws Exception
  {
    Method method = null;
    Exception firstException = null;

    do {
      try {
        method = cl.getDeclaredMethod(methodName, paramTypes);
      } catch (Exception e) {
        if (firstException == null)
          firstException = e;

        cl = cl.getSuperclass();
      }
    } while (method == null && cl != null);

    if (method == null)
      throw firstException;

    method.setAccessible(true);

    return method;
  }

  public static Method findMethod(Class<?> cl,
                                  String methodName,
                                  Class<?> ...paramTypes)
  {
    for (Class<?> ptr = cl; ptr != null; ptr = ptr.getSuperclass()) {
      for (Method method : ptr.getDeclaredMethods()) {
        if (AnnotatedTypeUtil.isMatch(method, methodName, paramTypes)) {
          return method;
        }
      }
    }

    log.warning(L.l("'{0}' is an unknown method in {1}",
                    methodName, cl.getName()));
    
    return null;
  }
  
  public static Method findAccessibleMethod(Class<?> cl,
                                            String methodName,
                                            Class<?> ...paramTypes)
  {
    Method method = findMethod(cl, methodName, paramTypes);
    
    if (method != null)
      method.setAccessible(true);
    
    return method;
  }
    
  public static Object []generateProxyDelegate(InjectManager manager,
                                               List<Decorator<?>> beans,
                                               Object delegateProxy,
                                               CreationalContextImpl<?> parentEnv)
  {
    Object []instances = new Object[beans.size()];

    DependentCreationalContext<Object> proxyEnv
      = new DependentCreationalContext<Object>(DelegateProxyBean.BEAN, parentEnv, null);
    
    proxyEnv.push(delegateProxy);
    
    for (int i = 0; i < beans.size(); i++) {
      Decorator<?> bean = beans.get(i);
      
      CreationalContextImpl<?> env = new DependentCreationalContext(bean, proxyEnv, null);

      Object instance = manager.getReference(bean, bean.getBeanClass(), env);
      
      // XXX:
      InjectionPoint ip = getDelegate(bean);

      if (ip.getMember() instanceof Field) {
        Field field = (Field) ip.getMember();
        field.setAccessible(true);
      
        try {
          field.set(instance, delegateProxy);
        } catch (Exception e) {
          throw new InjectionException(e);
        }
      } else if (ip.getMember() instanceof Method) {
        Method method = (Method) ip.getMember();
        method.setAccessible(true);
      
        try {
          method.invoke(instance, delegateProxy);
        } catch (Exception e) {
          throw new InjectionException(e);
        }
      }
      
      /*
      DecoratorBean<?> decoratorBean = (DecoratorBean<?>) bean;
      decoratorBean.setDelegate(instance, proxy);
      */

      instances[beans.size() - i - 1] = instance;
      
      if (parentEnv instanceof CreationalContextImpl<?>) {
        // InjectionPoint ip = decoratorBean.getDelegateInjectionPoint();
      
        ((CreationalContextImpl<?>) parentEnv).setInjectionPoint(ip);
      }
    }

    return instances;
  }
  
  private static InjectionPoint getDelegate(Decorator<?> bean)
  {
    if (bean instanceof DecoratorBean)
      return ((DecoratorBean) bean).getDelegateInjectionPoint();

    for (InjectionPoint ip : bean.getInjectionPoints()) {
      if (ip.isDelegate())
        return ip;
    }
    
    throw new IllegalStateException(String.valueOf(bean));
  }

  public static int nextDelegate(Object []beans,
                                 Method []methods,
                                 int index)
  {
    for (index--; index >= 0; index--) {
      if (methods[index] != null) {
        return index;
      }
    }

    return index;
  }

  public static int nextDelegate(Object []beans,
                                 Class<?> []apis,
                                 int index)
  {
    for (index--; index >= 0; index--) {
      for (Class<?> api : apis) {
        if (api.isAssignableFrom(beans[index].getClass()))
          return index;
      }
    }

    return index;
  }
  
  public static Method getDummyPostConstruct()
  {
    return _dummyPostConstruct;
  }
  
  public static void dummyPostConstruct()
  {
  }
  
  public static Method getDummyPreDestroy()
  {
    return _dummyPreDestroy;
  }
  
  public static void dummyPreDestroy()
  {
  }
  
  static {
    try {
      _dummyPostConstruct = CandiUtil.class.getMethod("dummyPostConstruct");
      _dummyPreDestroy = CandiUtil.class.getMethod("dummyPreDestroy");
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
}
