package com.openxc.sinks;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import android.util.Log;

import com.google.common.base.MoreObjects;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.openxc.NoValueException;
import com.openxc.measurements.BaseMeasurement;
import com.openxc.measurements.Measurement;
import com.openxc.measurements.UnrecognizedMeasurementTypeException;
import com.openxc.messages.KeyMatcher;
import com.openxc.messages.KeyedMessage;
import com.openxc.messages.SimpleVehicleMessage;
import com.openxc.messages.VehicleMessage;

/**
 * A data sink that sends new measurements of specific types to listeners.
 *
 * Applications requesting asynchronous updates for specific signals get their
 * values through this sink.
 */
public class MessageListenerSink extends AbstractQueuedCallbackSink {
    private final static String TAG = "MessageListenerSink";

    // The non-persistent listeners will be removed after they receive their
    // first message.
    private Map<KeyMatcher, MessageListenerGroup>
            mMessageListeners = new HashMap<>();
    private Multimap<Class<? extends Measurement>, Measurement.Listener>
            mMeasurementTypeListeners = HashMultimap.create();
    private Multimap<Class<? extends VehicleMessage>, VehicleMessage.Listener>
            mMessageTypeListeners = HashMultimap.create();

    private class MessageListenerGroup {
        
        ArrayList<VehicleMessage.Listener> mPersistentListeners = new ArrayList<>();
        ArrayList<VehicleMessage.Listener> mListeners = new ArrayList<>();
        
        void add(VehicleMessage.Listener listener, boolean persist) {
            if (persist) {
                mPersistentListeners.add(listener);
            } else {
                mListeners.add(listener);
            }
        }
        
        void removePersistent(VehicleMessage.Listener listener) {
            mPersistentListeners.remove(listener);
        }
        
        void removeAllNonPersistent() {
            mListeners = new ArrayList<>();
        }
        
    }
    
    public MessageListenerSink() {
        super();
    }

    public synchronized void register(KeyMatcher matcher,
            VehicleMessage.Listener listener, boolean persist) {
       
        MessageListenerGroup group = mMessageListeners.get(matcher);
        if (group == null) {
            group = new MessageListenerGroup();
            mMessageListeners.put(matcher, group);
        }
        
        group.add(listener, persist);
    }

    public synchronized void register(KeyMatcher matcher,
            VehicleMessage.Listener listener) {
        register(matcher, listener, true);
    }

    public synchronized void register(
            Class<? extends VehicleMessage> messageType,
            VehicleMessage.Listener listener) {
        mMessageTypeListeners.put(messageType, listener);
    }

    public void register(Class<? extends Measurement> measurementType,
            Measurement.Listener listener) {
        try {
            // A bit of a hack to cache this measurement's ID field so we
            // can deserialize incoming measurements of this type. Why don't we
            // have a getId() in the Measurement interface? Ah, because it would
            // have to be static and you can't have static methods in an
            // interface. It would work if we were passed an instance of the
            // measurement in this function, but we don't really have that when
            // adding a listener.
            BaseMeasurement.getKeyForMeasurement(measurementType);
        } catch(UnrecognizedMeasurementTypeException e) { }

        mMeasurementTypeListeners.put(measurementType, listener);
    }

    public synchronized void unregister(
            Class<? extends Measurement> measurementType,
            Measurement.Listener listener) {
        mMeasurementTypeListeners.remove(measurementType, listener);
    }

    public synchronized void unregister(
            Class<? extends VehicleMessage> messageType,
            VehicleMessage.Listener listener) {
        mMessageTypeListeners.remove(messageType, listener);
    }

    public synchronized void unregister(KeyMatcher matcher,
            VehicleMessage.Listener listener) {
        mMessageListeners.get(matcher).removePersistent(listener);
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this)
            .add("numMessageListeners", mMessageListeners.size())
            .add("numMessageTypeListeners", mMessageTypeListeners.size())
            .add("numPersistentMessageListeners", getNumPersistentListeners())
            .add("numMeasurementTypeListeners", mMeasurementTypeListeners.size())
            .toString();
    }
    
    private int getNumPersistentListeners() {
        
        int sum = 0;
        for (KeyMatcher matcher : mMessageListeners.keySet()) {
            sum += mMessageListeners.get(matcher).mPersistentListeners.size();
        }
        
        return sum;
    }

    @Override
    protected synchronized void propagateMessage(VehicleMessage message) {
        if (message instanceof KeyedMessage) {

            Set<KeyMatcher> matchedKeys = new HashSet<>();
            for (KeyMatcher matcher : mMessageListeners.keySet()) {
                if (matcher.matches(message.asKeyedMessage())) {
                    MessageListenerGroup group = mMessageListeners.get(matcher);
                    for (VehicleMessage.Listener listener : group.mPersistentListeners) {
                        listener.receive(message);
                    }
                    for (VehicleMessage.Listener listener : group.mListeners) {
                        listener.receive(message);
                    }
                }
            }

            for (KeyMatcher matcher : matchedKeys) {
                mMessageListeners.get(matcher).removeAllNonPersistent();
            }

            if (message instanceof SimpleVehicleMessage) {
                propagateMeasurementFromMessage(message.asSimpleMessage());
            }
        }

        if(mMessageTypeListeners.containsKey(message.getClass())) {
            for(VehicleMessage.Listener listener :
                    mMessageTypeListeners.get(message.getClass())) {
                listener.receive(message);
            }
        }
    }

    private synchronized void propagateMeasurementFromMessage(
            SimpleVehicleMessage message) {
        try {
            Measurement measurement =
                BaseMeasurement.getMeasurementFromMessage(message);
            if(mMeasurementTypeListeners.containsKey(measurement.getClass())) {
                for(Measurement.Listener listener :
                        mMeasurementTypeListeners.get(measurement.getClass())) {
                    listener.receive(measurement);
                }
            }
        } catch(UnrecognizedMeasurementTypeException e) {
            // The message is not a recognized Measurement, we don't propagate
            // it as a Measurement (only as a Message, which is handled
            // earlier).
        } catch(NoValueException e) {
            Log.w(TAG, "Received notification for a blank measurement", e);
        }
    }
}

@@ -2172,24 +2172,140 @@
  %7D%0A        %0A
+        boolean isEmpty() %7B%0A            return mPersistentListeners.isEmpty() && mListeners.isEmpty();%0A        %7D   %0A
     %7D%0A    %0A 
@@ -4502,67 +4502,199 @@
     
- mMessageListeners.get(matcher).removePersistent(listener);
+%0A        MessageListenerGroup group = mMessageListeners.get(matcher);%0A        if (group != null) %7B%0A            group.removePersistent(listener);%0A            pruneListeners(matcher);%0A        %7D
 %0A   
@@ -5386,24 +5386,246 @@
 n sum;%0A    %7D
+%0A    %0A    private void pruneListeners(KeyMatcher matcher) %7B%0A        MessageListenerGroup group = mMessageListeners.get(matcher);%0A        if (group.isEmpty()) %7B%0A            mMessageListeners.remove(matcher);%0A        %7D%0A    %7D
 %0A%0A    @Overr
@@ -6495,24 +6495,65 @@
 rsistent();%0A
+                pruneListeners(matcher);%0A
             
@@ -6546,32 +6546,44 @@
 ;%0A            %7D%0A
+            
 %0A            if 

Add map pruning typically handled automatically by Multimap.