/*
 * Copyright 2015 The original authors
 *
 * 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.vaadin.spring.events.internal;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.support.AopUtils;
import org.vaadin.spring.events.Event;
import org.vaadin.spring.events.EventBus;
import org.vaadin.spring.events.EventBusListener;
import org.vaadin.spring.events.EventScope;
import org.vaadin.spring.events.annotation.EventBusListenerMethod;

import javax.annotation.PreDestroy;
import java.io.Serializable;
import java.lang.reflect.Method;

/**
 * Implementation of {@link org.vaadin.spring.events.EventBus} that publishes events with one specific
 * {@link org.vaadin.spring.events.EventScope}.
 * A scoped event bus can also have a parent event bus, in which case all events published on the parent bus will
 * propagate to the scoped event bus as well.
 *
 * @author Petter Holmström ([email protected])
 */
public abstract class ScopedEventBus implements EventBus, Serializable {

    private static final long serialVersionUID = -582697574672947883L;
    private final Logger logger = LoggerFactory.getLogger(getClass());
    private final EventScope eventScope;

    private final ListenerCollection listeners = new ListenerCollection();

    private EventBus parentEventBus;
    private EventBusListener<Object> parentListener = new EventBusListener<Object>() {

        private static final long serialVersionUID = -8276470908536582989L;

        @Override
        public void onEvent(final Event<Object> event) {
            logger.debug("Propagating event [{}] from parent event bus [{}] to event bus [{}]", event, parentEventBus,
                    ScopedEventBus.this);
            listeners.publish(event);
        }

    };

    /**
     * @param scope the scope of the events that this event bus handles.
     */
    public ScopedEventBus(EventScope scope) {
        this(scope, null);
    }

    /**
     * @param scope          the scope of the events that this event bus handles.
     * @param parentEventBus the parent event bus to use, may be {@code null};
     */
    public ScopedEventBus(EventScope scope, EventBus parentEventBus) {
        eventScope = scope;
        this.parentEventBus = parentEventBus;
        if (parentEventBus != null) {
            if (AopUtils.isJdkDynamicProxy(parentEventBus)) {
                logger.debug("Parent event bus [{}] is proxied, trying to get the real EventBus instance",
                        parentEventBus);
                try {
                    this.parentEventBus = (EventBus) ((Advised) parentEventBus).getTargetSource().getTarget();
                } catch (Exception e) {
                    logger.error("Could not get target EventBus from proxy", e);
                    throw new RuntimeException("Could not get parent event bus", e);
                }
            }
            logger.debug("Using parent event bus [{}]", this.parentEventBus);
            this.parentEventBus.subscribe(parentListener);
        }
    }

    @PreDestroy
    void destroy() {
        logger.trace("Destroying event bus [{}] and removing all listeners", this);
        listeners.clear();
        if (parentEventBus != null) {
            parentEventBus.unsubscribe(parentListener);
        }
    }

    @Override
    public EventScope getScope() {
        return eventScope;
    }

    @Override
    public <T> void publish(Object sender, T payload) {
        publish("", sender, payload);
    }

    @Override
    public <T> void publish(String topic, Object sender, T payload) {
        logger.debug("Publishing payload [{}] from sender [{}] on event bus [{}] in topic  [{}]", payload, sender, this,
                topic);
        listeners.publish(new Event<T>(this, sender, payload, topic));
    }

    @Override
    public <T> void publish(EventScope scope, Object sender, T payload) throws UnsupportedOperationException {
        publish(scope, "", sender, payload);
    }

    @Override
    public <T> void publish(EventScope scope, String topic, Object sender, T payload)
            throws UnsupportedOperationException {
        logger.debug("Trying to publish payload [{}] from sender [{}] using scope [{}] on event bus [{}] in topic [{}]",
                payload, sender, scope, this, topic);

        if (eventScope.equals(scope)) {
            publish(topic, sender, payload);
        } else if (parentEventBus != null) {
            parentEventBus.publish(scope, topic, sender, payload);
        } else {
            logger.warn("Could not publish payload with scope [{}] on event bus [{}]", scope, this);
            throw new UnsupportedOperationException("Could not publish event with scope " + scope);
        }
    }

    @Override
    public <T> void subscribe(EventBusListener<T> listener) {
        subscribe(listener, true);
    }

    @Override
    public <T> void subscribeWithWeakReference(EventBusListener<T> listener) {
        subscribeWithWeakReference(listener, true);
    }

    @Override
    public <T> void subscribe(EventBusListener<T> listener, String topic) {
        logger.trace("Subscribing listener [{}] to event bus [{}]", listener, this);
        listeners.add(new EventBusListenerWrapper(this, listener, topic, true));
    }

    @Override
    public <T> void subscribe(EventBusListener<T> listener, boolean includingPropagatingEvents) {
        logger.trace("Subscribing listener [{}] to event bus [{}], includingPropagatingEvents = {}", listener, this,
                includingPropagatingEvents);
        listeners.add(new EventBusListenerWrapper(this, listener, null, includingPropagatingEvents));
    }

    @Override
    public <T> void subscribeWithWeakReference(EventBusListener<T> listener, String topic) {
        logger.trace("Subscribing listener [{}] to event bus [{}] with weak reference",
                listener, this);
        listeners.addWithWeakReference(new EventBusListenerWrapper(this, listener, topic, true));
    }

    @Override
    public <T> void subscribeWithWeakReference(EventBusListener<T> listener, boolean includingPropagatingEvents) {
        logger.trace("Subscribing listener [{}] to event bus [{}] with weak reference, includingPropagatingEvents = {}",
                listener, this, includingPropagatingEvents);
        listeners.addWithWeakReference(new EventBusListenerWrapper(this, listener, null, includingPropagatingEvents));
    }

    @Override
    public void subscribe(Object listener) {
        subscribe(listener, true);
    }

    @Override
    public void subscribe(Object listener, String topic) {
        subscribe(listener, topic, true, false);
    }

    @Override
    public void subscribeWithWeakReference(Object listener, String topic) {
        subscribe(listener, topic, true, false);
    }

    @Override
    public void subscribeWithWeakReference(Object listener) {
        subscribeWithWeakReference(listener, true);
    }

    @Override
    public void subscribe(Object listener, boolean includingPropagatingEvents) {
        subscribe(listener, null, includingPropagatingEvents, false);
    }

    @Override
    public void subscribeWithWeakReference(Object listener, boolean includingPropagatingEvents) {
        subscribe(listener, null, includingPropagatingEvents, true);
    }

    private void subscribe(final Object listener, final String topic, final boolean includingPropagatingEvents,
                           final boolean weakReference) {
        logger.trace("Subscribing listener [{}] to event bus [{}], includingPropagatingEvents = {}, weakReference = {}",
                listener, this, includingPropagatingEvents, weakReference);

        final int[] foundMethods = new int[1];
        ClassUtils.visitClassHierarchy(clazz -> {
            for (Method m : clazz.getDeclaredMethods()) {
                if (m.isAnnotationPresent(EventBusListenerMethod.class)) {
                    if (m.getParameterTypes().length == 1) {
                        logger.trace("Found listener method [{}] in listener [{}]", m.getName(), listener);
                        MethodListenerWrapper l = new MethodListenerWrapper(ScopedEventBus.this, listener, topic,
                                includingPropagatingEvents, m);
                        if (weakReference) {
                            listeners.addWithWeakReference(l);
                        } else {
                            listeners.add(l);
                        }
                        foundMethods[0]++;
                    } else {
                        throw new IllegalArgumentException(
                                "Listener method " + m.getName() + " does not have the required signature");
                    }
                }
            }
        }, listener.getClass());

        if (foundMethods[0] == 0) {
            logger.warn("Listener [{}] did not contain a single listener method!", listener);
        }
    }

    @Override
    public <T> void unsubscribe(EventBusListener<T> listener) {
        unsubscribe((Object) listener);
    }

    @Override
    public void unsubscribe(final Object listener) {
        logger.trace("Unsubscribing listener [{}] from event bus [{}]", listener, this);
        listeners.removeAll(l -> (l instanceof AbstractListenerWrapper)
                && (((AbstractListenerWrapper) l).getListenerTarget() == listener));
    }

    /**
     * Gets the parent of this event bus. Events published on the parent bus will also
     * propagate to the listeners of this event bus.
     *
     * @return the parent event bus, or {@code null} if this event bus has no parent.
     */
    protected EventBus getParentEventBus() {
        return parentEventBus;
    }

    @Override
    public String toString() {
        return String.format("%s[id=%x, eventScope=%s, parentEventBus=%s]", getClass().getSimpleName(),
                System.identityHashCode(this), eventScope, parentEventBus);
    }

    /**
     * Default implementation of {@link org.vaadin.spring.events.EventBus.ApplicationEventBus}.
     */
    public static class DefaultApplicationEventBus extends ScopedEventBus implements ApplicationEventBus {

        public DefaultApplicationEventBus() {
            super(EventScope.APPLICATION);
        }
    }

    /**
     * Default implementation of {@link org.vaadin.spring.events.EventBus.SessionEventBus}.
     */
    public static class DefaultSessionEventBus extends ScopedEventBus implements SessionEventBus {

        public DefaultSessionEventBus(ApplicationEventBus parentEventBus) {
            super(EventScope.SESSION, parentEventBus);
        }
    }

    /**
     * Default implementation of {@link org.vaadin.spring.events.EventBus.UIEventBus}.
     */
    public static class DefaultUIEventBus extends ScopedEventBus implements UIEventBus {

        public DefaultUIEventBus(SessionEventBus parentEventBus) {
            super(EventScope.UI, parentEventBus);
        }
    }
}