/***********************************************************************

GECCO, a platform for command and control gaming

Copyright (C) 2001 Henrik Bäärnhielm, Andreas Enblom, Niklas
Hallenfur, Karl Hasselström, Henrik Hägerström, Oskar Linde, Jing Fu
Zi and Jon Åslund

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or (at
your option) any later version.

This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
USA.

***********************************************************************/

package gecco.communication.server;
import gecco.client.Action;
import gecco.communication.messages.*;
import gecco.communication.server.*;
import gecco.server.core.ActionDefinition;
import gecco.server.core.Server;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.image.BufferedImage;

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

/**
 *  a class that listens for incoming messages from the client, receives them,
 *  and tells the server about them
 *
 * @author     Niklas Hallenfur, Henrik Hägerström, Jon Åslund
 * @created    June 27, 2001
 */
public class DataOutputExchangeHandler implements Runnable {
    /**
     *  Description of the Field
     */
    public Boolean pingSyncObject;

    /**
     *  Description of the Field
     */
    private MessageQueue messages_to_send;
    /**
     *  Description of the Field
     */
    private ClientConnection connection;
    /**
     *  Description of the Field
     */
    private Server server;
    /**
     *  Description of the Field
     */
    private ClientConnectionHandler i_am_working_for;
    /**
     *  Description of the Field
     */
    private boolean disconnect;


    /**
     *  Constructor for the DataOutputExchangeHandler object
     *
     * @param  q    Description of Parameter
     * @param  cc   Description of Parameter
     * @param  se   Description of Parameter
     * @param  cch  Description of Parameter
     */
    public DataOutputExchangeHandler(MessageQueue q, ClientConnection cc,
            Server se, ClientConnectionHandler cch) {
        messages_to_send = q;
        connection = cc;
        server = se;
        i_am_working_for = cch;
        disconnect = false;
        // Just a dummy object to synchronize on, the class doesn't matter at all...
        pingSyncObject = new Boolean(true);
    }


    /**
     *  Adds a feature to the MessageToQueue attribute of the
     *  DataOutputExchangeHandler object
     *
     * @param  message  The feature to be added to the MessageToQueue attribute
     */
    public void addMessageToQueue(Object message) {
        synchronized (messages_to_send) {
            messages_to_send.add(message);
            messages_to_send.notifyAll();
        }
    }


    /**
     *  Main processing method for the DataOutputExchangeHandler object
     */
    public void run() {
        while (!disconnect) {
            try {
                Object o = null;
                // Wait until a message is placed in the queue if it's empty!
                synchronized (messages_to_send) {
                    if (messages_to_send.isEmpty()) {
                        messages_to_send.wait();
                    }
                    o = messages_to_send.get();
                }
                // Release sync on message queue, and send message!
                if (o != null) {
                    sendMessage(o);
                }
                // Yield to other threads!
                Thread.yield();
            } catch (Exception e) {
                i_am_working_for.connectionLost(connection.getRole(),
                        connection.getId());
                break;
            }
        }
    }


    /**
     *  called when the client says it wants to abort the game or disconnect
     */
    public void abortGame() {
        i_am_working_for.connectionLost(connection.getRole(),
                connection.getId());
    }


    /**
     *  called whenever someone wants this Thread to die
     */
    public void kill() {
        disconnect = true;
    }


    /**
     *  Called when a message is to be sent Checks the class of the object to
     *  send, and calls the appropriate method that sends that kind of object
     *
     * @param  m              Description of Parameter
     * @exception  Exception  Description of Exception
     */
    private void sendMessage(Object m) throws Exception {

        // Send and receive a ping, to ensure that the client is ready
        // to receive a message!
//        ping();


        if (m instanceof UnitPositionUpdateMessage) {
            sendUnitPositionUpdate((UnitPositionUpdateMessage) m);

        } else if (m instanceof UnitPropertyChangedMessage) {
            sendUnitPropertiesUpdate((UnitPropertyChangedMessage) m);

        } else if (m instanceof UnitVisibleMessage) {
            sendUnitVisible((UnitVisibleMessage) m);

        } else if (m instanceof UnitMessageMessage) {
            sendUnitMessage((UnitMessageMessage) m);

        } else if (m instanceof UnitDeletedMessage) {
            sendUnitDeleted((UnitDeletedMessage) m);

        } else if (m instanceof UnitDestroyedMessage) {
            sendUnitDestroyed((UnitDestroyedMessage) m);

        } else if (m instanceof ActionBlockedMessage) {
            sendActionBlocked((ActionBlockedMessage) m);

        } else if (m instanceof MapUpdateMessage) {
            sendMapUpdate((MapUpdateMessage) m);

        } else if (m instanceof MapUpdatesMessage) {
            sendMapUpdates((MapUpdatesMessage) m);

        } else if (m instanceof MapPointInvisibleMessage) {
            sendMapPointInvisible((MapPointInvisibleMessage) m);

        } else if (m instanceof MapPointsInvisibleMessage) {
            sendMapPointsInvisible((MapPointsInvisibleMessage) m);

        } else if (m instanceof ActionCompletedMessage) {
            sendActionCompleted((ActionCompletedMessage) m);

        } else if (m instanceof ActionFailedMessage) {
            sendActionFailed((ActionFailedMessage) m);
        } else {
        }

        connection.getDataOutputStream().flush();
    }


    /**
     *  Sends a message that tells the client the new position of a unit
     *
     * @param  m              Description of Parameter
     * @exception  Exception  Description of Exception
     */
    private void sendUnitPositionUpdate(UnitPositionUpdateMessage m)
             throws Exception {

        connection.getDataOutputStream().writeInt(MessageType.UnitPositionUpdate);

        connection.getDataOutputStream().writeInt(m.getUnitHandle());
        connection.getDataOutputStream().writeDouble(m.getX());
        connection.getDataOutputStream().writeDouble(m.getY());

    }


    /**
     *  sends a message that tells the client the new values of the properties
     *  of a unit
     *
     * @param  m              Description of Parameter
     * @exception  Exception  Description of Exception
     */
    private void sendUnitPropertiesUpdate(UnitPropertyChangedMessage m)
             throws Exception {

        connection.getDataOutputStream().writeInt(MessageType.UnitPropertyUpdate);
        connection.getDataOutputStream().writeInt(m.handle);
        connection.getDataOutputStream().writeUTF(m.key);
        connection.getDataOutputStream().writeUTF(m.newValue);
    }


    /**
     *  sends a message that tells the client that a new unit is visible
     *
     * @param  m              Description of Parameter
     * @exception  Exception  Description of Exception
     */

    private void sendUnitVisible(UnitVisibleMessage m) throws Exception {
        connection.getDataOutputStream().writeInt(MessageType.UnitVisible);
        connection.getDataOutputStream().writeInt(m.handle);
        if (m.name == null) {
            connection.getDataOutputStream().writeUTF(new String());
        } else {
            connection.getDataOutputStream().writeUTF(m.name);
        }
        connection.getDataOutputStream().writeUTF(m.type);
        connection.getDataOutputStream().writeDouble(m.x);
        connection.getDataOutputStream().writeDouble(m.y);

        // send unblocked actions
        if (m.unblockedActions == null) {
            connection.getDataOutputStream().writeInt(0);
        } else {
            connection.getDataOutputStream().writeInt(m.unblockedActions.size());
            Iterator it = m.unblockedActions.iterator();
            while (it.hasNext()) {
                ActionDefinition actDef = (ActionDefinition) it.next();
                connection.getDataOutputStream().writeUTF(actDef.name);
                connection.getDataOutputStream().writeUTF(actDef.description);
                if (actDef.argumentType.equals("UNIT")) {
                    connection.getDataOutputStream().writeInt(Action.PIECE);
                } else if (actDef.argumentType.equals("POINT")) {
                    connection.getDataOutputStream().writeInt(Action.POSITION);
                } else if (actDef.argumentType.equals("VOID")) {
                    connection.getDataOutputStream().writeInt(Action.VOID);
                } else if (actDef.argumentType.equals("INSTANT_UNIT")) {
                    connection.getDataOutputStream().writeInt(Action.PIECE);
                } else if (actDef.argumentType.equals("INSTANT_POINT")) {
                    connection.getDataOutputStream().writeInt(Action.POSITION);
                } else if (actDef.argumentType.equals("INSTANT_VOID")) {
                    connection.getDataOutputStream().writeInt(Action.VOID);
                }
                connection.getDataOutputStream().writeBoolean(actDef.isInstantaneous);
            }
        }

        // send properties
        if (m.properties == null) {
            connection.getDataOutputStream().writeInt(0);
        } else {
            connection.getDataOutputStream().writeInt(m.properties.keySet().size());
            Iterator it = m.properties.keySet().iterator();
            while (it.hasNext()) {
                String key = (String) it.next();
                connection.getDataOutputStream().writeUTF(key);
                connection.getDataOutputStream().writeUTF(m.properties.get(key).toString());
            }
        }
    }


    /**
     *  Description of the Method
     *
     * @param  m              Description of Parameter
     * @exception  Exception  Description of Exception
     */
    private void sendUnitDeleted(UnitDeletedMessage m) throws Exception {
        connection.getDataOutputStream().writeInt(MessageType.UnitDeleted);
        connection.getDataOutputStream().writeInt(m.handle);
    }


    /**
     *  Description of the Method
     *
     * @param  m              Description of Parameter
     * @exception  Exception  Description of Exception
     */
    private void sendUnitMessage(UnitMessageMessage m) throws Exception {
        connection.getDataOutputStream().writeInt(MessageType.UnitMessage);
        connection.getDataOutputStream().writeInt(m.handle);
        connection.getDataOutputStream().writeUTF(m.message);
    }


    /**
     *  Description of the Method
     *
     * @param  m              Description of Parameter
     * @exception  Exception  Description of Exception
     */
    private void sendUnitDestroyed(UnitDestroyedMessage m) throws Exception {
        connection.getDataOutputStream().writeInt(MessageType.UnitDestroyed);
        connection.getDataOutputStream().writeInt(m.handle);
    }


    /**
     *  Description of the Method
     *
     * @param  m              Description of Parameter
     * @exception  Exception  Description of Exception
     */
    private void sendActionBlocked(ActionBlockedMessage m) throws Exception {
        connection.getDataOutputStream().writeInt(MessageType.ActionBlocked);

        connection.getDataOutputStream().writeInt(m.handle);
        connection.getDataOutputStream().writeUTF(m.name);
        connection.getDataOutputStream().writeUTF(m.description);

        if (m.argumentType.equals("UNIT")) {
            connection.getDataOutputStream().writeInt(Action.PIECE);
            connection.getDataOutputStream().writeBoolean(false);
        } else if (m.argumentType.equals("POINT")) {
            connection.getDataOutputStream().writeInt(Action.POSITION);
            connection.getDataOutputStream().writeBoolean(false);
        } else if (m.argumentType.equals("VOID")) {
            connection.getDataOutputStream().writeInt(Action.VOID);
            connection.getDataOutputStream().writeBoolean(false);
        } else if (m.argumentType.equals("INSTANT_UNIT")) {
            connection.getDataOutputStream().writeInt(Action.PIECE);
            connection.getDataOutputStream().writeBoolean(true);
        } else if (m.argumentType.equals("INSTANT_POINT")) {
            connection.getDataOutputStream().writeInt(Action.POSITION);
            connection.getDataOutputStream().writeBoolean(true);
        } else if (m.argumentType.equals("INSTANT_VOID")) {
            connection.getDataOutputStream().writeInt(Action.VOID);
            connection.getDataOutputStream().writeBoolean(true);
        }
        connection.getDataOutputStream().writeBoolean(m.isBlocked);

    }


    /**
     *  Description of the Method
     *
     * @param  m              Description of Parameter
     * @exception  Exception  Description of Exception
     */
    private void sendMapUpdate(MapUpdateMessage m) throws Exception {
        connection.getDataOutputStream().writeInt(MessageType.MapUpdate);

        connection.getDataOutputStream().writeInt(m.x);
        connection.getDataOutputStream().writeInt(m.y);
        connection.getDataOutputStream().writeInt(m.color.getRGB());
    }


    /**
     *  Description of the Method
     *
     * @param  m              Description of Parameter
     * @exception  Exception  Description of Exception
     */
    private void sendMapUpdates(MapUpdatesMessage m) throws Exception {
        connection.getDataOutputStream().writeInt(MessageType.MapUpdates);
        connection.getDataOutputStream().writeInt(m.changes.length);

        connection.getDataOutputStream().write(m.changes, 0, m.changes.length);
    }


    /**
     *  Description of the Method
     *
     * @param  m              Description of Parameter
     * @exception  Exception  Description of Exception
     */
    private void sendMapPointsInvisible(MapPointsInvisibleMessage m) throws Exception {
        connection.getDataOutputStream().writeInt(MessageType.MapPointsInvisible);
        connection.getDataOutputStream().writeInt(m.invisible.length);

        connection.getDataOutputStream().write(m.invisible);
    }


    /**
     *  Description of the Method
     *
     * @param  m              Description of Parameter
     * @exception  Exception  Description of Exception
     */
    private void sendMapPointInvisible(MapPointInvisibleMessage m) throws Exception {
        connection.getDataOutputStream().writeInt(MessageType.MapPointInvisible);

        connection.getDataOutputStream().writeInt(m.x);
        connection.getDataOutputStream().writeInt(m.y);
    }


    /**
     *  Description of the Method
     *
     * @param  m              Description of Parameter
     * @exception  Exception  Description of Exception
     */
    private void sendActionCompleted(ActionCompletedMessage m) throws Exception {
        connection.getDataOutputStream().writeInt(MessageType.ActionCompleted);
        connection.getDataOutputStream().writeLong(m.handle);
        connection.getDataOutputStream().writeUTF(m.message);
    }


    /**
     *  Description of the Method
     *
     * @param  m              Description of Parameter
     * @exception  Exception  Description of Exception
     */
    private void sendActionFailed(ActionFailedMessage m) throws Exception {
        connection.getDataOutputStream().writeInt(MessageType.ActionFailed);
        connection.getDataOutputStream().writeLong(m.handle);
        connection.getDataOutputStream().writeUTF(m.errorMessage);
    }


    /**
     *  send a 'ping' to see if this client is available
     *
     * @exception  Exception  Description of Exception
     */
    private void ping() throws Exception {
        connection.getDataOutputStream().writeInt(MessageType.Ping);
        connection.getDataOutputStream().flush();
        synchronized (pingSyncObject) {
            pingSyncObject.wait();
        }
    }

}
