package com.mobify.astro.messaging;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import android.support.annotation.NonNull;
import android.util.Log;

import org.json.JSONObject;

/**
 * A simple Java event manager for handlers that accept JSON data
 */
public class EventManager implements EventRegistrar, EventEmitter {
    private static final String TAG = EventManager.class.getName();

    protected Map<String, List<JSONCallback>> events = new HashMap<>();

    private class EventInfo {
        String name;
        JSONObject data;
        EventInfo(String name, JSONObject data) {
            this.name = name;
            this.data = data;
        }
    }

    private ArrayList<EventInfo> queuedEvents = new ArrayList<>();
    private boolean applicationPaused = false;

    public void onResume() {
        applicationPaused = false;
        for (EventInfo eventInfo : queuedEvents) {
            trigger(eventInfo.name, eventInfo.data);
        }
        queuedEvents.clear();
    }

    public void onPause() {
        applicationPaused = true;
    }

    public void onDestroy() {
        removeAllHandlers();
    }

    /**
     * Adds a new JSONCallback event handler to the event emitter.
     *
     * @param eventName the name of the event to listen for
     * @param jsonCallback a JSONCallback implementer whose run method will be called when the event
     *                     is triggered.
     */
    @Override
    public synchronized void on(@NonNull String eventName, @NonNull JSONCallback jsonCallback) {
        List<JSONCallback> callbackList = events.get(eventName);
        if (callbackList == null) {
            callbackList = new ArrayList<>();
        }
        callbackList.add(jsonCallback);
        events.put(eventName, callbackList);
    }

    /**
     * Takes an event name, returns null, or a clone of the List of callbacks for that event. Used
     * to avoid deadlocks when triggering events
     * @param eventName the name of the event whose callbacks are to be cloned
     * @return a clone of the callback List.
     */
    private synchronized List<JSONCallback> cloneCallbacks(String eventName) {
        List<JSONCallback> callbacks = events.get(eventName);
        if (callbacks == null) {
            return null;
        }
        return new ArrayList<>(callbacks);
    }

    /**
     * Triggers an event on the event emitter.
     *
     * Note: Will also call callbacks bound to all events ("*").
     *
     * @param eventName the name of the event whose handlers will be called
     * @param data JSON data that will be passed to all registered JSONCallback handlers run methods
     * @return returns false if no handlers were called, true if at least one was.
     */
    @Override
    public boolean trigger(@NonNull String eventName, @NonNull JSONObject data) {
        // Work with a clone of the callback list to avoid mutating the original, and remove the
        // need to be synchronized when executing callbacks (which may themselves trigger events,
        // which would be a nasty deadlock otherwise).
        List<JSONCallback> callbacks = cloneCallbacks(eventName);

        if (events.get("*") != null) {
            if (callbacks == null) {
                callbacks = new ArrayList<>();
            }
            callbacks.addAll(events.get("*"));
        }

        if (callbacks == null) {
            return false;
        }

        if (applicationPaused) {
            queuedEvents.add(new EventInfo(eventName, data));
            return true;
        }

        for (JSONCallback jsonCallback : callbacks) {
            // ignore exceptions
            try {
                jsonCallback.run(data);
            } catch (Exception e) {
                // TODO: consider passing exceptions to an error handler
                Log.e(TAG, "Exception thrown during dispatch of event \"" + eventName + "\" from " +
                        "JSONCallback subclass " + jsonCallback.getClass().getCanonicalName() + ":"
                        + e.getMessage(), e);
            }
        }

        return true;
    }

    /**
     * Clear all handlers for a given eventName. Returns true if any handlers were cleared, false
     * otherwise.
     * @param eventName event name to clear
     * @return true if any handlers were cleared, false otherwise
     */
    boolean removeHandlers(String eventName) {
        List<JSONCallback> callbacks = events.remove(eventName);
        if (callbacks != null) {
            return true;
        }
        return false;
    }

    /**
     * Removes all event handlers.
     */
    void removeAllHandlers() {
        events.clear();
    }
}
