package com.openxc.sinks;

import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import android.util.Log;

import com.openxc.messages.VehicleMessage;

/**
 * Functionality to notify multiple clients asynchronously of new measurements.
 *
 * This class encapsulates the functionality to keep a thread-safe list of
 * listeners that want to be notified of updates asynchronously. Subclasses need
 * only to implement the {@link #propagateMessage(VehicleMessage)}
 * to add the actual logic for looping over the list of receivers and send them
 * new values.
 *
 * New measurements are queued up and propagated to receivers in a separate
 * thread, to avoid blocking the original sender of the data.
 */
public abstract class AbstractQueuedCallbackSink implements VehicleDataSink {
    private final static String TAG = "AbstractQueuedCallbackSink";

    private NotificationThread mNotificationThread = new NotificationThread();
    private Lock mNotificationsLock = new ReentrantLock();
    private Condition mNotificationsChanged = mNotificationsLock.newCondition();
    private CopyOnWriteArrayList<VehicleMessage> mNotifications =
            new CopyOnWriteArrayList<>();

    public AbstractQueuedCallbackSink() {
        mNotificationThread.start();
    }

    @Override
    public synchronized void stop() {
        mNotificationThread.done();
    }

    @Override
    public void receive(VehicleMessage message) throws DataSinkException {
        try {
            mNotificationsLock.lock();
            mNotifications.add(message);
            mNotificationsChanged.signal();
        } finally {
            mNotificationsLock.unlock();
        }
    }

    /* Block until the notifications queue is cleared.
     */
    public void clearQueue() {
        try {
            mNotificationsLock.lock();
            while(!mNotifications.isEmpty()) {
                mNotificationsChanged.await();
            }
        } catch(InterruptedException e) {
            Log.w(TAG, "Interrupted...");
            e.printStackTrace();
            Thread.currentThread().interrupt();
        } finally {
            mNotificationsLock.unlock();
        }
    }

    abstract protected void propagateMessage(VehicleMessage message);

    private class NotificationThread extends Thread {
        private boolean mRunning = true;

        private synchronized boolean isRunning() {
            return mRunning;
        }

        public synchronized void done() {
            Log.d(TAG, "Stopping message notifier");
            mRunning = false;
            // A context switch right can cause a race condition if we
            // used take() instead of poll(): when mRunning is set to
            // false and interrupt is called but we haven't called
            // take() yet, so nobody is waiting. By using poll we can not be
            // locked for more than 1s.
            interrupt();
        }

        @Override
        public void run() {
            Log.d(TAG, "Starting notification thread");
            while(isRunning()) {
                mNotificationsLock.lock();
                try {
                    if(mNotifications.isEmpty()) {
                        mNotificationsChanged.await();
                    }

                    Iterator<VehicleMessage> it = mNotifications.iterator();
                    CopyOnWriteArrayList<VehicleMessage> deleted =
                            new CopyOnWriteArrayList<>(mNotifications);
                    while(it.hasNext()) {
                        VehicleMessage message = it.next();
                        propagateMessage(message);
                        deleted.add(message);
                    }
                    mNotifications.removeAll(deleted) ;

                } catch(InterruptedException e) {
                    Log.d(TAG, "Interrupted while waiting for a new " +
                            "item for notification -- likely shutting down");
                    e.printStackTrace();
                    Thread.currentThread().interrupt();
                    break;
                } finally {
                    mNotificationsChanged.signal();
                    mNotificationsLock.unlock();
                }
            }
            Log.d(TAG, "Stopped notification thread");
        }
    }
}
