/*
 * JBoss, Home of Professional Open Source
 * Copyright 2013, Red Hat, Inc. and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.richfaces.cdi.push;

import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;

import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.event.Observes;
import javax.enterprise.event.Reception;
import javax.enterprise.event.TransactionPhase;
import javax.enterprise.inject.spi.AfterBeanDiscovery;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.Extension;
import javax.enterprise.inject.spi.InjectionPoint;
import javax.enterprise.inject.spi.ObserverMethod;
import javax.enterprise.inject.spi.ProcessInjectionTarget;

import org.richfaces.application.push.MessageException;
import org.richfaces.application.push.TopicKey;
import org.richfaces.application.push.TopicsContext;

/**
 * <p>
 * CDI Extension for observing CDI events and delegating events with their payload to Push message bus.
 * </p>
 *
 * <p>
 * This extension was introduced as workaround for feature missing in CDI 1.0 (<a
 * href="https://issues.jboss.org/browse/CDI-36">CDI-36</a>).
 * </p>
 *
 * <p>
 * Thus this extension listens on injection target scanning process for all {@link Push} annotations defined on injection points
 * and then registers observer methods designed for one specific topic. As consequence is that this implementation can't be used
 * for observing dynamically created topic names.
 * </p>
 *
 * @author <a href="http://community.jboss.org/people/lfryc">Lukas Fryc</a>
 */
public class PushCDIExtension implements Extension {

    private Set<Push> pushAnnotations = new LinkedHashSet<Push>();

    /**
     * Scans all the injection points on found injection targets for {@link Push} annotations
     */
    public void processInjectionTarget(@Observes ProcessInjectionTarget<?> pit) {
        scanForPushAnnotations(pit.getInjectionTarget().getInjectionPoints());
    }

    /**
     * Stores all {@link Push} annotations on injection points annotated by {@link Push}
     *
     * @param injectionPoints injection points to be scanned
     */
    private void scanForPushAnnotations(Collection<InjectionPoint> injectionPoints) {
        for (InjectionPoint injectionPoint : injectionPoints) {
            Push pushAnnotation = injectionPoint.getAnnotated().getAnnotation(Push.class);
            if (pushAnnotation != null) {
                pushAnnotations.add(pushAnnotation);
            }
        }
    }

    /**
     * Register observer method {@link PushObserverMethod} for each {@link Push} annotation found in annotation scanning.
     *
     * @param event
     * @param beanManager
     */
    public void afterBeanDiscovery(@Observes AfterBeanDiscovery event, BeanManager beanManager) {
        for (Push pushAnnotation : pushAnnotations) {
            event.addObserverMethod(new PushObserverMethod(beanManager, pushAnnotation));
        }
    }

    /**
     * <p>
     * This class remembers {@link BeanManager} and {@link Push} annotation.
     * </p>
     *
     * <p>
     * {@link BeanManager} is needed to get reference to {@link TopicKeyResolver} and {@link TopicsContext}.
     * </p>
     *
     * <p>
     * {@link TopicKeyResolver} is then used to resolve {@link TopicKey} from {@link Push} annotation.
     * </p>
     *
     * <p>
     * {@link TopicsContext} is used for publishing into RichFaces Push message bus.
     * </p>
     *
     * @author lfryc
     *
     */
    private static class PushObserverMethod implements ObserverMethod<Object> {

        private BeanManager beanManager;
        private Push pushAnnotation;

        public PushObserverMethod(BeanManager beanManager, Push pushAnnotation) {
            this.beanManager = beanManager;
            this.pushAnnotation = pushAnnotation;
        }

        /**
         * Publishes message to topic determined by {@link TopicKey} using {@link TopicsContext}.
         *
         * References to {@link TopicsContext} and {@link TopicKeyResolver} (needed for {@link TopicKey} resolution) are done
         * through {@link BeanManager}.
         */
        public void notify(Object payload) {
            TopicsContext topicsContext = getBeanReference(TopicsContext.class);
            TopicKeyResolver topicKeyResolver = getBeanReference(TopicKeyResolver.class);
            TopicKey topicKey = topicKeyResolver.resolveTopicKey(pushAnnotation);
            try {
                topicsContext.publish(topicKey, payload);
            } catch (MessageException e) {
                throw new PushCDIMessageException("Message wasn't successfully deliver", e);
            }
        }

        public Class<?> getBeanClass() {
            return PushObserverMethod.class;
        }

        public Type getObservedType() {
            return Object.class;
        }

        public Set<Annotation> getObservedQualifiers() {
            return new HashSet<Annotation>(Arrays.asList(pushAnnotation));
        }

        public Reception getReception() {
            return Reception.IF_EXISTS;
        }

        public TransactionPhase getTransactionPhase() {
            return TransactionPhase.IN_PROGRESS;
        }

        private <T> T getBeanReference(Class<T> beanType) {
            Set<Bean<?>> beans = beanManager.getBeans(beanType);
            Bean<?> bean = (Bean<?>) beans.iterator().next();
            CreationalContext<?> ctx = beanManager.createCreationalContext(bean);
            @SuppressWarnings("unchecked")
            T reference = (T) beanManager.getReference(bean, beanType, ctx);
            return reference;
        }
    }
}
