/*
 * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
 * under one or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information regarding copyright
 * ownership. Camunda licenses this file to you under the Apache License,
 * Version 2.0; you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.camunda.bpm.engine.cdi.impl.util;

import java.util.Set;
import java.util.logging.Logger;

import javax.enterprise.context.Dependent;
import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;

import org.camunda.bpm.engine.impl.context.Context;
import org.camunda.bpm.engine.impl.interceptor.CommandContext;

/**
 * Utility class for performing programmatic bean lookups.
 *
 * @author Daniel Meyer
 * @author Mark Struberg
 */
public class ProgrammaticBeanLookup {

  public final static Logger LOG = Logger.getLogger(ProgrammaticBeanLookup.class.getName());

  public static <T> T lookup(Class<T> clazz, BeanManager bm) {
    return lookup(clazz, bm, true);
  }

  public static <T> T lookup(Class<T> clazz, BeanManager bm, boolean optional) {
    Set<Bean<?>> beans = bm.getBeans(clazz);
    T instance = getContextualReference(bm, beans, clazz);
    if (!optional && instance == null) {
      throw new IllegalStateException("CDI BeanManager cannot find an instance of requested type '" + clazz.getName() + "'");
    }
    return instance;
  }

  public static Object lookup(String name, BeanManager bm) {
    return lookup(name, bm, true);
  }

  public static Object lookup(String name, BeanManager bm, boolean optional) {
    Set<Bean<?>> beans = bm.getBeans(name);

    // NOTE: we use Object.class as BeanType of the ContextualReference to resolve.
    // Mark says this is not strictly spec compliant but should work on all implementations.
    // A strictly compliant implementation would
    // - collect all bean types of the bean
    // - calculate the type such that it has the most types in the set of bean types which are assignable from this type.
    Object instance = getContextualReference(bm, beans, Object.class);
    if (!optional && instance == null) {
      throw new IllegalStateException("CDI BeanManager cannot find an instance of requested type '" + name + "'");
    }
    return instance;
  }

  /**
   * @return a ContextualInstance of the given type
   * @throws javax.enterprise.inject.AmbiguousResolutionException if the given type is satisfied by more than one Bean
   * @see #lookup(Class, boolean)
   */
  public static <T> T lookup(Class<T> clazz) {
    return lookup(clazz, true);
  }

  /**
   * @param optional if <code>false</code> then the bean must exist.
   * @return a ContextualInstance of the given type if optional is <code>false</code>. If optional is <code>true</code> null might be returned if no bean got found.
   * @throws IllegalStateException if there is no bean of the given class, but only if optional is <code>false</code>
   * @throws javax.enterprise.inject.AmbiguousResolutionException if the given type is satisfied by more than one Bean
   * @see #lookup(Class, boolean)
   */
  public static <T> T lookup(Class<T> clazz, boolean optional) {
    BeanManager bm = BeanManagerLookup.getBeanManager();
    return lookup(clazz, bm, optional);
  }

  public static Object lookup(String name) {
    BeanManager bm = BeanManagerLookup.getBeanManager();
    return lookup(name, bm);
  }


  @SuppressWarnings("unchecked")
  private static <T> T getContextualReference(BeanManager bm, Set<Bean<?>> beans, Class<?> type) {
    if (beans == null || beans.size() == 0) {
      return null;
    }

    // if we would resolve to multiple beans then BeanManager#resolve would throw an AmbiguousResolutionException
    Bean<?> bean = bm.resolve(beans);
    if (bean == null) {
      return null;

    } else {
      CreationalContext<?> creationalContext = bm.createCreationalContext(bean);

      // if we obtain a contextual reference to a @Dependent scope bean, make sure it is released
      if(isDependentScoped(bean)) {
        releaseOnContextClose(creationalContext, bean);
      }

      return (T) bm.getReference(bean, type, creationalContext);

    }
  }

  private static boolean isDependentScoped(Bean<?> bean) {
    return Dependent.class.equals(bean.getScope());
  }

  private static void releaseOnContextClose(CreationalContext<?> creationalContext, Bean<?> bean) {
    CommandContext commandContext = Context.getCommandContext();
    if(commandContext != null) {
      commandContext.registerCommandContextListener(new CreationalContextReleaseListener(creationalContext));

    } else {
      LOG.warning("Obtained instance of @Dependent scoped bean "+bean +" outside of process engine command context. "
          + "Bean instance will not be destroyed. This is likely to create a memory leak. Please use a normal scope like @ApplicationScoped for this bean.");

    }
  }

}
