/*
 * Copyright (c) 2012, 2018, Werner Keil, Anatole Tresch and others.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); 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.
 *
 * Contributors: @atsticks, @keilw, @otjava
 */
package org.javamoney.cdi.internal;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.annotation.Annotation;
import java.util.*;

import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.inject.Instance;
import javax.enterprise.inject.Vetoed;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.CDI;
import javax.enterprise.util.TypeLiteral;
import javax.inject.Named;
import javax.naming.InitialContext;

/**
 * Small utility class that glues together service loading and CDI, including support for different CDI version.
 */
final class CDIAccessor {

    private static final Logger LOG = LoggerFactory.getLogger(CDIAccessor.class);

    private static final Instance<?> EMPTY_INSTANCE = new EmptyInstance<>();

    private CDIAccessor() {
    }

    private static <T> Instance<T> emptyInstance() {
        return Instance.class.cast(EMPTY_INSTANCE);
    }

    public static <T> Optional<T> getInstance(Class<T> instanceType,
                                              Annotation... qualifiers) {
        BeanManager man = getBeanManager();
        if (man != null) {
            return Optional.ofNullable(getInstanceByType(man, instanceType, qualifiers));
        }
        return Optional.empty();
    }

    private static BeanManager getBeanManager() {
        // 1 try JNDI
        try {
            InitialContext ctx = new InitialContext();
            BeanManager man = (BeanManager) ctx.lookup("comp:/env/BeanManager");
            if (man != null) {
                return man;
            }
        } catch (Exception e) {
            LoggerFactory.getLogger(CDIAccessor.class).debug("Unable to locate BeanManager from JNDI...", e);
        }
        try {
            Class.forName("javax.enterprise.inject.spi.CDI");
            // OK CDI 1.1 is loaded
            return CDI.current().getBeanManager();
        } catch (ClassNotFoundException e) {
            LoggerFactory.getLogger(CDIAccessor.class).debug("CDI accessor not available (CDI 1.0).");
        } catch (Exception e) {
            LoggerFactory.getLogger(CDIAccessor.class).debug("Unable to locate BeanManager from CDI providers...", e);
        }
        // 3 error, CDI not loaded...
        throw new IllegalStateException("CDI is not available.");
    }

    /**
     * Utility method allowing managed instances of beans to provide entry points
     * for non-managed beans (such as {@link org.jboss.weld.environment.se.WeldContainer}). Should only called
     * once Weld has finished booting.
     *
     * @param manager  the BeanManager to use to access the managed instance
     * @param type     the type of the Bean
     * @param bindings the bean's qualifiers
     * @return a managed instance of the bean
     * @throws IllegalArgumentException                               if the given type represents a type
     *                                                                variable
     * @throws IllegalArgumentException                               if two instances of the same qualifier
     *                                                                type are given
     * @throws IllegalArgumentException                               if an instance of an annotation that is
     *                                                                not a qualifier type is given
     * @throws javax.enterprise.inject.UnsatisfiedResolutionException if no beans can be resolved * @throws
     *                                                                AmbiguousResolutionException if the ambiguous dependency
     *                                                                resolution rules fail
     * @throws IllegalArgumentException                               if the given type is not a bean type of
     *                                                                the given bean
     */
    private static <T> T getInstanceByType(BeanManager manager, Class<T> type, Annotation... bindings) {
        final Bean<?> bean = manager.resolve(manager.getBeans(type));
        if (bean == null) {
            return null;
        }
        CreationalContext<?> cc = manager.createCreationalContext(bean);
        return type.cast(manager.getReference(bean, type, cc));
    }

    public static <T> Instance<T> getInstances(Class<T> instanceType,
                                               Annotation... qualifiers) {
        try {
            //noinspection ConstantConditions
            return getInstance(Instance.class).get().select(instanceType, qualifiers);
        } catch (Exception e) {
            LOG.info("Failed to load instances from CDI.", e);
            return emptyInstance();
        }
    }

    public static Set<?> getInstances(String name) {
        try {
            return getBeanManager().getBeans(name);
        } catch (Exception e) {
            LOG.info("Failed to load instances from CDI.", e);
            return Collections.emptySet();
        }

    }

    public static void fireEvent(Object evt, Annotation... qualifiers) {
        try {
            getBeanManager().fireEvent(evt, qualifiers);
        } catch (Exception e) {
            LOG.info("Failed to fire event from CDI.", e);
        }
    }

    @SuppressWarnings("unchecked")
    public static <T> T getNamedInstance(Class<T> type, String id) {
        try {
            Set<?> found = getBeanManager().getBeans(id);
            if (found.isEmpty()) {
                return null;
            }
            for (Object object : found) {
                if (type.isAssignableFrom(object.getClass())) {
                    return (T) object;
                }
            }
            return null;
        } catch (Exception e) {
            LOG.info("Failed to load instances from CDI.", e);
            return null;
        }

    }

    public static String getName(Object o) {
        if (o == null) {
            return "<null>";
        }
        Named named = o.getClass().getAnnotation(Named.class);
        if (named != null) {
            return named.value();
        }
        return o.getClass().getSimpleName();
    }

    @Vetoed
    private static final class EmptyInstance<T> implements Instance<T> {
        @Override
        public Instance<T> select(Annotation... annotations) {
            return this;
        }

        @Override
        public <U extends T> Instance<U> select(Class<U> uClass, Annotation... annotations) {
            return new EmptyInstance<>();
        }

        @Override
        public <U extends T> Instance<U> select(TypeLiteral<U> uTypeLiteral, Annotation... annotations) {
            return new EmptyInstance<>();
        }

        @Override
        public boolean isUnsatisfied() {
            return true;
        }

        @Override
        public boolean isAmbiguous() {
            return false;
        }

        @Override
        public void destroy(T t) {
            // unsupported
        }

        @Override
        public Iterator<T> iterator() {
            return Collections.emptyIterator();
        }

        @Override
        public T get() {
            throw new NoSuchElementException();
        }
    }
}