/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.googlecode.android_scripting.facade;

import java.util.HashMap;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;

import org.json.JSONException;

import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.googlecode.android_scripting.Log;
import com.googlecode.android_scripting.event.Event;
import com.googlecode.android_scripting.event.EventObserver;
import com.googlecode.android_scripting.event.EventServer;
import com.googlecode.android_scripting.future.FutureResult;
import com.googlecode.android_scripting.jsonrpc.JsonBuilder;
import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
import com.googlecode.android_scripting.rpc.Rpc;
import com.googlecode.android_scripting.rpc.RpcDefault;
import com.googlecode.android_scripting.rpc.RpcDeprecated;
import com.googlecode.android_scripting.rpc.RpcName;
import com.googlecode.android_scripting.rpc.RpcOptional;
import com.googlecode.android_scripting.rpc.RpcParameter;

/**
 * Manage the event queue. <br>
 * <br>
 * <b>Usage Notes:</b><br>
 * EventFacade APIs interact with the Event Queue (a data buffer containing up to 1024 event
 * entries).<br>
 * Events are automatically entered into the Event Queue following API calls such as startSensing()
 * and startLocating().<br>
 * The Event Facade provides control over how events are entered into (and removed from) the Event
 * Queue.<br>
 * The Event Queue provides a useful means of recording background events (such as sensor data) when
 * the phone is busy with foreground activities.
 *
 */
public class EventFacade extends RpcReceiver {
    /**
     * The maximum length of the event queue. Old events will be discarded when this limit is
     * exceeded.
     */
    private static final int MAX_QUEUE_SIZE = 1024;
    private final Queue<Event> mEventQueue = new ConcurrentLinkedQueue<Event>();
    private final CopyOnWriteArrayList<EventObserver> mGlobalEventObservers =
            new CopyOnWriteArrayList<EventObserver>();
    private final Multimap<String, EventObserver> mNamedEventObservers = Multimaps
            .synchronizedListMultimap(ArrayListMultimap.<String, EventObserver> create());
    private EventServer mEventServer = null;
    private final HashMap<String, BroadcastListener> mBroadcastListeners =
            new HashMap<String, BroadcastListener>();
    private final Context mContext;
    private boolean bEventServerRunning;

    public EventFacade(FacadeManager manager) {
        super(manager);
        mContext = manager.getService().getApplicationContext();
        Log.v("Creating new EventFacade Instance()");
        bEventServerRunning = false;
    }

    /**
     * Example (python): droid.eventClearBuffer()
     */
    @Rpc(description = "Clears all events from the event buffer.")
    public void eventClearBuffer() {
        mEventQueue.clear();
    }

    /**
     * Registers a listener for a new broadcast signal
     */
    @Rpc(description = "Registers a listener for a new broadcast signal")
    public boolean eventRegisterForBroadcast(
            @RpcParameter(name = "category") String category,
            @RpcParameter(name = "enqueue",
                    description = "Should this events be added to the event queue or only dispatched") @RpcDefault(value = "true") Boolean enqueue) {
        if (mBroadcastListeners.containsKey(category)) {
            return false;
        }

        BroadcastListener b = new BroadcastListener(this, enqueue.booleanValue());
        IntentFilter c = new IntentFilter(category);
        mContext.registerReceiver(b, c);
        mBroadcastListeners.put(category, b);

        return true;
    }

    @Rpc(description = "Stop listening for a broadcast signal")
    public void eventUnregisterForBroadcast(
            @RpcParameter(name = "category") String category) {
        if (!mBroadcastListeners.containsKey(category)) {
            return;
        }

        mContext.unregisterReceiver(mBroadcastListeners.get(category));
        mBroadcastListeners.remove(category);
    }

    @Rpc(description = "Lists all the broadcast signals we are listening for")
    public Set<String> eventGetBrodcastCategories() {
        return mBroadcastListeners.keySet();
    }

    /**
     * Actual data returned in the map will depend on the type of event.
     *
     * <pre>
     * Example (python):
     *     import android, time
     *     droid = android.Android()
     *     droid.startSensing()
     *     time.sleep(1)
     *     droid.eventClearBuffer()
     *     time.sleep(1)
     *     e = eventPoll(1).result
     *     event_entry_number = 0
     *     x = e[event_entry_ number]['data']['xforce']
     * </pre>
     *
     * e has the format:<br>
     * [{u'data': {u'accuracy': 0, u'pitch': -0.48766891956329345, u'xmag': -5.6875, u'azimuth':
     * 0.3312483489513397, u'zforce': 8.3492730000000002, u'yforce': 4.5628165999999997, u'time':
     * 1297072704.813, u'ymag': -11.125, u'zmag': -42.375, u'roll': -0.059393649548292161,
     * u'xforce': 0.42223078000000003}, u'name': u'sensors', u'time': 1297072704813000L}]<br>
     * x has the string value of the x force data (0.42223078000000003) at the time of the event
     * entry. </pre>
     */

    @Rpc(description = "Returns and removes the oldest n events (i.e. location or sensor update, etc.) from the event buffer.",
            returns = "A List of Maps of event properties.")
    public List<Event> eventPoll(
            @RpcParameter(name = "number_of_events") @RpcDefault("1") Integer number_of_events) {
        List<Event> events = Lists.newArrayList();
        for (int i = 0; i < number_of_events; i++) {
            Event event = mEventQueue.poll();
            if (event == null) {
                break;
            }
            events.add(event);
        }
        return events;
    }

    @Rpc(description = "Blocks until an event with the supplied name occurs. Event is removed from the buffer if removeEvent is True.",
            returns = "Map of event properties.")
    public Event eventWaitFor(
            @RpcParameter(name = "eventName")
            final String eventName,
            @RpcParameter(name = "removeEvent")
            final Boolean removeEvent,
            @RpcParameter(name = "timeout", description = "the maximum time to wait (in ms)") @RpcOptional Integer timeout)
            throws InterruptedException {
        Event result = null;
        final FutureResult<Event> futureEvent;
        synchronized (mEventQueue) { // First check to make sure it isn't already there
            for (Event event : mEventQueue) {
                if (event.getName().equals(eventName)) {
                    result = event;
                    if (removeEvent)
                        mEventQueue.remove(event);
                    return result;
                }
            }
            futureEvent = new FutureResult<Event>();
            addNamedEventObserver(eventName, new EventObserver() {
                @Override
                public void onEventReceived(Event event) {
                    if (event.getName().equals(eventName)) {
                        synchronized (futureEvent) {
                            if (!futureEvent.isDone()) {
                                futureEvent.set(event);
                                // TODO: Remove log.
                                Log.v(String.format("Removing observer (%s) got event  (%s)",
                                        this,
                                        event));
                                removeEventObserver(this);
                            }
                            if (removeEvent)
                                mEventQueue.remove(event);
                        }
                    }
                }
            });
        }
        if (futureEvent != null) {
            if (timeout != null) {
                result = futureEvent.get(timeout, TimeUnit.MILLISECONDS);
            } else {
                result = futureEvent.get();
            }
        }
        return result;
    }

    @Rpc(description = "Blocks until an event occurs. The returned event is removed from the buffer.",
            returns = "Map of event properties.")
    public Event eventWait(
            @RpcParameter(name = "timeout", description = "the maximum time to wait") @RpcOptional Integer timeout)
            throws InterruptedException {
        Event result = null;
        final FutureResult<Event> futureEvent = new FutureResult<Event>();
        EventObserver observer;
        synchronized (mEventQueue) { // Anything in queue?
            if (mEventQueue.size() > 0) {
                return mEventQueue.poll(); // return it.
            }
            observer = new EventObserver() {
                @Override
                public void onEventReceived(Event event) { // set up observer for any events.
                    synchronized (futureEvent) {
                        if (!futureEvent.isDone()) {
                            futureEvent.set(event);
                            // TODO: Remove log.
                            Log.v(String.format("onEventReceived for event (%s)", event));
                        }
                    }
                }
            };
            addGlobalEventObserver(observer);
        }
        if (timeout != null) {
            result = futureEvent.get(timeout, TimeUnit.MILLISECONDS);
        } else {
            result = futureEvent.get();
        }
        if (result != null) {
            mEventQueue.remove(result);
        }
        // TODO: Remove log.
        Log.v(String.format("Removing observer (%s) got event  (%s)", observer, result));
        if (observer != null) {
            removeEventObserver(observer); // Make quite sure this goes away.
        }
        return result;
    }

    /**
     * <pre>
     * Example:
     *   import android
     *   from datetime import datetime
     *   droid = android.Android()
     *   t = datetime.now()
     *   droid.eventPost('Some Event', t)
     * </pre>
     */
    @Rpc(description = "Post an event to the event queue.")
    public void eventPost(
            @RpcParameter(name = "name", description = "Name of event") String name,
            @RpcParameter(name = "data", description = "Data contained in event.") String data,
            @RpcParameter(name = "enqueue",
                    description = "Set to False if you don't want your events to be added to the event queue, just dispatched.") @RpcOptional @RpcDefault("false") Boolean enqueue) {
        postEvent(name, data, enqueue.booleanValue());
    }

    /**
     * Post an event and queue it
     */
    public void postEvent(String name, Object data) {
        postEvent(name, data, true);
    }

    /**
     * Posts an event with to the event queue.
     */
    public void postEvent(String name, Object data, boolean enqueue) {
        Event event = new Event(name, data);
        if (enqueue != false) {
            synchronized (mEventQueue) {
                while (mEventQueue.size() >= MAX_QUEUE_SIZE) {
                    mEventQueue.remove();
                }
                mEventQueue.add(event);
            }
            Log.v(String.format("postEvent(%s)", name));
        }
        synchronized (mNamedEventObservers) {
            for (EventObserver observer : mNamedEventObservers.get(name)) {
                observer.onEventReceived(event);
            }
        }
        synchronized (mGlobalEventObservers) {
            // TODO: Remove log.
            Log.v(String.format("mGlobalEventObservers size (%s)", mGlobalEventObservers.size()));
            for (EventObserver observer : mGlobalEventObservers) {
                observer.onEventReceived(event);
            }
        }
    }

    @RpcDeprecated(value = "eventPost", release = "r4")
    @Rpc(description = "Post an event to the event queue.")
    @RpcName(name = "postEvent")
    public void rpcPostEvent(
            @RpcParameter(name = "name") String name,
            @RpcParameter(name = "data") String data) {
        postEvent(name, data);
    }

    @RpcDeprecated(value = "eventPoll", release = "r4")
    @Rpc(description = "Returns and removes the oldest event (i.e. location or sensor update, etc.) from the event buffer.",
            returns = "Map of event properties.")
    public Event receiveEvent() {
        return mEventQueue.poll();
    }

    @RpcDeprecated(value = "eventWaitFor", release = "r4")
    @Rpc(description = "Blocks until an event with the supplied name occurs. Event is removed from the buffer if removeEvent is True.",
            returns = "Map of event properties.")
    public Event waitForEvent(
            @RpcParameter(name = "eventName")
            final String eventName,
            @RpcOptional
            final Boolean removeEvent,
            @RpcParameter(name = "timeout", description = "the maximum time to wait") @RpcOptional Integer timeout)
            throws InterruptedException {
        return eventWaitFor(eventName, removeEvent, timeout);
    }

    @Rpc(description = "Opens up a socket where you can read for events posted")
    public int startEventDispatcher(
            @RpcParameter(name = "port", description = "Port to use") @RpcDefault("0") @RpcOptional() Integer port) {
        if (mEventServer == null) {
            if (port == null) {
                port = 0;
            }
            mEventServer = new EventServer(port);
            addGlobalEventObserver(mEventServer);
            bEventServerRunning = true;
        }
        return mEventServer.getAddress().getPort();
    }

    @Rpc(description = "sl4a session is shutting down, send terminate event to client.")
    public void closeSl4aSession() {
        eventClearBuffer();
        postEvent("EventDispatcherShutdown", null);
    }

    @Rpc(description = "Stops the event server, you can't read in the port anymore")
    public void stopEventDispatcher() throws RuntimeException {
        if (bEventServerRunning == true) {
            if (mEventServer == null) {
                throw new RuntimeException("Not running");
            }
            bEventServerRunning = false;
            mEventServer.shutdown();
            Log.v(String.format("stopEventDispatcher   (%s)", mEventServer));
            removeEventObserver(mEventServer);
            mEventServer = null;
        }
        return;
    }

    @Override
    public void shutdown() {

        try {
            stopEventDispatcher();
        } catch (Exception e) {
            Log.e("Exception tearing down event dispatcher", e);
        }
        mGlobalEventObservers.clear();
        mEventQueue.clear();
    }

    public void addNamedEventObserver(String eventName, EventObserver observer) {
        mNamedEventObservers.put(eventName, observer);
    }

    public void addGlobalEventObserver(EventObserver observer) {
        mGlobalEventObservers.add(observer);
    }

    public void removeEventObserver(EventObserver observer) {
        mNamedEventObservers.removeAll(observer);
        mGlobalEventObservers.remove(observer);
    }

    public class BroadcastListener extends android.content.BroadcastReceiver {
        private EventFacade mParent;
        private boolean mEnQueue;

        public BroadcastListener(EventFacade parent, boolean enqueue) {
            mParent = parent;
            mEnQueue = enqueue;
        }

        @Override
        public void onReceive(Context context, Intent intent) {
            Bundle data;
            if (intent.getExtras() != null) {
                data = (Bundle) intent.getExtras().clone();
            } else {
                data = new Bundle();
            }
            data.putString("action", intent.getAction());
            try {
                mParent.eventPost("sl4a", JsonBuilder.build(data).toString(), mEnQueue);
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }

    }
}