/*
 * JBoss, Home of Professional Open Source
 * Copyright 2011, Red Hat, Inc. and/or its affiliates, and individual
 * contributors by the @authors tag. See the copyright.txt in the
 * distribution for a full listing of individual contributors.
 *
 * 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.
 */
package org.jboss.solder.unwraps;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import javax.enterprise.context.Dependent;
import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.inject.spi.Annotated;
import javax.enterprise.inject.spi.AnnotatedMethod;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.InjectionPoint;
import javax.inject.Named;

import javassist.util.proxy.MethodFilter;
import javassist.util.proxy.ProxyFactory;
import javassist.util.proxy.ProxyObject;
import org.jboss.solder.bean.Beans;
import org.jboss.solder.literal.DefaultLiteral;

import static org.jboss.solder.reflection.Reflections.cast;

/**
 * Bean implementation that produces a JDK proxy
 * <p/>
 * when a method is invoked on the proxy it calls the Unwraps producer method
 * and invokes the method on the returned object
 *
 * @author Stuart Douglas
 */
public class UnwrapsProducerBean<M> implements Bean<M> {

    final private Class<?> beanClass;

    final private String name;

    final private Set<Annotation> qualifiers;

    final private Set<Annotation> declaringClassQualifiers;

    final private Set<Type> types;

    final private Class<M> proxyClass;

    final private BeanManager manager;

    final private AnnotatedMethod<?> method;

    private final static Annotation[] defaultQualifiers = {DefaultLiteral.INSTANCE};

    public UnwrapsProducerBean(AnnotatedMethod<?> method, BeanManager manager) {
        this(method, resolveQualifiers(method, manager), resolveQualifiers(method.getDeclaringType(), manager), manager);
    }

    public UnwrapsProducerBean(AnnotatedMethod<?> method, Set<Annotation> methodQualifiers, Set<Annotation> beanQualifiers, BeanManager manager) {
        this.method = method;
        beanClass = method.getDeclaringType().getJavaClass();
        // get the name
        if (method.isAnnotationPresent(Named.class)) {
            name = method.getAnnotation(Named.class).value();
        } else {
            name = null;
        }
        // get the qualifiers
        qualifiers = new HashSet<Annotation>(methodQualifiers);
        declaringClassQualifiers = new HashSet<Annotation>(beanQualifiers);
        // get the bean types
        types = new HashSet<Type>();
        Set<Class<?>> classes = new HashSet<Class<?>>();
        for (Type t : method.getTypeClosure()) {
            if (t instanceof Class<?>) {
                Class<?> c = (Class<?>) t;
                types.add(c);
                classes.add(c);
            } else if (t instanceof ParameterizedType) {
                types.add(t);
            }
        }
        // build the properties
        Class<?>[] iarray = new Class[classes.size()];
        int count = 0;
        this.manager = manager;
        for (Class<?> c : classes) {
            iarray[count++] = c;
        }
        ProxyFactory f = new ProxyFactory();
        Class<?> retType = method.getJavaMember().getReturnType();
        if (retType.isInterface()) {
            f.setSuperclass(Object.class);
            Class<?>[] ifaces = {retType};
            f.setInterfaces(ifaces);
        } else {
            f.setSuperclass(retType);
        }

        f.setFilter(new MethodFilter() {
            public boolean isHandled(Method m) {
                // ignore finalize()
                return !m.getName().equals("finalize");
            }
        });
        proxyClass = cast(f.createClass());
    }

    private static Set<Annotation> resolveQualifiers(Annotated method, BeanManager manager) {
        Set<Annotation> qualifiers = Beans.getQualifiers(manager, method.getAnnotations());
        if (qualifiers.isEmpty()) {
            qualifiers.add(DefaultLiteral.INSTANCE);
        }
        return qualifiers;
    }

    public Class<?> getBeanClass() {
        return beanClass;
    }

    public Set<InjectionPoint> getInjectionPoints() {
        return Collections.emptySet();
    }

    public String getName() {
        return name;
    }

    public Set<Annotation> getQualifiers() {
        return qualifiers;
    }

    /**
     * the proxies that are injected all have Dependant scope
     */
    public Class<? extends Annotation> getScope() {
        return Dependent.class;
    }

    public Set<Class<? extends Annotation>> getStereotypes() {
        return Collections.emptySet();
    }

    public Set<Type> getTypes() {
        return types;
    }

    public boolean isAlternative() {
        return false;
    }

    public boolean isNullable() {
        return false;
    }

    public M create(CreationalContext<M> creationalContext) {
        Set<Bean<?>> beans = manager.getBeans(InjectionPoint.class, defaultQualifiers);
        Bean<?> injectionPointBean = (Bean<?>) beans.iterator().next();
        InjectionPoint injectionPoint = (InjectionPoint) manager.getReference(injectionPointBean, InjectionPoint.class, creationalContext);
        UnwrapsInvocationHandler hdl = new UnwrapsInvocationHandler(manager, this.method, this, injectionPoint, declaringClassQualifiers);
        try {
            M obj = proxyClass.newInstance();
            ((ProxyObject) obj).setHandler(hdl);
            creationalContext.push(obj);
            return obj;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void destroy(M instance, CreationalContext<M> creationalContext) {
        creationalContext.release();
    }

}
