package jasync;

import java.util.HashMap;

/**
 * Factory class for making message. Handles ID numbers and automatically
 * sets fields.
 * @see Message
 */
public class MessageFactory {
    /**
     * ID number counter.
     */
    private int nextUniqueID;

    /**
     * Stores used ID numbers.
     */
    private HashMap<Integer, Boolean> usedID;

    /**
     * The Peer instance of the owner of this factory, for the purposes of
     * adding that information to appropriate messages.
     */
    private Peer owner;

    /**
     * Constructor.
     * @param peer Peer instance for this connection.
     */
    public MessageFactory (Peer peer) {
	usedID = new HashMap<Integer, Boolean>();
	nextUniqueID = 0;
	this.owner = peer;
    }

    /**
     * Returns an ID guarunteed to not be in use.
     * @return a unique ID
     */
    private synchronized int getUniqueMessageID () {
	int outgo;

	/* If we've used up the whole positive integer space. I know it's 
	   MAX_VALUE + 1, but size() returns an int, so the maximum size
	   must be MAX_VALUE */
	while (usedID.size() == Integer.MAX_VALUE) {
	    try {
		wait();
	    }
	    catch (InterruptedException e) {
		e.printStackTrace();
	    }
	}

	while (true) {
	    if (nextUniqueID == Integer.MAX_VALUE) {
		nextUniqueID = 0;
		outgo = nextUniqueID;
	    }
	    else {
		outgo = nextUniqueID;
		nextUniqueID++;
	    }

	    if (!usedID.containsKey(outgo)) {
		break;
	    }
	}

	usedID.put(outgo, true);
	return outgo;
    }

    /**
     * Causes an ID to become usable again.
     * @param in Message with the ID that can now be recycled.
     */
    public synchronized void retireMessage (Message in) {
	retireID(getUniqueMessageID());
    }

    /**
     * Causes an ID to become usable again.
     * @param id ID to be retired.
     */
    protected synchronized void retireID (int id) {
	usedID.remove(id);
	notifyAll();
    }

    /**
     * Creates a message with an automatically set uniqueID field and no
     * payload.
     * @param type Type of the message
     * @param sync SYNC or ASYNC
     * @param status error code
     * @param command Action to take upon reciept
     * @return A newly created Message instance.
     * @see Message
     */
    protected Message makeGenericMessage (MessageType type,
					MessageSync sync,
					MessageStatus status,
					MessageCommand command) {
	Message outgo;
	int id = getUniqueMessageID();

	try {
	    outgo = new Message(id, type, sync, status, command);
	}
	catch (java.lang.Throwable e) {
	    retireID(id);
	    return null;
	}

	return outgo;
    }

    /**
     * Creates a message with an automatically set uniqueID field with payload
     * @param type Type of the message
     * @param sync SYNC or ASYNC
     * @param status error code
     * @param command Action to take upon reciept
     * @param data Any number of payload objects
     * @return A newly created Message instance.
     * @see Message
     */
    protected Message makeGenericMessage (MessageType type,
					MessageSync sync,
					MessageStatus status,
					MessageCommand command,
					Object... data) {
	Message outgo;
	int id = getUniqueMessageID();

	try {
	    outgo = new Message(id, type, sync, status, command, data);
	}
	catch (java.lang.Throwable e) {
	    retireID(id);
	    return null;
	}
	
	return outgo;
    }

    /**
     * Creates a message with the given ID, with no payload.
     * @param id ID number to use
     * @param type Type of the message
     * @param sync SYNC or ASYNC
     * @param status error code
     * @param command Action to take upon reciept
     * @return A newly created Message instance.
     * @see Message
     */
    protected Message makeGenericMessage (int id,
					MessageType type,
					MessageSync sync,
					MessageStatus status,
					MessageCommand command) {
	return new Message(id, type, sync, status, command);
    }

    /**
     * Creates a message with the given ID and payload.
     * @param type Type of the message
     * @param sync SYNC or ASYNC
     * @param status error code
     * @param command Action to take upon reciept
     * @param data Any number of payload objects
     * @return A newly created Message instance.
     * @see Message
     */
    protected Message makeGenericMessage (int id,
					MessageType type,
					MessageSync sync,
					MessageStatus status,
					MessageCommand command,
					Object... data) {
	return new Message(id, type, sync, status, command, data);
    }

    /**
     * Create the first part of a handshake.
     * @return A newly created handshake Message instance.
     * @see Message
     */
    public Message makeHandshakeMessage () {
	return makeGenericMessage(MessageType.PUSH,
				  MessageSync.SYNC,
				  MessageStatus.OK,
				  MessageCommand.HANDSHAKE,
				  new PeerImpl(owner));
    }

    /**
     * Create the second part of a handshake.
     * @return A newly created handshake Message acknowledgement.
     * @see Message
     */
    public Message makeHandshakeMessageAck (Message orig) {
       	return makeGenericMessage(orig.getUniqueID(),
				  MessageType.PUSH_ACK,
				  MessageSync.SYNC,
				  MessageStatus.OK,
				  MessageCommand.HANDSHAKE,
				  new PeerImpl(owner));
    }

    /**
     * Create a synchronous NOP Message.
     * @return A newly created NOP Message instance.
     * @see Message
     */
    public Message makeNopMessage () {
	return makeGenericMessage(MessageType.PUSH,
				  MessageSync.SYNC,
				  MessageStatus.OK,
				  MessageCommand.NOP);
    }
    
    /**
     * Create a NOP acknowledgement.
     * @return A newly created NOP Message acknowledgement.
     * @see Message
     */
    public Message makeNopMessageAck (Message orig) {
	return makeGenericMessage(orig.getUniqueID(),
				  MessageType.PUSH_ACK,
				  MessageSync.SYNC,
				  MessageStatus.OK,
				  MessageCommand.NOP);
    }

    /**
     * Create an asynchronous NOP Message.
     * @return A newly created asynchronous NOP Message instance.
     * @see Message
     */
    public Message makeAsyncNopMessage () {
	return makeGenericMessage(Message.ASYNC_ID,
				  MessageType.PUSH,
				  MessageSync.ASYNC,
				  MessageStatus.OK,
				  MessageCommand.NOP);
    }

    /**
     * Create an asynchronous CONNECT pseudo-message. These are never sent over
     * the network. Instead, they are used as notifications to the application
     * that the connection has been established.
     * @return A newly created asynchronous CONNECT Message.
     * @see Message
     */
    public static Message makeAsyncConnectMessage () {
	return new Message(Message.ASYNC_ID, MessageType.PUSH,
			   MessageSync.ASYNC, MessageStatus.OK,
			   MessageCommand.CONNECT);
    }

    /**
     * Create an asynchronous DISCONNECT pseudo-message. These are never sent
     * over the network. Instead, they are used as notifications to the
     * application that the connection has been dropped.
     * @return A newly created asynchronous DISCONNECT Message.
     * @see Message
     */
    public static Message makeAsyncDisconnectMessage () {
	return new Message(Message.ASYNC_ID, MessageType.PUSH,
			   MessageSync.ASYNC, MessageStatus.OK,
			   MessageCommand.DISCONNECT);
    }
}

