/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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.apache.webbeans.event;

import java.lang.annotation.Annotation;
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.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.annotation.Priority;
import javax.enterprise.context.ContextNotActiveException;
import javax.enterprise.context.Dependent;
import javax.enterprise.context.spi.Context;
import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.event.Observes;
import javax.enterprise.event.ObservesAsync;
import javax.enterprise.event.Reception;
import javax.enterprise.event.TransactionPhase;
import javax.enterprise.inject.spi.AnnotatedMethod;
import javax.enterprise.inject.spi.AnnotatedParameter;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.EventContext;
import javax.enterprise.inject.spi.EventMetadata;
import javax.enterprise.inject.spi.InjectionPoint;
import javax.enterprise.inject.spi.ObserverMethod;
import javax.enterprise.inject.spi.WithAnnotations;

import org.apache.webbeans.component.AbstractOwbBean;
import org.apache.webbeans.component.WebBeansType;
import org.apache.webbeans.config.OWBLogConst;
import org.apache.webbeans.config.WebBeansContext;
import org.apache.webbeans.container.BeanManagerImpl;
import org.apache.webbeans.context.creational.CreationalContextImpl;
import org.apache.webbeans.exception.WebBeansConfigurationException;
import org.apache.webbeans.exception.WebBeansException;
import org.apache.webbeans.logger.WebBeansLoggerFacade;
import org.apache.webbeans.proxy.OwbNormalScopeProxy;
import org.apache.webbeans.spi.plugins.OpenWebBeansEjbPlugin;

/**
 * Defines observers that are declared in observer methods.
 * <p>
 * Example:
 * <pre>
 *  public class X {
 *      
 *      public void afterLoggedIn(@Observes @Current LoggedInEvent event)
 *      {
 *          .....
 *      }
 *  }
 * </pre>
 * Above class X instance observes for the event with type <code>LoggedInEvent</code>
 * and event qualifier is <code>Current</code>. Whenever event is fired, its {@link javax.enterprise.inject.spi.ObserverMethod#notify()}
 * method is called.
 * </p>
 * 
 * @version $Rev$ $Date$
 *
 * @param <T> event type
 */
public class ObserverMethodImpl<T> implements OwbObserverMethod<T>
{
    /**Logger instance*/
    private static final Logger logger = WebBeansLoggerFacade.getLogger(ObserverMethodImpl.class);

    /**Observer owner bean that defines observer method*/
    private final AbstractOwbBean<?> ownerBean;

    /**Using existing bean instance or not*/
    private final boolean ifExist;
    
    /** the observed qualifiers */
    private final Set<Annotation> observedQualifiers;

    /** the type of the observed event */
    private final Type observedEventType;

    /** the transaction phase */
    private final TransactionPhase phase;
    
    /** the injection points */
    private final Set<InjectionPoint> injectionPoints;

    private final Method view;

    /**Annotated method*/
    private AnnotatedMethod<T> annotatedObserverMethod;
    
    /**\@Observes parameter*/
    private AnnotatedParameter<T> annotatedObservesParameter;

    private int priority = ObserverMethod.DEFAULT_PRIORITY;

    private boolean isAsync;
    
    private static class ObserverParams
    {
        private Bean<Object> bean;
        
        private Object instance;
        
        private CreationalContext<Object> creational;
        
        private boolean isBean;
    }

    /**
     * used if the qualifiers and event type are already known, e.g. from the XML.
     */
    public ObserverMethodImpl(AbstractOwbBean<?> ownerBean, AnnotatedMethod<T> annotatedObserverMethod, AnnotatedParameter<T> annotatedObservesParameter)
    {
        this(ownerBean, annotatedObserverMethod, annotatedObservesParameter, true);
    }

    protected ObserverMethodImpl(AbstractOwbBean<?> ownerBean, AnnotatedMethod<T> annotatedObserverMethod, AnnotatedParameter<T> annotatedObservesParameter, boolean fireEvent)
    {
        this.ownerBean = ownerBean;
        this.annotatedObservesParameter = annotatedObservesParameter;
        this.annotatedObserverMethod = annotatedObserverMethod;
        observedEventType = annotatedObservesParameter.getBaseType();
        Observes observes = annotatedObservesParameter.getAnnotation(Observes.class);

        Class<? extends Annotation> observerAnnotation;

        if (observes != null)
        {
            ifExist = observes.notifyObserver() == Reception.IF_EXISTS;
            phase = observes.during();
            observerAnnotation = Observes.class;
        }
        else
        {
            ObservesAsync observesAsync = annotatedObservesParameter.getAnnotation(ObservesAsync.class);
            ifExist = observesAsync.notifyObserver() == Reception.IF_EXISTS;
            phase = TransactionPhase.IN_PROGRESS;
            observerAnnotation = ObservesAsync.class;

            isAsync = true;
        }

        observedQualifiers = new HashSet<>();
        for (Annotation annotation: annotatedObservesParameter.getAnnotations())
        {
            if (ownerBean.getWebBeansContext().getAnnotationManager().isQualifierAnnotation(annotation.annotationType()))
            {
                observedQualifiers.add(annotation);
            }
        }

        // detect the Priority
        Priority priorityAnn = annotatedObservesParameter.getAnnotation(Priority.class);
        if (priorityAnn != null)
        {
            priority = priorityAnn.value();
        }
        
        OpenWebBeansEjbPlugin ejbPlugin = getWebBeansContext().getPluginLoader().getEjbPlugin();
        if (ejbPlugin != null && ejbPlugin.isNewSessionBean(ownerBean.getBeanClass()))
        {
            view = ejbPlugin.resolveViewMethod(ownerBean , annotatedObserverMethod.getJavaMember());
        }
        else
        {
            view = annotatedObserverMethod.getJavaMember();
        }

        injectionPoints = new LinkedHashSet<>();
        for (AnnotatedParameter<?> parameter: annotatedObserverMethod.getParameters())
        {
            if (!parameter.isAnnotationPresent(observerAnnotation))
            {
                injectionPoints.add(getWebBeansContext().getInjectionPointFactory().buildInjectionPoint(ownerBean, parameter, fireEvent));
            }
        }

        checkObserverCondition(annotatedObservesParameter);
    }

    protected void checkObserverCondition(AnnotatedParameter<T> annotatedObservesParameter)
    {
        if (annotatedObservesParameter.getAnnotation(WithAnnotations.class) != null)
        {
            throw new WebBeansConfigurationException("@WithAnnotations must only be used for ProcessAnnotatedType events");
        }
    }

    @Override
    public AbstractOwbBean<?> getOwnerBean()
    {
        return ownerBean;
    }

    @Override
    public boolean isAsync()
    {
        return isAsync;
    }

    @Override
    public int getPriority()
    {
        return priority;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Set<InjectionPoint> getInjectionPoints()
    {
        return injectionPoints;
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated actually. Use the method with the EventContext instead
     */
    @Override
    public void notify(T event)
    {
        notify(new EventContextImpl<>(event, null));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @SuppressWarnings("unchecked")
    public void notify(EventContext<T> eventContext)
    {
        T event = eventContext.getEvent();
        EventMetadata metadata = eventContext.getMetadata();

        AbstractOwbBean<Object> component = (AbstractOwbBean<Object>) ownerBean;
        if (!ownerBean.isEnabled())
        {
            return;
        }

        Object object = null;
        
        List<ObserverParams> methodArgsMap = getMethodArguments(event, metadata);
        
        BeanManagerImpl manager = ownerBean.getWebBeansContext().getBeanManagerImpl();
        CreationalContextImpl<Object> creationalContext = manager.createCreationalContext(component);
        if (metadata != null)
        {
            creationalContext.putInjectionPoint(metadata.getInjectionPoint());
            creationalContext.putEventMetadata(metadata);
        }
        
        ObserverParams[] obargs = null;
        try
        {
            obargs = new ObserverParams[methodArgsMap.size()];
            obargs = methodArgsMap.toArray(obargs);
            Object[] args = new Object[obargs.length];
            int i = 0;
            for(ObserverParams param : obargs)
            {
                args[i++] = param.instance;
            }

            //Static or not
            if (Modifier.isStatic(view.getModifiers()))
            {
                if (!view.isAccessible())
                {
                    view.setAccessible(true);
                }
                //Invoke Method
                view.invoke(null, args);
            }
            else
            {
                Context context;
                try
                {
                    context = manager.getContext(component.getScope());
                }
                catch (ContextNotActiveException cnae)
                {
                    if (ifExist)
                    {
                        return;
                    }
                    // this may happen if we try to e.g. send an event to a @ConversationScoped bean from a ServletListener
                    logger.log(Level.INFO, OWBLogConst.INFO_0010, ownerBean);
                    return;
                }
                

                // on Reception.IF_EXISTS: ignore this bean if a the contextual instance doesn't already exist
                object = context.get(component);

                if (ifExist && object == null)
                {
                    return;
                }

                if (object == null)
                {
                    object = context.get(component, creationalContext);
                }

                if (object == null)
                {
                    // this might happen for EJB components.
                    Type t = component.getBeanClass();

                    // If the bean is an EJB, its beanClass may not be one of
                    // its types. Instead pick a local interface
                    if (component.getWebBeansType() == WebBeansType.ENTERPRISE)
                    {
                        t = (Type) component.getTypes().toArray()[0];
                    }

                    object = manager.getReference(component, t, creationalContext);

                }

                if (object != null)
                {
                    if (!view.isAccessible())
                    {
                        ownerBean.getWebBeansContext().getSecurityService().doPrivilegedSetAccessible(view, true);
                    }

                    if (Modifier.isPrivate(view.getModifiers()))
                    {
                        // since private methods cannot be intercepted, we have to unwrap anny possible proxy
                        if (object instanceof OwbNormalScopeProxy)
                        {
                            object = getWebBeansContext().getInterceptorDecoratorProxyFactory().unwrapInstance(object);
                        }
                    }

                    //Invoke Method
                    invoke(object, args);
                }
            }                        
        }
        catch (InvocationTargetException ite)
        {
            throw new WebBeansException(ite.getCause());
        }
        catch (Exception e)
        {
            throw new WebBeansException(e);
        }
        finally
        {
            creationalContext.removeEventMetadata();
            creationalContext.removeInjectionPoint();
            //Destory bean instance
            if (component.getScope().equals(Dependent.class) && object != null)
            {
                component.destroy(object, creationalContext);
            }
            
            //Destroy observer method dependent instances
            if(methodArgsMap != null)
            {
                for(ObserverParams param : obargs)
                {
                    if(param.isBean && param.bean.getScope().equals(Dependent.class))
                    {
                        param.bean.destroy(param.instance, param.creational);
                    }
                }
            }
        }

    }

    protected void invoke(Object object, Object[] args) throws IllegalAccessException, InvocationTargetException
    {
        view.invoke(object, args);
    }

    /**
     * Gets observer method parameters.
     * @param event event payload
     * @return observer method parameters
     */
    protected List<ObserverParams> getMethodArguments(Object event, EventMetadata metadata)
    {
        List<ObserverParams> list = new ArrayList<>();
        if (annotatedObservesParameter.getPosition() == 0)
        {
            ObserverParams param = new ObserverParams();
            param.instance = event;
            list.add(param);
        }
        WebBeansContext webBeansContext = ownerBean.getWebBeansContext();
        BeanManagerImpl manager = webBeansContext.getBeanManagerImpl();

        for (InjectionPoint injectionPoint: injectionPoints)
        {
            Bean<Object> injectedBean = (Bean<Object>)manager.getInjectionResolver().getInjectionPointBean(injectionPoint);
            
            CreationalContextImpl<Object> creational = manager.createCreationalContext(injectedBean);
            creational.putInjectionPoint(metadata.getInjectionPoint());
            creational.putInjectionPoint(injectionPoint);
            creational.putEventMetadata(metadata);
            Object instance;
            try
            {
                instance = manager.getReference(injectedBean, null, creational);
            }
            finally
            {
                creational.removeEventMetadata();
                creational.removeInjectionPoint();
                creational.removeInjectionPoint();
            }
                                
            ObserverParams param = new ObserverParams();
            param.isBean = true;
            param.creational = creational;
            param.instance = instance;
            param.bean = injectedBean;
            list.add(param);

            if (list.size() == annotatedObservesParameter.getPosition())
            {
                param = new ObserverParams();
                param.instance = event;
                list.add(param);                 
            }
        }
                
        return list;
    }

    /**
     * Returns observer owner bean.
     * 
     * @return the bean
     */
    @Override
    @SuppressWarnings("unchecked")
    public Class<?> getBeanClass()
    {
        return ownerBean.getBeanClass();
    }

    /** 
     * {@inheritDoc}
     */
    @Override
    public Set<Annotation> getObservedQualifiers()
    {
        return observedQualifiers;
    }
    
    /** 
     * {@inheritDoc}
     */
    @Override
    public Type getObservedType()
    {
        return observedEventType;
    }

    /** 
     * {@inheritDoc}
     */
    @Override
    public Reception getReception()
    {
        return ifExist ? Reception.IF_EXISTS : Reception.ALWAYS;
    }

    @Override
    public TransactionPhase getTransactionPhase()
    {
        return phase;
    }
    
    public AnnotatedMethod<T> getObserverMethod()
    {
        return annotatedObserverMethod;
    }

    protected WebBeansContext getWebBeansContext()
    {
        return ownerBean.getWebBeansContext();
    }
    
    /**
     * Provides a way to set the observer method. This may need to be done for
     * EJBs so that the method used will be from an interface and not the
     * EJB class that likely can not be invoked on the EJB proxy
     * 
     * @param m method to be invoked as the observer
     */
    public void setObserverMethod(AnnotatedMethod<T> m)
    {
        annotatedObserverMethod = m;
    }
}
