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

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import net.ranides.assira.collection.maps.HashMap;
import net.ranides.assira.trace.LoggerUtils;
import net.ranides.assira.trace.ThreadUtils;
import org.slf4j.Logger;

/**
 * Beznarzutowy EventRouter, nie kolejkuje zdarzeń.
 * Wywołuje procedury obsługi natychmiast po wywołaniu zdarzenia, w wątku, który
 * zasygnalizował event.
 * <p>
 * Użyteczny wszędzie tam, gdzie mamy pewność, że nadawcy i odbiorcy działają
 * w jednym wątku, albo wtedy, gdy rejestrowani obserwatorzy zdarzeń realizują
 * obsługę komunikatów w taki sposób, że mogą ruszyć w dowolnym wątku (bo wykonują
 * operacje thread-safe, albo używają innych metod synchronizacji).
 * </p>
 * @author ranides
 */
public class EventDispatcher implements EventRouter {

    private static final Logger LOGGER = LoggerUtils.getLogger();
    
    private static final Map<String, WeakReference<EventDispatcher>> DISPATCHERS = new HashMap<>();

    private Object[] listeners = null;
    
    private final String name;
    
    public EventDispatcher() {
        this.name = "";
    }

    public EventDispatcher(String name) {
        this.name = name;
        synchronized(DISPATCHERS) {
            DISPATCHERS.put(name, new WeakReference<>(this));
        }
    }
    
    public static EventDispatcher find(String name) {
        WeakReference<EventDispatcher> ref = DISPATCHERS.get(name);
        return null!=ref ? ref.get() : null;
    }

    @Override
    public String name() {
        return name;
    }

    @Override
    public <T extends Event> void addEventListener(Class<T> event, EventListener<? super T> listener) {
        // atomic operation, signal update
        synchronized(this) {
            if(null == listeners) {
                listeners = new Object[]{event, listener};
            } else {
                int size = listeners.length;
                Object[] realloc = new Object[size+2];
                System.arraycopy(listeners, 0, realloc, 0, size);
                realloc[size] = event;
                realloc[size+1] = listener;
                listeners = realloc;
            }
        }
    }

    @Override
    public <T extends Event> void removeEventListener(Class<T> event, EventListener<? super T> listener) {
        // atomic operation, signal update
        synchronized(this) {
            if(null == listeners) {
                return;
            }
            int index = -1;
            for (int i = listeners.length-2; i>=0; i-=2) {
                if ((listeners[i]==event) && listeners[i+1].equals(listener) ) {
                    index = i;
                    break;
                }
            }

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

    @Override
    public void removeAllEventListeners() {
        // atomic operation, signal update
        synchronized(this) {
            listeners = null;
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public Collection<EventBinding<?>> getEventListeners() {
        Object[] array;
        // read updated
        synchronized(this) {
            array = listeners;
        }
        // read update
        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;
    }
    
    @Override
    public int getEventListenersCount() {
        synchronized(this) {
            return null == listeners ? 0 : listeners.length / 2;
        }
    }

    /**
     * Metoda blokująca, uruchamia kolejno obsługę zdarzenia u wszystkich
     * zarejestrowanych obserwatorów, które na zdarzenie o określonym typie oczekują.
     * Domyślna implementacja wywołuje tę metodę bezpośrednio w {@link #signalEvent}.
     * Metody pochodne mogą użyć poniższej metody w zupełnie inny sposób i w
     * zupełnie innym wątku, aby zaimplementować inną strategię przetwarzania zdarzeń.
     *
     * <div class="message-note">thread-safe method</div>
     * @param event
     */
    protected void dispatchEvent(Event event) {
        dispatchEvent(event, false);
    }

    @SuppressWarnings("unchecked")
    protected void dispatchEvent(Event event, boolean direct) {
        Object[] array;
        // read updated
        synchronized(this) {
            array = this.listeners;
        }
        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 {
                    if(direct) {
                        ((EventListener)array[i+1]).handleEvent(event);
                    } else {
                        dispatchEvent((EventListener)array[i+1], 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));
                }
            }
        }
    }

    protected <T extends Event> void dispatchEvent(EventListener<? super T> listener, T event) {
        listener.handleEvent(event);
    }

    /**
     * {@inheritDoc}
     * <p>
     * Metoda czeka na obsłużenie komunikatu przez wszystkich zarejestrowanych obserwatorów.
     * Obsługa komunikatu jest uruchamiana natychmiastowo, w bieżącym wątku
     * (t.j. w tym samym, który zasygnalizował zdarzenie).
     * </p>
     * <p>Interesującym przypadkiem jest przekazanie Dispatcher'owi obserwatora,
     * który jest {@code EventRouterThread}'em. Taki obserwator w bieżącym wątku
     * obsłuży zdarzenie <i>w swoim imieniu</i>, ale ta obsługa, to nic więcej, niż zapis do
     * wewnętrznej kolejki. Dokładniej, zgodnie z dokumentacją:</p>
     *  <ul>
     *      <li>ruszy metoda {@link EventRouterThread#handleEvent}</li>
     *      <li>metoda wydeleguje obsługę do {@link EventRouterThread#signalEvent}</li>
     *      <li>{@code signalEvent} wstawi komunikat do kolejki zdarzeń oczekujących</li>
     *      <li>wstawiony komunikat zostanie przekazany do obserwatorów w zupełnie
     *      innym wątku</li>
     *      <li>w szczególności, czas obsługi jest całkowicie niezdefiniowany.
     *          Może nastąpić przed zakończeniem metody signal z bieżącego wątku.
     *          A może być odwrotnie - nastapić długo po jej zakończeniu</li>
     *  </ul>
     *
     * <div class="message-note">thread-safe method</div>
     * @param event
     */
    @Override
    public boolean signalEvent(Event event) {
		ThreadUtils.dump(LOGGER.isTraceEnabled(), LOGGER::trace);
        dispatchEvent(event);
        return true;
    }

    /**
     * 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
     */
    @Override
    public void handleEvent(Event event) {
        signalEvent(event);
    }

    protected final void dispose(boolean direct) {
        dispatchEvent(Events.dispose(this), direct);
        // signal update
        synchronized(this) {
            this.listeners = null;
            DISPATCHERS.remove(name());
        }
    }

    @Override
    public void dispose() {
        dispose(true);
    }
    
}
