/*
 * Copyright (C) 2016 PGS Software SA
 * Copyright (C) 2012 Square, Inc.
 * Copyright (C) 2007 The Guava Authors
 *
 * This software may be modified and distributed under the terms
 * of the MIT license.  See the LICENSE file for details.
 *
 */
package com.pgssoft.gimbus;

import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Dispatches events to subscribers handlers, and provides ways for subscribers to register themselves.
 * <p/>
 * <p>The EventBus allows publish-subscribe-style communication between components without requiring
 * the components to explicitly register with one another (and thus be aware of each other).
 * It is <em>not</em> intended for interprocess communication.
 * <p/>
 * <h2>Receiving Events</h2>
 * <p/>
 * <p>To receive events, an object should:
 * <ol>
 * <li>Expose a method, known as the <i>event handler</i>, which accepts a single argument
 * of the type of event desired;
 * <li>Mark it with a {@link Subscribe} annotation;
 * <li>Pass itself to an EventBus instance's {@link #register(Object)} method.
 * </ol>
 * <p/>
 * <h2>Posting Events</h2>
 * <p/>
 * <p>To post an event, simply provide the event object to the {@link #post(Object)} method, or
 * the {@link #send(Object)} method, or any of its variants. The EventBus instance will determine the
 * type of event and route it to all registered subscribers.
 * <p/>
 * <p>Events are routed based on their type &mdash; an event will be delivered to any subscriber for
 * any type to which the event is <em>assignable</em>. This includes implemented interfaces, all
 * superclasses, and all interfaces implemented by superclasses.
 * <p/>
 * <h2>Event Handlers</h2>
 * <p/>
 * <p>Event handler methods must accept only one argument: the event.
 * <p/>
 * <p>Subscribers should not, in general, throw. If they do, the EventBus will TODO catch and log the
 * exception. This is rarely the right solution for error handling and should not be relied upon; it
 * is intended solely to help find problems during development.
 * <p/>
 * <h2>Dead Events</h2>
 * <p>If an event is posted, but no registered subscribers can accept it, it is considered "dead."
 * To give the system a second chance to handle dead events, they are wrapped in an instance of
 * {@link DeadEvent} and reposted.
 * <p/>
 * <p>This class is safe for concurrent use.
 *
 * @author Lukasz Plominski (Android EventBus code)
 * @author Cliff Biffle (Guava inherited code)
 */
@SuppressWarnings("unused")
public class EventBus {

//todo add sticky events
//sticky event is and event that when posted stays in bus until some conditions are meet, ex timeout, or it is removed explicitly.
//there should be only one sticky event for event class, new one replaces old one.
//sticky events should be delivered to subscriber just after he registered in bus

    /**
     * Bus will deliver the even in the same thread as object was registered in.
     * Object can always have assigned different default thread, see assignThreadForSubscriber().
     * <p/>
     * This is the default delivery mode, chosen for compatibility with Otto.
     * <p/>
     * In many cases this is close equivalent to the DELIVER_IN_UI_THREAD, id You registered subscriber
     * in the UI thread;
     * <p/>
     * The thread have to have a Looper. If there is no Looper, effectively it will work
     * like DELIVER_IN_BACKGROUND_THREAD.
     */
    public static final int DELIVER_IN_DEFAULT_THREAD = 0;

    /**
     * Bus will deliver the event in the UI thread
     * .
     * Use this for events that have to update UI.
     * Avoid for events that may execute longer, have to do some processing.
     */
    public static final int DELIVER_IN_UI_THREAD = 1;

    /**
     * Bus will deliver the event in a background thread, using either internal or external Executor.
     * <p/>
     * Use this for events that have to do processing, and does not access UI.
     */
    public static final int DELIVER_IN_BACKGROUND_THREAD = 2;

    /**
     * Bus will deliver the event in the dispatcher thread, the thread that is used for event dispatching.
     * <p/>
     * This is the most efficient delivery mode, but have drawback of blocking the delivery thread.
     * In case of EventBus.sendXXX() methods, where the dispatch is executed in sender thread, the
     * event handler will be executed in this thread, before sendXXX() returns.
     * <p/>
     * Use it only for very short handlers, for example to update few variables, or to generate other
     * events, or to start some async job, or to start the network call (that have its own thread).
     */
    public static final int DELIVER_IN_DISPATCHER_THREAD = 3;

    @IntDef({DELIVER_IN_DEFAULT_THREAD, DELIVER_IN_UI_THREAD, DELIVER_IN_BACKGROUND_THREAD, DELIVER_IN_DISPATCHER_THREAD})
    @Retention(RetentionPolicy.SOURCE)
    public @interface DeliveryThread {
    }


    /**
     * Creates a new EventBus named "default".
     * Will use default background threads executor, will share threads and pools with other instances
     */
    public EventBus() {
        this("default", null);
    }

    /**
     * Creates a new EventBus with the given {@code identifier} and Executor.
     *
     * @param identifier a brief identifier for this bus, for debugging purposes.
     * @param executor   executor to manage background threads. Pass null to use internal one.
     */
    public EventBus(@NonNull String identifier, @Nullable Executor executor) {
        mIdentifier = identifier;
        mDispatcherThread = createDispatcherThread();
        mBackgroundExecutor = executor != null ? executor : getSharedExecutor();
    }


    ////////////////////////////////////////////////////////////////////////////////////////////////
    // API

    /**
     * Call to register all event handlers for subscriber.
     */
    public void register(@NonNull Object subscriber) {
        assignThreadForSubscriber(subscriber);

        //Key: the event class to handle
        //Value: set of event handlers that can handle this event class.
        for (Map.Entry<Class<?>, List<EventHandler>> entry : Cache.findAllEventHandlersForSubscriber(subscriber).entrySet()) {
            Class<?> eventType = entry.getKey();
            CopyOnWriteArraySet<EventHandler> registeredEventHandlersForEventType = mRegisteredEventHandlersByEventType.get(eventType);
            //If there is no Set of EventHandlers for this type of event, we have to create one. But it must be safe, no race condition, thus synchronized().
            if (registeredEventHandlersForEventType == null) {
                synchronized (mRegisteredEventHandlersByEventType) {
                    //check again, maybe other thread managed to add the Set already, while this one waied for the synchronization
                    registeredEventHandlersForEventType = mRegisteredEventHandlersByEventType.get(eventType);
                    if (registeredEventHandlersForEventType == null) {
                        registeredEventHandlersForEventType = new CopyOnWriteArraySet<>();
                        mRegisteredEventHandlersByEventType.put(eventType, registeredEventHandlersForEventType);
                    }
                }
            }
            //finally, add new event handlers to the registered handlers
            registeredEventHandlersForEventType.addAll(entry.getValue());

            Object stickyEvent = Cache.stickyEvents.get(entry.getKey());
            if (stickyEvent != null) {
                sendTo(stickyEvent, subscriber);
            }
        }
    }

    /**
     * Unregister all event handler methods for a subscriber.
     * By the way this method also removes all subscribers that was already garbage collected.
     *
     * @param subscriber a @Nullable object whose event handlers methods should be unregistered.
     *                   Pass null to remove old, already garbage collected objects.
     */
    public void unregister(@Nullable Object subscriber) {
        List<EventHandler> eventHandlersToRemove = new ArrayList<>();
        //NOTE: mRegisteredEventHandlersByEventType is a concurrent map, reads are permitted without synchronisation.
        //this method does not modify the mRegisteredEventHandlersByEventType, it modify only its values, sets of eventHandlers.
        for (CopyOnWriteArraySet<EventHandler> eventHandlers : mRegisteredEventHandlersByEventType.values()) {
            for (EventHandler eventHandler : eventHandlers) {
                Object eventHandlerSubscriber = eventHandler.mSubscriber.get();
                //Note: if the eventHandlerSubscriber is null, it means that object was GCed,
                //so it should be unregistered too.
                if (eventHandlerSubscriber == null || eventHandlerSubscriber == subscriber) {
                    //Note: the eventHandlers is a CopyOnWriteArraySet, it is much better performance-wise
                    //to remove all handlers in one steep, at the end of loop.
                    eventHandlersToRemove.add(eventHandler);
                }
            }
            eventHandlers.removeAll(eventHandlersToRemove);
            eventHandlersToRemove.clear();
        }


        //remove related default thread handler
        for (IdentityWeakReferenceKey<Object> key : mSubscribersDefaultThreads.keySet()) {
            Object ref = key.get();
            //Note: if the ref is null, it means that object was GCed,
            //so it should be removed too.
            if (ref == null || ref == subscriber) {
                mSubscribersDefaultThreads.remove(key);
            }
        }
    }

    /**
     * Assign current thread to the subscriber object.
     * To make it work, the thread have to have a Looper (Looper.myLooper() != null).
     * If current thread have no Looper, all event handlers marked with DELIVER_IN_DEFAULT_THREAD,
     * it will be executed as DELIVER_IN_BACKGROUND_THREAD.
     * <p/>
     * This method can be used to re-assign the default thread for already registered object.
     *
     * @param subscriber @NonNull a subscriber object to assign thread for.
     */
    public void assignThreadForSubscriber(@NonNull Object subscriber) {
        Looper looper = Looper.myLooper();

        if (looper != null) {
            //Assumption: this is called always to change thread, so no check for old value.
            mSubscribersDefaultThreads.put(
                    new IdentityWeakReferenceKey<>(subscriber),
                    looper != Looper.getMainLooper() ? new Handler(looper) : mUiThreadHandler
            );
        } else {
            mSubscribersDefaultThreads.remove(new IdentityWeakReferenceKey<>(subscriber));
        }
    }


    /**
     * Posts an event to all registered subscribers.
     * The dispatch code will be executed in the separated dispatch thread, method will return immediately.
     * <p/>
     * Generally PostXxx() methods are less performance-friendly than sendXxx() methods, but have
     * advantage od constant, fast eecution time.
     * Use this method in cases when Your thread is already busy, or You can not afford ags, like in the UI thread.
     * <p/>
     * If no subscribers have been subscribed for {@code event}'s class, and {@code event} is not already a
     * {@link DeadEvent}, it will be wrapped in a DeadEvent and reposted.
     *
     * @param event @NonNull event to post.
     * @throws NullPointerException if the event is null.
     */
    public void post(@NonNull final Object event) {
        mDispatcherThread.post(new Dispatcher(this, event, null));
    }

    /**
     * Posts an event to one specific subscriber object.
     * The dispatch code will be executed in the separated dispatch thread, method will return immediately.
     * <p/>
     * Generally PostXxx() methods are less performance-friendly than sendXxx() methods, but have
     * advantage od constant, fast eecution time.
     * Use this method in cases when Your thread is already busy, or You can not afford ags, like in the UI thread.
     * <p/>
     * If no subscribers have been subscribed for {@code event}'s class, and {@code event} is not already a
     * {@link DeadEvent}, it will be wrapped in a DeadEvent and re-posted.
     *
     * @param event      @NonNull event to post.
     * @param subscriber @NonNull subscriber to deliver event to. Subscriber must be registered in the event bus already.
     * @throws NullPointerException if the event is null.
     */
    public void postTo(@NonNull final Object event, @NonNull Object subscriber) {
        mDispatcherThread.post(new Dispatcher(this, event, subscriber));
    }

    /**
     * Posts an event to all registered subscribers, with delay. Subscriber must be registered
     * after requested time passes.
     * The dispatch code will be executed in the separated dispatch thread, method will return immediately.
     * <p/>
     * If no subscribers have been subscribed for {@code event}'s class, and {@code event} is not already a
     * {@link DeadEvent}, it will be wrapped in a DeadEvent and reposted.
     *
     * @param event        @NonNull event to post.
     * @param milliseconds delay in milliseconds
     * @throws NullPointerException if the event is null.
     */
    public void postDelayed(@NonNull final Object event, long milliseconds) {
        mDispatcherThread.postDelayed(new Dispatcher(this, event, null), milliseconds);
    }

    /**
     * Posts an event to one specific subscriber object, with delay. Subscriber must be registered
     * after requested time passes.
     * The dispatch code will be executed in the separated dispatch thread, method will return immediately.
     * <p/>
     * If no subscribers have been subscribed for {@code event}'s class, and {@code event} is not already a
     * {@link DeadEvent}, it will be wrapped in a DeadEvent and re-posted.
     *
     * @param event        @NonNull event to post.
     * @param subscriber   @NonNull subscriber to deliver event to. Subscriber must be registered in the event bus already.
     * @param milliseconds delay in milliseconds
     * @throws NullPointerException if the event is null.
     */
    public void postToDelayed(@NonNull final Object event, @NonNull Object subscriber, long milliseconds) {
        mDispatcherThread.postDelayed(new Dispatcher(this, event, subscriber), milliseconds);
    }

    /**
     * Send an event to all registered subscribers, dispatching it in current thread, before method returns.
     * Note: All subscribers with DELIVER_IN_DISPATCHER_THREAD will also be processed in same thread,
     * before method returns.
     * <p/>
     * Generally sendXxx() methods are more performance-friendly than postXxx() methods.
     * Use this method in cases when you are not concerned about how long the dispatch will take.
     * Avoid using sendXxx() and prefer postXxx() in UI thread, as there is no guarantee how long it may take.
     * <p/>
     * If no subscribers have been subscribed for {@code event}'s class, and {@code event} is not already a
     * {@link DeadEvent}, it will be wrapped in a DeadEvent and reposted.
     *
     * @param event @NonNull event to post.
     * @throws NullPointerException if the event is null.
     */
    public void send(@NonNull final Object event) {
        new Dispatcher(this, event, null).run();
    }

    /**
     * Same as {@link #send(Object)}, but additionally {@code event} will be cached and delivered to
     * every new subscriber immediately after it registers itself in the event bus. Sticky events remain
     * active unless they get removed using {@link #removeStickyEvent(Class)} method.
     * There can only exist one sticky event of given time at a time. If another sticky event of given type is sent,
     * old instance gets replaced by a new one.
     *
     * @param event @NonNull sticky event to send
     */
    public void sendSticky(@NonNull final Object event) {
        Cache.stickyEvents.put(event.getClass(), event);
        send(event);
    }

    /**
     * Removes sticky event, which was previously sent using {@link #sendSticky(Object)} method.
     * Once event is removed from cache, it will no longer be sent to new subscribers on registration.
     *
     * @param eventClass @NonNull class of sticky event to be removed
     */
    public void removeStickyEvent(@NonNull Class<?> eventClass) {
        Cache.stickyEvents.remove(eventClass);
    }

    /**
     * Send an event to all registered subscribers, dispatching it in current thread, before method
     * returns, to one specific subscriber.
     * This method will process all dispatch code in caller thread. It means that Thread.DISPATCHER subscribers
     * will have to finish before control will be returned to caller.
     * <p/>
     * Generally sendXxx() methods are more performance-friendly than postXxx() methods.
     * Use this method in cases when you are not concerned about how long the dispatch will take.
     * Avoid using sendXxx() and prefer postXxx() in UI thread, as there is no guarantee how long it may take.
     * <p/>
     * If no subscribers have been subscribed for {@code event}'s class, and {@code event} is not already a
     * {@link DeadEvent}, it will be wrapped in a DeadEvent and re-posted.
     *
     * @param event      @NonNull event to post.
     * @param subscriber @NonNull subscriber to deliver event to. Subscriber must be registered in the event bus already.
     * @throws NullPointerException if the event is null.
     */
    public void sendTo(@NonNull final Object event, @NonNull Object subscriber) {
        new Dispatcher(this, event, subscriber).run();
    }


    ////////////////////////////////////////////////////////////////////////////////////////////////
    // implementation

    static final String BACKGROUND_THREAD_NAME = "EventBus.Executor #";
    static final String DISPATHER_THREAD_NAME = "EventBus.Dispatcher";


    static ThreadPoolExecutor mSharedExecutor = null;
    static final Handler mUiThreadHandler = new Handler(Looper.getMainLooper());

    @NonNull
    final String mIdentifier;
    @NonNull
    final Handler mDispatcherThread;
    @NonNull
    final Executor mBackgroundExecutor;


    /**
     * All registered subscribers, indexed by event type.
     * Inner Set is a CopyOnWriteArraySet.
     */
    final Map<Class<?>, CopyOnWriteArraySet<EventHandler>> mRegisteredEventHandlersByEventType = new ConcurrentHashMap<>();

    /**
     * A map of android Handler objects that are default thread handlers for the subscribers.
     */
    final Map<IdentityWeakReferenceKey<Object>, Handler> mSubscribersDefaultThreads = new ConcurrentHashMap<>();


    synchronized Executor getSharedExecutor() {
        if (mSharedExecutor == null) {
            int numberOfThreads = Math.max(4, Math.min(16, Runtime.getRuntime().availableProcessors() * 2));
            mSharedExecutor = new ThreadPoolExecutor(
                    numberOfThreads, numberOfThreads,
                    10, TimeUnit.SECONDS,
                    new LinkedBlockingQueue<Runnable>(),
                    new ThreadFactory() {
                        private final AtomicInteger mCount = new AtomicInteger(1);

                        @Override
                        public Thread newThread(@NonNull Runnable r) {
                            Thread thread = new Thread(r, BACKGROUND_THREAD_NAME + mCount.getAndIncrement());
                            thread.setPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);
                            return thread;
                        }
                    });
            mSharedExecutor.allowCoreThreadTimeOut(true);
        }
        return mSharedExecutor;
    }

    Handler createDispatcherThread() {
        HandlerThread thread = new HandlerThread(DISPATHER_THREAD_NAME, android.os.Process.THREAD_PRIORITY_BACKGROUND - 4);
        thread.start();
        return new Handler(thread.getLooper());
    }

    Handler getDefaultThreadForSubscriber(@NonNull Object subscriber) {
        return mSubscribersDefaultThreads.get(new IdentityWeakReferenceKey<>(subscriber));
    }


}