package lcm.lcm;

import java.net.*;
import java.io.*;
import java.util.*;
import java.util.regex.*;
import java.nio.*;

import lcm.util.*;

/** Lightweight Communications and Marshalling Java implementation **/
public class LCM
{
    static class SubscriptionRecord
    {
	String  regex;
	Pattern pat;
	LCMSubscriber lcsub;
    }

    ArrayList<SubscriptionRecord> subscriptions = new ArrayList<SubscriptionRecord>();
    ArrayList<Provider> providers = new ArrayList<Provider>();

    HashMap<String,ArrayList<SubscriptionRecord>> subscriptionsMap = new HashMap<String,ArrayList<SubscriptionRecord>>();

    boolean closed = false;

    static LCM singleton;

    LCMDataOutputStream encodeBuffer = new LCMDataOutputStream(new byte[1024]);

    /** Create a new LCM object, connecting to one or more URLs. If
     * no URL is specified, the environment variable LCM_DEFAULT_URL is
     * used. If that environment variable is not defined, then the
     * default URL is used.
     **/
    public LCM(String... urls) throws IOException
    {
	if (urls.length==0) {
	    String env = System.getenv("LCM_DEFAULT_URL");
	    if (env == null)
		urls = new String[] {"udpm://239.255.76.67:7667"};
	    else
		urls = new String[] { env };
	}
	
	for (String url : urls) {
	    URLParser up = new URLParser(url);
	    String protocol = up.get("protocol");
	    
	    if (protocol.equals("udpm"))
		providers.add(new UDPMulticastProvider(this, up));
	    else if (protocol.equals("tcp"))
		providers.add(new TCPProvider(this, up));
	    else if (protocol.equals("file"))
		providers.add(new LogFileProvider(this, up));
	    else
		System.err.println("LCM: Unknown URL protocol: "+protocol);
	}
    }

    /** Retrieve a default instance of LCM using either the environment
     * variable LCM_DEFAULT_URL or the default. If an exception
     * occurs, System.exit(-1) is called. 
     **/
    public static LCM getSingleton()
    {
	if (singleton == null) {
	    try {
		singleton = new LCM();
	    } catch (Exception ex) {
		System.err.println("LC singleton fail: "+ex);
		System.exit(-1);
		return null;
	    }
	}

	return singleton;
    }

    /** Return the number of subscriptions. **/
    public int getNumSubscriptions()
    {
	if (this.closed) throw new IllegalStateException();
	return subscriptions.size();
    }

    /** Publish a string on a channel. This method does not use the
     * LCM type definitions and thus is not type safe. This method is
     * primarily provided for testing purposes and may be removed in
     * the future.
     **/
    public void publish(String channel, String s) throws IOException
    {
	if (this.closed) throw new IllegalStateException();
	s=s+"\0";
	byte[] b = s.getBytes();
	publish(channel, b, 0, b.length);
    }

    /** Publish an LCM-defined type on a channel. If more than one URL was
     * specified, the message will be sent on each.
     **/
    public synchronized void publish(String channel, LCMEncodable e)
    {
	if (this.closed) throw new IllegalStateException();

	try {
	    encodeBuffer.reset();

	    e.encode(encodeBuffer);
	    
	    publish(channel, encodeBuffer.getBuffer(), 0, encodeBuffer.size());
	} catch (IOException ex) {
	    System.err.println("LC publish fail: "+ex);
	}
    }

    /** Publish raw data on a channel, bypassing the LCM type
     * specification. If more than one URL was specified when the LCM
     * object was created, the message will be sent on each.
     **/
    public synchronized void publish(String channel, byte[] data, int offset, int length) 
	throws IOException
    {
	if (this.closed) throw new IllegalStateException();
	for (Provider p : providers) 
	    p.publish(channel, data, offset, length);
    }

    /** Subscribe to all channels whose name matches the regular
     * expression. Note that to subscribe to all channels, you must
     * specify ".*", not "*". 
     **/
    public void subscribe(String regex, LCMSubscriber sub)
    {
	if (this.closed) throw new IllegalStateException();
	SubscriptionRecord srec = new SubscriptionRecord();
	srec.regex = regex;
	srec.pat = Pattern.compile(regex);
	srec.lcsub = sub;

	synchronized(this) {
	    for (Provider p : providers)
		p.subscribe (regex);
	}

	synchronized(subscriptions) {
	    subscriptions.add(srec);
	    
	    for (String channel : subscriptionsMap.keySet()) {
		if (srec.pat.matcher(channel).matches()) {
		    ArrayList<SubscriptionRecord> subs = subscriptionsMap.get(channel);
		    subs.add(srec);
		}
	    }
	}
    }

    /** Remove this particular regex/subscriber pair (UNTESTED AND API
     * MAY CHANGE). If regex is null, all subscriptions for 'sub' are
     * cancelled. If subscriber is null, any previous subscriptions
     * matching the regular expression will be cancelled. If both
     * 'sub' and 'regex' are null, all subscriptions will be
     * cancelled.
     **/
    public void unsubscribe(String regex, LCMSubscriber sub) {
	if (this.closed) throw new IllegalStateException();

        // TODO: need providers to unsubscribe?

        // TODO: providers don't seem to use anything beyond first channel

	synchronized(subscriptions) {

            // Find and remove subscriber from list
            for (Iterator<SubscriptionRecord> it = subscriptions.iterator(); it.hasNext(); ) {
		SubscriptionRecord sr = it.next();

                if ((sub == null || sr.lcsub == sub) &&
		    (regex == null || sr.regex.equals(regex))) {
                    it.remove();
                }
            }

            // Find and remove subscriber from map
	    for (String channel : subscriptionsMap.keySet()) {
              for (Iterator<SubscriptionRecord> it = subscriptionsMap.get(channel).iterator(); it.hasNext(); ) {
		  SubscriptionRecord sr = it.next();
		  
		  if ((sub == null || sr.lcsub == sub) &&
		      (regex == null || sr.regex.equals(regex))) {
		      it.remove();
		  }
                }
	    }
        }      
    }

    /** Not for use by end users. Provider back ends call this method
     * when they receive a message. The subscribers that match the
     * channel name are synchronously notified. 
     **/
    public void receiveMessage(String channel, byte data[], int offset, int length)
    {
	if (this.closed) throw new IllegalStateException();
	synchronized (subscriptions) {
	    ArrayList<SubscriptionRecord> srecs = subscriptionsMap.get(channel);
	    
	    if (srecs == null) {
		// must build this list!
		srecs = new ArrayList<SubscriptionRecord>();
		subscriptionsMap.put(channel, srecs);
		
		for (SubscriptionRecord srec : subscriptions) {
		    if (srec.pat.matcher(channel).matches())
			srecs.add(srec);
		}
	    }
	    
	    for (SubscriptionRecord srec : srecs) {
		srec.lcsub.messageReceived(this,
					   channel, 
					   new LCMDataInputStream(data, offset, length));
	    }
	}
    }
    
    /** A convenience function that subscribes to all LCM channels. **/
    public synchronized void subscribeAll(LCMSubscriber sub)
    {
	subscribe(".*", sub);
    }

    /** Call this function to release all resources used by the LCM instance.  After calling this
     * function, the LCM instance should consume no resources, and cannot be used to 
     * receive or transmit messages.
     */
    public synchronized void close()
    {
	if (this.closed) throw new IllegalStateException();
	for (Provider p : providers) {
	    p.close();
	}
	providers = null;
	this.closed = true;
    }

    ////////////////////////////////////////////////////////////////

    /** Minimalist test code. 
     **/
    public static void main(String args[])
    {
	LCM lcm;

	try {
	    lcm = new LCM();
	} catch (IOException ex) {
	    System.err.println("ex: "+ex);
	    return;
	}

	lcm.subscribeAll(new SimpleSubscriber());

	while (true) {				
	    try {
		Thread.sleep(1000);
		lcm.publish("TEST", "foobar");
	    } catch (Exception ex) {
		System.err.println("ex: "+ex);
	    }
	}
    }

    static class SimpleSubscriber implements LCMSubscriber
    {
	public void messageReceived(LCM lcm, String channel, LCMDataInputStream dins)
	{
	    System.err.println("RECV: "+channel);
	}
    }
}
