package org.jgroups.blocks;

import org.jgroups.logging.Log;
import org.jgroups.logging.LogFactory;
import org.jgroups.*;
import org.jgroups.annotations.Unsupported;
import org.jgroups.util.RspList;
import org.jgroups.util.Util;

import java.io.Serializable;
import java.util.*;


/**
 * Provides the abstraction of a java.util.LinkedList that is replicated at several
 * locations. Any change to the list (reset, add, remove, etc.) will transparently be
 * propagated to all replicas in the group. All read-only methods will always access the
 * local replica.<p>
 * Both keys and values added to the list <em>must be serializable</em>, the reason
 * being that they will be sent across the network to all replicas of the group.
 * An instance of this class will contact an existing member of the group to fetch its
 * initial state.
 * Beware to use a <em>total protocol</em> on initialization or elements would not be in same
 * order on all replicas.
 * @author Romuald du Song
 */
@Unsupported
public class DistributedQueue implements MessageListener, MembershipListener, Cloneable
{
    public interface Notification
    {
        void entryAdd(Object value);

        void entryRemoved(Object key);

        void viewChange(Vector new_mbrs, Vector old_mbrs);

        void contentsCleared();

        void contentsSet(Collection new_entries);
    }

    protected Log logger = LogFactory.getLog(getClass());
    private long internal_timeout = 10000; // 10 seconds to wait for a response

    /*lock object for synchronization*/
    protected final Object mutex = new Object();
    protected boolean stopped = false; // whether to we are stopped !
    protected LinkedList internalQueue;
    protected Channel channel;
    protected RpcDispatcher disp = null;
    protected String groupname = null;
    protected Vector notifs = new Vector(); // to be notified when mbrship changes
    protected Vector members = new Vector(); // keeps track of all DHTs
    private Class[] add_signature = null;
    private Class[] addAtHead_signature = null;
    private Class[] addAll_signature = null;
    private Class[] reset_signature = null;
    private Class[] remove_signature = null;
    
    /**
     * Creates a DistributedQueue
     * @param groupname The name of the group to join
     * @param factory The ChannelFactory which will be used to create a channel
     * @param properties The property string to be used to define the channel
     * @param state_timeout The time to wait until state is retrieved in milliseconds. A value of 0 means wait forever.
     */
    public DistributedQueue(String groupname, ChannelFactory factory, String properties, long state_timeout)
                     throws ChannelException
    {
        if (logger.isDebugEnabled())
        {
            logger.debug("DistributedQueue(" + groupname + ',' + properties + ',' + state_timeout);
        }

        this.groupname = groupname;
        initSignatures();
        internalQueue = new LinkedList();
        channel = (factory != null) ? factory.createChannel((Object)properties) : new JChannel(properties);
        disp = new RpcDispatcher(channel, this, this, this);
        disp.setDeadlockDetection(false); // To ensure strict FIFO MethodCall
        channel.connect(groupname);
        start(state_timeout);
    }

    public DistributedQueue(JChannel channel)
    {
        this.groupname = channel.getClusterName();
        this.channel = channel;
        init();
    }

    /**
      * Uses a user-provided PullPushAdapter to create the dispatcher rather than a Channel. If id is non-null, it will be
      * used to register under that id. This is typically used when another building block is already using
      * PullPushAdapter, and we want to add this building block in addition. The id is the used to discriminate
      * between messages for the various blocks on top of PullPushAdapter. If null, we will assume we are the
      * first block created on PullPushAdapter.
      * The caller needs to call start(), before using the this block. It gives the opportunity for the caller
      * to register as a lessoner for Notifications events.
      * @param adapter The PullPushAdapter which to use as underlying transport
      * @param id A serializable object (e.g. an Integer) used to discriminate (multiplex/demultiplex) between
      *           requests/responses for different building blocks on top of PullPushAdapter.
      */
    public DistributedQueue(PullPushAdapter adapter, Serializable id)
    {
        this.channel = (Channel)adapter.getTransport();
        this.groupname = this.channel.getClusterName();

        initSignatures();
        internalQueue = new LinkedList();

        disp = new RpcDispatcher(adapter, id, this, this, this);
        disp.setDeadlockDetection(false); // To ensure strict FIFO MethodCall
    }

    protected final void init()
    {
        initSignatures();
        internalQueue = new LinkedList();
        disp = new RpcDispatcher(channel, this, this, this);
        disp.setDeadlockDetection(false); // To ensure strict FIFO MethodCall
    }

    public final void start(long state_timeout) throws ChannelClosedException, ChannelNotConnectedException
    {
        boolean rc;
        logger.debug("DistributedQueue.initState(" + groupname + "): starting state retrieval");

        rc = channel.getState(null, state_timeout);

        if (rc)
        {
            logger.info("DistributedQueue.initState(" + groupname + "): state was retrieved successfully");
        }
        else
        {
            logger.info("DistributedQueue.initState(" + groupname + "): state could not be retrieved (first member)");
        }
    }

    public Address getLocalAddress()
    {
        return (channel != null) ? channel.getAddress() : null;
    }

    public Channel getChannel()
    {
        return channel;
    }

    public void addNotifier(Notification n)
    {
        if (n != null && !notifs.contains(n))
        {
            notifs.addElement(n);
        }
    }

    public void removeNotifier(Notification n)
    {
        notifs.removeElement(n);
    }

    public void stop()
    {
        /*lock the queue from other threads*/
        synchronized (mutex)
        {
            internalQueue.clear();

            if (disp != null)
            {
                disp.stop();
                disp = null;
            }

            if (channel != null)
            {
                channel.close();
                channel = null;
            }

            stopped = true;
        }
    }

    /**
     * Add the speficied element at the bottom of the queue
     * @param value
     */
    public void add(Object value)
    {
        try
        {
            Object retval = null;

            RspList rsp = disp.callRemoteMethods(null, "_add", new Object[]{value}, add_signature, GroupRequest.GET_ALL, 0);
            Vector results = rsp.getResults();

            if (results.size() > 0)
            {
                retval = results.elementAt(0);

                if (logger.isDebugEnabled())
                {
                    checkResult(rsp, retval);
                }
            }
        }
         catch (Exception e)
        {
            logger.error("Unable to add value " + value, e);
        }

    }

    /**
     * Add the speficied element at the top of the queue
     * @param value
     */
    public void addAtHead(Object value)
    {
        try
        {
            disp.callRemoteMethods(null, "_addAtHead", new Object[]{value}, addAtHead_signature, GroupRequest.GET_ALL, 0);
        }
         catch (Exception e)
        {
            logger.error("Unable to addAtHead value " + value, e);
        }

    }

    /**
     * Add the speficied collection to the top of the queue.
     * Elements are added in the order that they are returned by the specified
     * collection's iterator.
     * @param values
     */
    public void addAll(Collection values)
    {
        try
        {
            disp.callRemoteMethods(null, "_addAll", new Object[]{values}, addAll_signature, GroupRequest.GET_ALL, 0);
        }
         catch (Exception e)
        {
            logger.error("Unable to addAll value: " + values, e);
        }

    }

    public Vector getContents()
    {
        Vector result = new Vector();

        for (Iterator e = internalQueue.iterator(); e.hasNext();)
            result.add(e.next());

        return result;
    }

    public int size()
    {
        return internalQueue.size();
    }

    /**
      * returns the first object on the queue, without removing it.
      * If the queue is empty this object blocks until the first queue object has
      * been added
      * @return the first object on the queue
      */
    public Object peek()
    {
        Object retval = null;

        try
        {
            retval = internalQueue.getFirst();
        }
         catch (NoSuchElementException e)
        {
        }

        return retval;
    }

    public void reset()
    {
        try
        {
            disp.callRemoteMethods(null, "_reset", null, reset_signature, GroupRequest.GET_ALL, 0);
        }
         catch (Exception e)
        {
            logger.error("DistributedQueue.reset(" + groupname + ')', e);
        }
    }

    protected void checkResult(RspList rsp, Object retval)
    {
        if (logger.isDebugEnabled())
        {
            logger.debug("Value updated from " + groupname + " :" + retval);
        }

        Vector results = rsp.getResults();

        for (int i = 0; i < results.size(); i++)
        {
            Object data = results.elementAt(i);

            if (!data.equals(retval))
            {
                logger.error("Reference value differs from returned value " + retval + " != " + data);
            }
        }
    }

    /**
     * Try to return the first objet in the queue.It does not wait for an object.
     * @return the first object in the queue or null if none were found.
     */
    public Object remove()
    {
        Object retval = null;
        RspList rsp = disp.callRemoteMethods(null, "_remove", null, remove_signature, GroupRequest.GET_ALL, internal_timeout);
        Vector results = rsp.getResults();

        if (results.size() > 0)
        {
            retval = results.elementAt(0);

            if (logger.isDebugEnabled())
            {
                checkResult(rsp, retval);
            }
        }

        return retval;
    }

    /**
     * @param timeout The time to wait until an entry is retrieved in milliseconds. A value of 0 means wait forever.
     * @return the first object in the queue or null if none were found
     */
    public Object remove(long timeout)
    {
        Object retval = null;
        long start = System.currentTimeMillis();

        if (timeout <= 0)
        {
            while (!stopped && (retval == null))
            {
                RspList rsp = disp.callRemoteMethods(null, "_remove", null, remove_signature, GroupRequest.GET_ALL, internal_timeout);
                Vector results = rsp.getResults();

                if (results.size() > 0)
                {
                    retval = results.elementAt(0);

                    if (logger.isDebugEnabled())
                    {
                        checkResult(rsp, retval);
                    }
                }

                if (retval == null)
                {
                    try
                    {
                        synchronized (mutex)
                        {
                            mutex.wait();
                        }
                    }
                     catch (InterruptedException e)
                    {
                    }
                }
            }
        }
        else
        {
            while (((System.currentTimeMillis() - start) < timeout) && !stopped && (retval == null))
            {
                RspList rsp = disp.callRemoteMethods(null, "_remove", null, remove_signature, GroupRequest.GET_ALL, internal_timeout);
                Vector results = rsp.getResults();

                if (results.size() > 0)
                {
                    retval = results.elementAt(0);

                    if (logger.isDebugEnabled())
                    {
                        checkResult(rsp, retval);
                    }
                }

                if (retval == null)
                {
                    try
                    {
                        long delay = timeout - (System.currentTimeMillis() - start);

                        synchronized (mutex)
                        {
                            if (delay > 0)
                            {
                                mutex.wait(delay);
                            }
                        }
                    }
                     catch (InterruptedException e)
                    {
                    }
                }
            }
        }

        return retval;
    }

    public String toString()
    {
        return internalQueue.toString();
    }

    /*------------------------ Callbacks -----------------------*/
    public void _add(Object value)
    {
        if (logger.isDebugEnabled())
        {
            logger.debug(groupname + '@' + getLocalAddress() + " _add(" + value + ')');
        }

        /*lock the queue from other threads*/
        synchronized (mutex)
        {
            internalQueue.add(value);

            /*wake up all the threads that are waiting for the lock to be released*/
            mutex.notifyAll();
        }

        for (int i = 0; i < notifs.size(); i++)
            ((Notification)notifs.elementAt(i)).entryAdd(value);
    }

    public void _addAtHead(Object value)
    {
        /*lock the queue from other threads*/
        synchronized (mutex)
        {
            internalQueue.addFirst(value);

            /*wake up all the threads that are waiting for the lock to be released*/
            mutex.notifyAll();
        }

        for (int i = 0; i < notifs.size(); i++)
            ((Notification)notifs.elementAt(i)).entryAdd(value);
    }

    public void _reset()
    {
        if (logger.isDebugEnabled())
        {
            logger.debug(groupname + '@' + getLocalAddress() + " _reset()");
        }

        _private_reset();

        for (int i = 0; i < notifs.size(); i++)
            ((Notification)notifs.elementAt(i)).contentsCleared();
    }

    protected void _private_reset()
    {
        /*lock the queue from other threads*/
        synchronized (mutex)
        {
            internalQueue.clear();

            /*wake up all the threads that are waiting for the lock to be released*/
            mutex.notifyAll();
        }
    }

    public Object _remove()
    {
        Object retval = null;

        try
        {
            /*lock the queue from other threads*/
            synchronized (mutex)
            {
                retval = internalQueue.removeFirst();

                /*wake up all the threads that are waiting for the lock to be released*/
                mutex.notifyAll();
            }

            if (logger.isDebugEnabled())
            {
                logger.debug(groupname + '@' + getLocalAddress() + "_remove(" + retval + ')');
            }

            for (int i = 0; i < notifs.size(); i++)
                ((Notification)notifs.elementAt(i)).entryRemoved(retval);
        }
         catch (NoSuchElementException e)
        {
            logger.debug(groupname + '@' + getLocalAddress() + "_remove(): nothing to remove");
        }

        return retval;
    }

    public void _addAll(Collection c)
    {
        if (logger.isDebugEnabled())
        {
            logger.debug(groupname + '@' + getLocalAddress() + " _addAll(" + c + ')');
        }

        /*lock the queue from other threads*/
        synchronized (mutex)
        {
            internalQueue.addAll(c);

            /*wake up all the threads that are waiting for the lock to be released*/
            mutex.notifyAll();
        }

        for (int i = 0; i < notifs.size(); i++)
            ((Notification)notifs.elementAt(i)).contentsSet(c);
    }

    /*----------------------------------------------------------*/
    /*-------------------- State Exchange ----------------------*/
    public void receive(Message msg)
    {
    }

    public byte[] getState()
    {
        Vector copy = (Vector)getContents().clone();

        try
        {
            return Util.objectToByteBuffer(copy);
        }
         catch (Throwable ex)
        {
            logger.error("DistributedQueue.getState(): exception marshalling state.", ex);

            return null;
        }
    }

    public void setState(byte[] new_state)
    {
        Vector new_copy;

        try
        {
            new_copy = (Vector)Util.objectFromByteBuffer(new_state);

            if (new_copy == null)
            {
                return;
            }
        }
         catch (Throwable ex)
        {
            logger.error("DistributedQueue.setState(): exception unmarshalling state.", ex);

            return;
        }

        _private_reset(); // remove all elements      
        _addAll(new_copy);
    }

    /*------------------- Membership Changes ----------------------*/
    public void viewAccepted(View new_view)
    {
        Vector new_mbrs = new_view.getMembers();

        if (new_mbrs != null)
        {
            sendViewChangeNotifications(new_mbrs, members); // notifies observers (joined, left)
            members.removeAllElements();

            for (int i = 0; i < new_mbrs.size(); i++)
                members.addElement(new_mbrs.elementAt(i));
        }
    }

    /** Called when a member is suspected */
    public void suspect(Address suspected_mbr)
    {
        ;
    }

    /** Block sending and receiving of messages until ViewAccepted is called */
    public void block()
    {
    }

    void sendViewChangeNotifications(Vector new_mbrs, Vector old_mbrs)
    {
        Vector joined;
        Vector left;
        Object mbr;
        Notification n;

        if ((notifs.size() == 0) || (old_mbrs == null) || (new_mbrs == null) || (old_mbrs.size() == 0) ||
                (new_mbrs.size() == 0))
        {
            return;
        }

        // 1. Compute set of members that joined: all that are in new_mbrs, but not in old_mbrs
        joined = new Vector();

        for (int i = 0; i < new_mbrs.size(); i++)
        {
            mbr = new_mbrs.elementAt(i);

            if (!old_mbrs.contains(mbr))
            {
                joined.addElement(mbr);
            }
        }

        // 2. Compute set of members that left: all that were in old_mbrs, but not in new_mbrs
        left = new Vector();

        for (int i = 0; i < old_mbrs.size(); i++)
        {
            mbr = old_mbrs.elementAt(i);

            if (!new_mbrs.contains(mbr))
            {
                left.addElement(mbr);
            }
        }

        for (int i = 0; i < notifs.size(); i++)
        {
            n = (Notification)notifs.elementAt(i);
            n.viewChange(joined, left);
        }
    }

    final void initSignatures()
    {
        try
        {
            if (add_signature == null)
            {
                add_signature = new Class[] { Object.class };
            }

            if (addAtHead_signature == null)
            {
                addAtHead_signature = new Class[] { Object.class };
            }

            if (addAll_signature == null)
            {
                addAll_signature = new Class[] { Collection.class };
            }

            if (reset_signature == null)
            {
                reset_signature = new Class[0];
            }

            if (remove_signature == null)
            {
                remove_signature = new Class[0];
            }
        }
         catch (Throwable ex)
        {
            logger.error("DistributedQueue.initMethods()", ex);
        }
    }

    public static void main(String[] args)
    {
        try
        {
            // The setup here is kind of weird:
            // 1. Create a channel
            // 2. Create a DistributedQueue (on the channel)
            // 3. Connect the channel (so the HT gets a VIEW_CHANGE)
            // 4. Start the HT
            //
            // A simpler setup is
            // DistributedQueue ht = new DistributedQueue("demo", null, 
            //         "file://c:/JGroups-2.0/conf/total-token.xml", 5000);
            JChannel c = new JChannel("file:/c:/JGroups-2.0/conf/conf/total-token.xml");

            DistributedQueue ht = new DistributedQueue(c);
            c.connect("demo");
            ht.start(5000);

            ht.add("name");
            ht.add("Michelle Ban");

            Object old_key = ht.remove();
            System.out.println("old key was " + old_key);
            old_key = ht.remove();
            System.out.println("old value was " + old_key);

            ht.add("name 'Michelle Ban'");

            System.out.println("queue is " + ht);
        }
         catch (Throwable t)
        {
            t.printStackTrace();
        }
    }
}
