package org.hcjf.events;

import org.hcjf.cloud.Cloud;
import org.hcjf.errors.Errors;
import org.hcjf.errors.HCJFRuntimeException;
import org.hcjf.log.Log;
import org.hcjf.properties.SystemProperties;
import org.hcjf.service.Service;
import org.hcjf.service.ServiceSession;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

/**
 * This class implements the event service for the instance.
 * @author javaito
 */
public final class Events extends Service<EventListener> {

    private static final Events instance;

    static {
        instance = new Events();
    }

    public final List<EventListener> listeners;

    private Events() {
        super(SystemProperties.get(SystemProperties.Event.SERVICE_NAME),
                SystemProperties.getInteger(SystemProperties.Event.SERVICE_PRIORITY));
        listeners = new ArrayList<>();
    }

    /**
     * Register a new event listener to the service.
     * @param consumer Event listener.
     */
    @Override
    public void registerConsumer(EventListener consumer) {
        if(consumer == null) {
            throw new IllegalArgumentException(Errors.getMessage(Errors.ORG_HCJF_EVENTS_1));
        }

        synchronized (listeners) {
            listeners.add(consumer);
        }
    }

    /**
     * Unregister a event listener to the service.
     * @param consumer Event listener.
     */
    @Override
    public void unregisterConsumer(EventListener consumer) {
        if(consumer == null) {
            throw new IllegalArgumentException(Errors.getMessage(Errors.ORG_HCJF_EVENTS_1));
        }

        synchronized (listeners) {
            listeners.remove(consumer);
        }
    }

    /**
     * Return all the listeners to be able of process the specific event.
     * @param event Event to dispatch.
     * @return List of listeners.
     */
    private List<EventListener> getListeners(Event event) {
        List<EventListener> result = new ArrayList<>();
        synchronized (listeners) {
            for(EventListener listener : listeners) {
                if(listener.getEventType().isAssignableFrom(event.getClass())) {
                    result.add(listener);
                }
            }
        }
        return result;
    }

    /**
     * Dispatch the event to all the listeners.
     * @param event Event to dispatch.
     */
    private void dispatchEvent(Event event) {
        if(event != null) {
            if(event instanceof DistributedEvent) {
                dispatchDistributedEvent((DistributedEvent) event);
            } else if(event instanceof RemoteEvent) {
                dispatchLocalEvent(((RemoteEvent)event).getEvent());
            } else {
                dispatchLocalEvent(event);
            }
        }
    }

    private void dispatchLocalEvent(Event event) {
        for (EventListener listener : getListeners(event)) {
            try {
                run(() -> listener.onEventReceived(event), ServiceSession.getCurrentIdentity());
            } catch(Exception ex){
                Log.e(SystemProperties.get(SystemProperties.Event.LOG_TAG), "Unable to dispatch event", ex);
            }
        }
    }

    /**
     * Dispatch the event to all the listeners and wait that the all listeners finish.
     * @param event Event to dispatch.
     */
    private void synchronousDispatchEvent(Event event) {
        if(event != null) {
            if(event instanceof DistributedEvent) {
                throw new HCJFRuntimeException("Unable to use synchronous dispatch for distributed events");
            } else if(event instanceof RemoteEvent) {
                synchronousDispatchLocalEvent(((RemoteEvent)event).getEvent());
            } else {
                synchronousDispatchLocalEvent(event);
            }
        }
    }

    /**
     * Dispatch the event to all the listeners and wait that the all listeners finish.
     * @param event Event to dispatch.
     */
    private void synchronousDispatchLocalEvent(Event event) {
        for (EventListener listener : getListeners(event)) {
            try {
                listener.onEventReceived(event);
            } catch(Exception ex){
                Log.e(SystemProperties.get(SystemProperties.Event.LOG_TAG), "Unable to dispatch event", ex);
            }
        }
    }

    /**
     * Dispatch a distributed event instance only for the cloud singleton.
     * @param distributedEvent Distributed event.
     */
    private void dispatchDistributedEvent(DistributedEvent distributedEvent) {
        Cloud.dispatchEvent(distributedEvent);
    }

    /**
     * Dispatch the event and return control immediately
     * @param event Event to dispatch.
     */
    public static void sendEvent(Event event) {
        instance.dispatchEvent(event);
    }

    /**
     * Dispatch the event and wait all the listeners process the event.
     * @param event Event to dispatch
     */
    public static void processEvent(Event event) {
        instance.synchronousDispatchEvent(event);
    }

    /**
     * Add event listener.
     * @param eventListener Event listener.
     */
    public static void addEventListener(EventListener eventListener) {
        instance.registerConsumer(eventListener);
    }

    /**
     * Remove event listener.
     * @param eventListener Event listener.
     */
    public static void removeEventListener(EventListener eventListener) {
        instance.unregisterConsumer(eventListener);
    }

    public static <E extends Event> E waitForEvent(Class<E> eventClass) throws InterruptedException {
        return waitForEvent(eventClass, Long.MAX_VALUE);
    }

    public static <E extends Event> E waitForEvent(Class<E> eventClass, long timeout) throws InterruptedException {
        AtomicReference<E> result = new AtomicReference<>();
        addEventListener(new EventListener<E>(){
            @Override
            public void onEventReceived(E event) {
                result.set(event);
                synchronized (result) {
                    result.notifyAll();
                }
            }
            @Override
            public Class<E> getEventType() {
                return eventClass;
            }
        });
        synchronized (result) {
            result.wait(timeout);
        }
        return result.get();
    }

    public static <O extends Object, E extends Event> O waitForEvent(Class<E> eventClass, EventCollector<E> eventCollector) throws InterruptedException {
        return waitForEvent(eventClass, eventCollector, Long.MAX_VALUE);
    }

    public static <O extends Object, E extends Event> O waitForEvent(Class<E> eventClass, EventCollector<E> eventCollector, long timeout) throws InterruptedException {
        return eventCollector.collect(waitForEvent(eventClass, timeout));
    }

    public interface EventCollector<E extends Event> {

        <O extends Object> O collect(E event);

    }
}