/*
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.events;

import net.ranides.assira.collection.maps.OpenMap;
import net.ranides.assira.generic.Ref;
import net.ranides.assira.text.StringTraits;
import net.ranides.assira.trace.LoggerUtils;
import org.slf4j.Logger;

import java.lang.ref.WeakReference;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.function.Supplier;

/**
 * EventRouter is used to propagate events into listeners.
 *
 * Please note, that EventRouter extends EventListener, so it is capable to emit events
 * on their own (using signalEvent), or to forward events emitted by other routers.
 *
 * {@link EventListener#handleEvent} inherited from EventListener does not give you a way
 * to check if event was accepted. {@link #signalEvent} returns true, if event was scheduled
 * to route.
 *
 * Event routers could implement many strategies for event propagation. The simplest one:
 * {@link EventDispatcher} executes everything in current thread. {@link EventReactor} uses
 * message que, and {@link EventProactor} uses thread pool.
 *
 * @author ranides
 */
public abstract class EventRouter implements EventListener<Event> {

    private static final Logger LOGGER = LoggerUtils.getLogger();

    private static final Map<String, WeakReference<EventRouter>> DISPATCHERS = new OpenMap<>();

    private final AtomicReference<Object[]> listeners = new AtomicReference<>(null);

    private final String name;

    /**
     * Creates anonymous router.
     * Anonymous dispatchers can't be find by using {@link EventDispatcher#find(String)}.
     *
     * Subclasses should use this constructor only inside protected or private constructors.
     * Although anonymous routers could be created directly, it is preferred not to do so.
     */
    protected EventRouter() {
        this.name = "";
    }

    /**
     * Creates new dispatcher with provided name, using factory method.
     *
     * Registers provided dispatcher if it is not anonymous.
     * Registered named dispatchers can be find by using {@link EventDispatcher#find(String)}.
     *
     * If there was already dispatcher with specified name,
     * it won't create new one but it will throw IllegalArgumentException.
     *
     * WARNING:
     * all dispatchers MUST BE constructed using this method.
     * Especially subclasses MUST NOT construct objects without using it.
     * This is the only method in whole codebase, which is allowed to create dispatchers.
     *
     * @param name name
     * @param factory factory
     * @param <R> dispatcher type
     * @return dispatcher
     */
    protected static <R extends EventRouter> R newInstance(String name, Supplier<R> factory) {
        if(StringTraits.isEmpty(name)) {
            return factory.get();
        }
        synchronized(DISPATCHERS) {
            WeakReference<EventRouter> ref = DISPATCHERS.get(name);
            if (ref != null && ref.get() != null) {
                throw new IllegalArgumentException("EventDispatcher with name " + name + " already exists");
            }
            R that = factory.get();
            DISPATCHERS.put(name, new WeakReference<>(that));
            return that;
        }
    }

    /**
     * Finds named EventDispatcher.
     * If there is no dispatcher with specified name, it returns none.
     *
     * @param name name
     * @return optional
     */
    public static Ref<EventRouter> find(String name) {
        synchronized (DISPATCHERS) {
            WeakReference<EventRouter> ref = DISPATCHERS.get(name);
            if(ref != null) {
                return Ref.ofNullable(ref.get());
            }
            return Ref.empty();
        }
    }

    /**
     * Creates named router.
     * Subclasses should use this constructor only inside protected or private constructors.
     *
     * Correctly created dispatcher is always created by static method {@code newInstance(String, Supplier)}.
     *
     * @param name name
     */
    protected EventRouter(String name) {
        this.name = name;
    }

    /**
     * Sends provided event to all listeners.
     * Returns false if router was unable to schedule event processing and discared it.
     *
     * Returns true if event was scheduled for processing.
     * It does not mean, that event was successfully processed by listeners.
     *
     * @param event event
     * @return boolean
     */
    public abstract boolean signalEvent(Event event);

    /**
     * Stops this event router and release all resources used by it.
     * It sends {@link Events.Dispose} event to all listeners.
     *
     * <div class="message-note">thread-safe method</div>
     */
    public abstract void dispose();

    /**
     * Event router can have a name, usable for debug, logging and dependency management.
     * Named event routers can be resolved using {@link EventDispatcher#find(String)}.
     *
     * If you create your own EventRouter implementation, preferred way is to extend {@link EventDispatcher}.
     * Even if you decidate to write your own class from scrach, you should remember to create
     * new instances of your class using {@link EventDispatcher#newInstance(String, Supplier)}.
     *
     * @return String
     */
    public final String name() {
        return name;
    }

    /**
     * Registers listener which will receive all events of specified types (and descendant types).
     *
     * <div class="message-note">thread-safe method</div>
     *
     * @param event event
     * @param listener listener
     * @param <T> T
     */
    public final <T extends Event> void addEventListener(Class<T> event, EventListener<? super T> listener) {
        // atomic write operation
        synchronized(this) {
            Object[] array = listeners.get();
            if(null == array) {
                listeners.set(new Object[]{event, listener});
            } else {
                int size = array.length;
                Object[] realloc = new Object[size+2];
                System.arraycopy(array, 0, realloc, 0, size);
                realloc[size] = event;
                realloc[size+1] = listener;
                listeners.set(realloc);
            }
        }
    }

    /**
     * Removes listener, which was previously registered for specified type.
     * If listener was registered multiple time for different types,
     * those other subscriptions won't be removed.
     *
     * <div class="message-note">thread-safe method</div>
     *
     * @param event event
     * @param listener listener
     * @param <T> T
     */
    public final <T extends Event> void removeEventListener(Class<T> event, EventListener<? super T> listener) {
        // atomic write operation
        synchronized(this) {
            Object[] array = listeners.get();
            if(null == array) {
                return;
            }
            int index = -1;
            for (int i = array.length-2; i>=0; i-=2) {
                if ((array[i]==event) && array[i+1].equals(listener) ) {
                    index = i;
                    break;
                }
            }

            if (index != -1) {
                if(array.length==2) {
                    listeners.set(null);
                } else {
                    int size = array.length-2;
                    Object[] realloc = new Object[size];
                    System.arraycopy(array, 0, realloc, 0, index);
                    if (index < size) {
                        System.arraycopy(array, index+2, realloc, index, size-index);
                    }
                    listeners.set(realloc);
                }
            }
        }
    }

    /**
     * Removes all listeners, which were previously registered for specified type.
     *
     * <div class="message-note">thread-safe method</div>
     *
     * @param event event
     * @param <T> T
     */
    public final <T extends Event> void removeEventListener(Class<T> event) {
        // atomic write operation
        synchronized(this) {
            Object[] array = listeners.get();
            if(null == array) {
                return;
            }
            int found = 0;
            for (int i = 0; i <= array.length; i += 2) {
                if (array[i]==event) {
                    found++;
                }
            }
            if(found == array.length/2) {
                listeners.set(null);
            } else {
                Object[] realloc = new Object[array.length - 2*found];

                int index = 0;
                for (int i = 0; i <= array.length; i += 2) {
                    if (array[i]!=event) {
                        realloc[index] = array[i];
                        realloc[index+1] = array[i+1];
                        index++;
                    }
                }
                listeners.set(realloc);
            }
        }
    }

    /**
     * Removes all registered listeners.
     *
     * <div class="message-note">thread-safe method</div>
     */
    public final void removeAllEventListeners() {
        listeners.set(null);
    }

    /**
     * Returns collection with all registered bindings.
     * Binding is defined by listener and type of event which it listens to.
     *
     * Returned collection is not a view, but snapshot of current state of router.
     *
     * @return collection
     */
    @SuppressWarnings("unchecked")
    public final Collection<EventBinding<?>> getEventListeners() {
        Object[] array = listeners.get();

        if (null == array) {
            return Collections.emptyList();
        }
        List<EventBinding<?>> list = new ArrayList<>(array.length / 2);
        for (int i = 0, n = array.length; i < n; i += 2) {
            list.add(new EventBinding.Immutable<>((Class) array[i], (EventListener) array[i + 1]));
        }
        return list;
    }

    /**
     * Returns number of registered event listeners.
     *
     * @return int
     */
    public final int getEventListenersCount() {
        Object[] array = listeners.get();
        return null == array ? 0 : array.length / 2;
    }

    /**
     * Rozsyła otrzymany komunikat do wszystkich obserwatorów, delegując
     * obsługę zdarzenia do metody {@link #signalEvent}
     * <div class="message-note">thread-safe method</div>
     * @param event event
     */
    @Override
    public final void handleEvent(Event event) {
        signalEvent(event);
    }

    protected final void executeDisposeNow() {
        executeListenersNow(Events.dispose(this));
        listeners.set(null);
        synchronized (DISPATCHERS) {
            DISPATCHERS.remove(name());
        }
    }

    protected final void executeListenersNow(Event event) {
        executeListeners(event, EventListener::handleEvent);
    }

    protected final void executeListeners(Event event, BiConsumer<EventListener<Event>, Event> executor) {
        Object[] array = listeners.get();
        if(array == null) {
            return;
        }
        Class<?> clazz = event.getClass();
        for(int i=0, n=array.length; i<n; i+=2) {
            if( ((Class)array[i]).isAssignableFrom(clazz) ) {
                try {
                    EventListener listener = (EventListener) array[i + 1];
                    executor.accept(listener, event);
                } catch(Exception ex) {
                    String message = String.format("Exception thrown from event listener. Thread: %s. Listener: %s. Event: %s",
                        Thread.currentThread().getName(),
                        array[i+1], event
                    );
                    LOGGER.debug(message, ex);
                    signalEvent(Events.failure(ex));
                }
            }
        }
    }
}
