/* DistributedWave.java */

package distributedwave;

import java.util.Observable;
import java.net.InetAddress;
import java.io.IOException;
import java.util.Arrays;

/** An object that performs a distributed version of the "wave".  Concrete
 *  subclasses communicate with neighbors either by using UDP or TCP
 *
 *  A wave is propagated from one node to the next by a message consisting of 
 *  the character 'WAVE_STARTED_HERE_MESSAGE' followed by the IP of the system on 
 *  which the wave originated.  (The latter information is used to stop
 *  propagating a wave once it gets back to the node where it started, unless
 *  the "Keep propagating a wave forever" checkbox is checked in the GUI)
 *
 *  Copyright (c) 2010, 2011 - Russell C. Bjork
 */
public abstract class DistributedWave extends Observable
{
    /** Start the listeners.  This cannot be done until after the concrete
     *  class constructor has completed
     */
    public void startListeners()
    {
        // Create the and start threads that will listen for messages from
        // neighbors

        new Thread() {
            public void run()
            {
                listenForMessages(LEFT);
            }
        }.start();

        new Thread() {
            public void run()
            {
                listenForMessages(RIGHT);
            }
        }.start();
    }
    
    /** Show a wave
     *
     *  @param startingSide which side the wave should start on -
     *         one of LEFT, RIGHT
     *  @param messageReceived the message that was received from a neighbor 
     *         that resulted in this wave - null if this wave was originated by
     *         a button
     */
    public synchronized void doWave(int startingSide, byte [] messageReceived)
    {
        for (int i = 0; i < NUMBER_OF_STEPS; i ++)
        {
            setChanged();
            if (startingSide == LEFT)
                notifyObservers(i);
            else
                notifyObservers(NUMBER_OF_STEPS - i - 1);

            if (i == STEPS_BEFORE_SIGNALLING_NEIGHBOR)
            {
                byte [] messageToPropagate = 
                    (messageReceived != null) ? messageReceived 
                                              : WAVE_STARTED_HERE_MESSAGE;
                    
                // Propagate to the neighbor at the other side from where
                // the wave started

                sendToNeighbor(1 - startingSide, messageToPropagate);
            }
            try
            {
                Thread.sleep(TIME_PER_STEP);
            }
            catch(InterruptedException e)
            { }
        }
    }
                       
    /** Method executed by a thread that listens for incoming messages
     *
     *  @param side the side on which to listen - one of LEFT, RIGHT
     */
    void listenForMessages(int side)
    {
        byte [] messageReceived = null;
        while(true)
        {
            messageReceived = receiveMessage(side);
            
            // Do the wave unless the wave originated here and we are not
            // propagating forever
    
            if (! Arrays.equals(messageReceived, WAVE_STARTED_HERE_MESSAGE) ||
                    propagateForever)
                doWave(side, messageReceived);
        }
    }
    
    /** Set whether the wave should propagate forever, or stop when it gets
     *  back to the node where it started
     *
     *  @param propagatedForever true to propagate forever; false to stop when
     *         it gets back to where it started
     */
    public void setPropagateForever(boolean propagateForever)
    {
        this.propagateForever = propagateForever;
    }

    /** Constants that specify a direction
     */
    static final int LEFT = 0;
    static final int RIGHT = 1;
    
    /** Names for directions
     */
    static final String [] SIDE_NAME = { "left", "right" };

    /** The ports used for communicating with neighbors
     */
    static final int [] COMMUNICATE_WITH_NEIGHBOR_PORT = { 42420, 42421 };

    /** The length of a message - will be initialized by static constructor
     */
    static final int MESSAGE_LENGTH;

    /** The message that is sent when a wave was initiated by a GUI button.
     *  This will be initialized by static constructor
     */
    private static final byte [] WAVE_STARTED_HERE_MESSAGE;

    /** The number of individual steps to be displayed when displaying the wave
     */
    static final int NUMBER_OF_STEPS = 13;

    /** The time (in ms) for each step of the wave
     */
    private static final int TIME_PER_STEP = 200;

    /** The number of steps to perform before passing the wave on to a neighbor
     */
    private static final int STEPS_BEFORE_SIGNALLING_NEIGHBOR = 7;

    /** True for the wave to keep propagating forever; false if it should stop
     *  when it gets back to where it started
     */
    private volatile boolean propagateForever;

    /** Static constructor - initialize static constants
     */
    static
    {
        // Create the message used for propagating a wave that originated
        // here

        byte [] thisNodesIP = null;
        try
        {
            thisNodesIP = InetAddress.getLocalHost().getAddress();
        }
        catch(IOException e)
        {
            System.err.println("Error accessing IP for this host: " + e);
            System.exit(1);
        }
        MESSAGE_LENGTH = 1 + thisNodesIP.length;

        WAVE_STARTED_HERE_MESSAGE = new byte[MESSAGE_LENGTH];
        WAVE_STARTED_HERE_MESSAGE[0] = (byte) 'W';
        for (int i = 0; i < thisNodesIP.length; i ++)
            WAVE_STARTED_HERE_MESSAGE[i + 1] = thisNodesIP[i];

    }

    /***************************************************************************
     * ABSTRACT METHODS THAT WILL BE IMPLEMENTED BY CONCRETE SUBCLASSES
     ***************************************************************************
     
    /** Send a message to a neighbor
     *
     *  @param side the side of send to
     *  @param waveMessage the message describing this wave to be sent to the 
     *         neighbor
     */
    abstract void sendToNeighbor(int side, byte [] waveMessage);

    /** Receive a message from a neighbor.  This method blocks until a
     *  message is received
     *
     *  @param side the side to receive from
     *  @return the message received from this neighbor
     */
    abstract byte [] receiveMessage(int side);
}
