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

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.client;
import gecco.client.animation.*;
import gecco.client.dialogs.*;
import gecco.client.infopanels.*;
import java.awt.*;
import java.awt.event.*;

import java.util.*;
import javax.swing.*;

/**
 *  This is the main game class. It contains the complete client for an
 *  arbitrary game.
 *
 * @author     Andreas Enblom
 * @created    June 27, 2001
 */
public class Game extends JFrame implements Client, Animator {
    /**
     *  The title of the game, shown as title in window.
     */
    String gameTitle;

    /**
     *  Description of the Field
     */
    Server server = new gecco.communication.client.ServerAmbassador(this);
    /**
     *  Description of the Field
     */
    HostPort lastHost;
    /**
     *  Description of the Field
     */
    Board board;
    /**
     *  Description of the Field
     */
    Map map;
    /**
     *  Description of the Field
     */
    PieceInfoPanel pieceInfoPanel = new PieceInfoPanel(this);

    /**
     *  The pieces that are currenlty in the game, hashed on piece handle.
     */
    TreeMap pieces;

    /**
     *  The pieces that are currenlty executing some actions. These actions are
     *  those on top of the queue and are of course not instantaneous. Hashed on
     *  action id.
     */
    Hashtable actionExecutingPieces;

    /**
     *  The action id of the currenlty executing action for some piece. Hashed
     *  on piece handle. The inverse of actionExecutingPieces.
     */
    Hashtable actionIds;

    /**
     *  The handle of the ghost or explosion that was added last. The handles of
     *  ghost pieces and explosion are created and maintianed by the client, and
     *  they should be negative as it is assumed that the handles that come from
     *  the server are positive.
     */
    int lastExtraPieceId = 0;

    /**
     *  Description of the Field
     */
    Piece selectedPiece = null;
    /**
     *  Description of the Field
     */
    Action selectedAction = null;

    /**
     *  The listener that handles selections in the menu that displays the
     *  actions of a piece.
     */
    ActionListener actionMenuListener;

    /**
     *  The listener that handles clicks on the board.
     */
    MouseListener boardClicks;

    /**
     *  The listener that handles drags on the board.
     */
    MouseMotionListener boardDrags;

    /**
     *  The menu that is displayed with piece actions, ghost pieces or ghost
     *  actions. Different from time to time.
     */
    JPopupMenu actionMenu;

    /**
     *  The menu that contains the actions of a ghost piece (so far only remove.
     *  It is created the first time it is used, then it remains the same.
     */
    JPopupMenu ghostActionMenu;

    /**
     *  The point that was clicked on the board to bring up the ghost menu. This
     *  point is used to place the new ghost piece where the mouse was clicked.
     */
    Point ghostMenuClick = new Point(0, 0);

    /**
     *  All things that has to do with ghost pieces are turned on or off by this
     *  flag. Default: on.
     */
    boolean useGhosts = true;


    /*
     * ------------------
     * Giant constructor.
     * ------------------
     */

    /**
     *  Constructor for the Game object
     *
     * @param  gameTitle  Description of Parameter
     */
    public Game(String gameTitle) {
        this.gameTitle = new String(gameTitle);

        // set up game control listeners

        // the behavior when an actions is selected from the menu
        // of actions for a piece.
        actionMenuListener =
            new ActionListener() {
                /**
                 *  Description of the Method
                 *
                 * @param  e  Description of Parameter
                 */
                public void actionPerformed(ActionEvent e) {
                    board.repaintBoard();
                    if (selectedPiece == null) {
                        return;
                    }
                    try {
                        ActionMenuItem item = (ActionMenuItem) e.getSource();
                        Action action = item.getMyAction();

                        // no parameter
                        if (action.getParameterType() == Action.VOID) {
                            if (action.isInstantaneous()) {
                                server.doAction(selectedPiece.getHandle(),
                                        action);
                            } else {
                                selectedPiece.queue(action);
                                pieceInfoPanel.updateActionDisplay();
                                if (selectedPiece.getExecutingAction() == null) {
                                    executeNextAction(selectedPiece);
                                }
                            }
                            return;
                        }

                        // has parameter
                        selectAction(action);
                    } catch (Exception ex) {
                    }
                }
            };

        // The behavior when the board is clicked. Selects pieces
        // and sets up action parameters, displays menus etc.
        boardClicks =
            new MouseAdapter() {
                /**
                 *  Description of the Method
                 *
                 * @param  e  Description of Parameter
                 */
                public void mousePressed(MouseEvent e) {
                    board.repaintBoard();
                    if ((e.getModifiers() & MouseEvent.BUTTON1_MASK) != 0) {
                        if (selectedAction == null) {
                            Piece p = getPieceAt(e.getPoint());
                            selectPiece(p);
                            return;
                        }
                        if (selectedAction.getParameterType() ==
                                Action.POSITION) {
                            Position p = board.getPositionAt(e.getPoint());
                            selectedAction.setParameter(p);
                        }
                        if (selectedAction.getParameterType() ==
                                Action.PIECE) {
                            Piece p = getPieceAt(e.getPoint());
                            // use only pieces from the server as parameters
                            // of actions, not ghosts or explosions
                            if ((p == null) || (p.getHandle() < 0)) {
                                unselectAction();
                                return;
                            }
                            selectedAction.setParameter(p);
                        }
                        if (selectedAction.isInstantaneous()) {
                            server.doAction(selectedPiece.getHandle(),
                                    selectedAction);
                        } else {
                            selectedPiece.queue(selectedAction);
                            pieceInfoPanel.updateActionDisplay();
                            if (selectedPiece.getExecutingAction() == null) {
                                executeNextAction(selectedPiece);
                            }
                        }
                        unselectAction();
                        return;
                    }
                    if ((e.getModifiers() & (MouseEvent.BUTTON2_MASK |
                            MouseEvent.BUTTON3_MASK)) != 0) {
                        Piece p = getPieceAt(e.getPoint());
                        if (p != null) {
                            selectPiece(p);
                        }

                        if (actionMenu != null) {
                            actionMenu.setVisible(false);
                            board.remove(actionMenu);
                            actionMenu = null;
                        }
                        if (selectedPiece != null) {
                            if (selectedPiece instanceof Ghost) {
                                actionMenu = getGhostActionMenu();
                            } else {
                                actionMenu = createActionMenu(selectedPiece);
                            }
                        } else if (useGhosts) {
                            ghostMenuClick = new Point(e.getPoint());
                            actionMenu = getGhostMenu();
                        }
                        if (actionMenu != null) {
                            board.add(actionMenu);
                            actionMenu.show(board, e.getX(), e.getY());
                        }

                        return;
                    }
                }
            };

        // lets the ghost pieces be draggable
        boardDrags =
            new MouseMotionAdapter() {
                /**
                 *  Description of the Method
                 *
                 * @param  e  Description of Parameter
                 */
                public void mouseDragged(MouseEvent e) {
                    if ((selectedPiece != null) &&
                            (actionMenu != null) &&
                            (!actionMenu.isVisible())
                             && (selectedPiece instanceof Ghost)) {
                        Position newPosition =
                                board.getPositionAt(e.getPoint());
                        selectedPiece.setPosition(newPosition);
                        board.repaintBoard();
                    }
                }
            };

        // set up game objects

        // XXX There is a possibility of different threads trying to access
        // the thing simultaneously, this makes sure any iterators over
        // the HashMap does not crash when for example a piece is removed
        // somewhere else.
        pieces =
            new TreeMap() {
                /**
                 *  Description of the Method
                 *
                 * @return    Description of the Returned Value
                 */
                public Collection values() {
                    return new HashSet(super.values());
                }
            };

        actionExecutingPieces = new Hashtable();
        actionIds = new Hashtable();
        board = new Board(this);
        board.addBoardMouseListener(boardClicks);
        board.addBoardMouseMotionListener(boardDrags);

        // set up layout
        JLabel logo = new JLabel();
        logo.setIcon(new ImageIcon(
                ClassLoader.getSystemClassLoader().
                getResource("gecco/client/icons/logo.gif")));
        logo.setAlignmentX(Component.CENTER_ALIGNMENT);
        Box boardPanel = Box.createVerticalBox();
        board.setAlignmentX(Component.CENTER_ALIGNMENT);
        boardPanel.add(board);
        boardPanel.add(Box.createVerticalStrut(10));
        boardPanel.add(logo);
        boardPanel.add(Box.createVerticalStrut(10));
        getContentPane().setLayout(new BorderLayout());
        getContentPane().add(boardPanel, BorderLayout.CENTER);
        getContentPane().add(pieceInfoPanel, BorderLayout.EAST);

        // set up menus
        JMenu gameMenu = new JMenu("Game");
        JMenuItem item = new JMenuItem("Connect to game...");
        item.addActionListener(
            new ActionListener() {
                /**
                 *  Description of the Method
                 *
                 * @param  e  Description of Parameter
                 */
                public void actionPerformed(ActionEvent e) {
                    connectToGame();
                }
            });
        item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C,
                ActionEvent.ALT_MASK));
        gameMenu.add(item);
        item = new JMenuItem("Disconnect");
        item.addActionListener(
            new ActionListener() {
                /**
                 *  Description of the Method
                 *
                 * @param  e  Description of Parameter
                 */
                public void actionPerformed(ActionEvent e) {
                    disconnectFromGame();
                }
            });
        gameMenu.add(item);
        gameMenu.addSeparator();
        item = new JCheckBoxMenuItem("Use ghost units", useGhosts);
        item.addItemListener(
            new ItemListener() {
                /**
                 *  Description of the Method
                 *
                 * @param  e  Description of Parameter
                 */
                public void itemStateChanged(ItemEvent e) {
                    if (e.getStateChange() == ItemEvent.SELECTED) {
                        useGhosts = true;
                    } else {
                        useGhosts = false;
                    }
                }
            });
        gameMenu.add(item);
        gameMenu.addSeparator();
        item = new JMenuItem("Quit");
        item.addActionListener(
            new ActionListener() {
                /**
                 *  Description of the Method
                 *
                 * @param  e  Description of Parameter
                 */
                public void actionPerformed(ActionEvent e) {
                    quit();
                }
            });
        item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q,
                ActionEvent.ALT_MASK));
        gameMenu.add(item);

        JMenu infoMenu = new JMenu("Info");
        item = new JMenuItem("About...");
        item.addActionListener(
            new ActionListener() {
                /**
                 *  Description of the Method
                 *
                 * @param  e  Description of Parameter
                 */
                public void actionPerformed(ActionEvent e) {
                    GameInfo.display(Game.this);
                }
            });
        item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_I,
                ActionEvent.ALT_MASK));
        infoMenu.add(item);

        JMenuBar menubar = new JMenuBar();
        menubar.add(gameMenu);
        menubar.add(infoMenu);
        setJMenuBar(menubar);

        // set up window
        setTitle(gameTitle);
        setLocation(100, 100);
        pack();
        addWindowListener(
            new WindowAdapter() {
                /**
                 *  Description of the Method
                 *
                 * @param  e  Description of Parameter
                 */
                public void windowClosing(WindowEvent e) {
                    quit();
                }
            });
    }


    /**
     *  Returns the piece at the given point of the board, or null if no such
     *  piece exists.
     *
     * @param  point  Description of Parameter
     * @return        The PieceAt value
     */
    public Piece getPieceAt(Point point) {
        Iterator iter = pieces.values().iterator();
        Piece piece = null;
        while (iter.hasNext()) {
            Piece p = (Piece) iter.next();
            Rectangle r = board.getPieceBounds(p);
            if (r.contains(point)) {
                piece = p;
            }
        }
        return piece;
    }


    /**
     *  Returns all pieces of the game that intersect the given rectangle. The
     *  rectangle is in actual screen coordinates, relative to the component of
     *  the board where the map and the pieces are painted.
     *
     * @param  rect  Description of Parameter
     * @return       The PiecesIntersecting value
     */
    public Enumeration getPiecesIntersecting(Rectangle rect) {
        Iterator all = pieces.values().iterator();
        Vector intersecting = new Vector();
        while (all.hasNext()) {
            Piece p = (Piece) all.next();
            Rectangle r = board.getPieceBounds(p);
            if (r.intersects(rect)) {
                intersecting.addElement(p);
            }
        }
        return intersecting.elements();
    }


    /**
     *  Returns the preferred center of the board. This is the center of the
     *  current screen or, if there is a selected piece, the position of the
     *  selected piece. The preferred center is the center around which the
     *  board will zoom in.
     *
     * @return    The PreferredCenter value
     */
    public Position getPreferredCenter() {
        if (selectedPiece == null) {
            return board.getPositionAt(new Point(Board.WIDTH / 2,
                    Board.HEIGHT / 2));
        } else {
            return selectedPiece.getPosition();
        }
    }


    /**
     *  Returns the positions of the queued actions (and the currently executing
     *  action) of the selected piece, or NULL if no piece is selected. The
     *  position of an action is the position of its argument (if any). The
     *  collection points is started with the position of the selected piece.
     *
     * @return    The SelectedPiecePositions value
     */
    public Enumeration getSelectedPiecePositions() {
        if (selectedPiece == null) {
            return null;
        }
        Vector positions = new Vector();

        // first, the position of the selected piece.
        positions.add(selectedPiece.getPosition());

        // executing action
        Action ea = selectedPiece.getExecutingAction();
        if (ea != null) {
            if (ea.getParameterType() == Action.POSITION) {
                Position p = (Position) ea.getParameter();
                positions.add(new Position(p));
            } else if (ea.getParameterType() == Action.PIECE) {
                Piece p = (Piece) ea.getParameter();
                positions.add(new Position(p.getPosition()));
            }
        }

        // queued actions
        Enumeration actions = selectedPiece.getAllQueuedActions().elements();
        while (actions.hasMoreElements()) {
            Action a = (Action) actions.nextElement();
            if (a.getParameterType() == Action.POSITION) {
                Position p = (Position) a.getParameter();
                positions.add(new Position(p));
            } else if (a.getParameterType() == Action.PIECE) {
                Piece p = (Piece) a.getParameter();
                positions.add(new Position(p.getPosition()));
            }
        }
        return positions.elements();
    }


    /*
     * --------------------------------------------------------
     * Client interface methods. For comments, see Client.java.
     * --------------------------------------------------------
     */

    /**
     *  Description of the Method
     *
     * @param  points  Description of Parameter
     * @param  colors  Description of Parameter
     */
    public void mapUpdate(Point[] points, int[] colors) {
        int size = Math.min(points.length, colors.length);
        for (int i = 0; i < size; ++i) {
            board.mapUpdate(points[i], colors[i]);
        }
        board.repaintBoard();
    }


    /**
     *  Description of the Method
     *
     * @param  p      Description of Parameter
     * @param  color  Description of Parameter
     */
    public void mapUpdate(Point p, int color) {
        board.mapUpdate(p, color);
        board.repaintBoard();
    }


    /**
     *  Description of the Method
     *
     * @param  points  Description of Parameter
     */
    public void invisibilizeMapPoints(Point[] points) {
        for (int i = 0; i < points.length; ++i) {
            board.invisibilizeMapPoint(points[i]);
        }
        board.repaintBoard();
    }


    /**
     *  Description of the Method
     *
     * @param  p  Description of Parameter
     */
    public void invisibilizeMapPoint(Point p) {
        board.invisibilizeMapPoint(p);
        board.repaintBoard();
    }


    /**
     *  Description of the Method
     *
     * @param  handle   Description of Parameter
     * @param  message  Description of Parameter
     */
    public void unitMessage(int handle, String message) {
        Piece p = (Piece) pieces.get(new Integer(handle));
        if (p == null) {
            return;
        }
        p.addMessage(message);
        if (p == selectedPiece) {
            pieceInfoPanel.updateMessageDisplay();
        }
    }


    /**
     *  Adds the specified piece, as described in Client.java. If there is a
     *  ghost piece with the same type and position, that ghost piece will be
     *  removed and replaced with the new piece. If the ghost is selected, the
     *  selection will carry over to the new piece.
     *
     * @param  handle      The feature to be added to the Piece attribute
     * @param  name        The feature to be added to the Piece attribute
     * @param  pos         The feature to be added to the Piece attribute
     * @param  actions     The feature to be added to the Piece attribute
     * @param  properties  The feature to be added to the Piece attribute
     * @param  type        The feature to be added to the Piece attribute
     */
    public void addPiece(int handle, String name, Position pos,
            Enumeration actions, Enumeration properties,
            String type) {
        Piece p = new Piece(handle, pos, name, type);
        pieces.put(new Integer(p.getHandle()), p);
        if (actions != null) {
            while (actions.hasMoreElements()) {
                p.addAction((Action) actions.nextElement());
            }
        }
        if (properties != null) {
            while (properties.hasMoreElements()) {
                p.setProperty((Property) properties.nextElement());
            }
        }

        // find any ghost piece at the same location and with the same type
        Iterator pieces = this.pieces.values().iterator();
        while (pieces.hasNext()) {
            Piece q = (Piece) pieces.next();
            if ((q.getPosition().equals(pos))
                     && (q instanceof Ghost)
                     && (q.getType().equals(type))) {
                if (selectedPiece == q) {
                    selectPiece(p);
                }
                pieces.remove();
            }
        }

        board.repaintBoard();
    }


    /**
     *  Description of the Method
     *
     * @param  handle         Description of Parameter
     * @param  newProperties  Description of Parameter
     */
    public void updatePiece(int handle, Property[] newProperties) {
        Piece p = (Piece) pieces.get(new Integer(handle));
        if (p == null) {
            return;
        }
        for (int i = 0; i < newProperties.length; ++i) {
            p.setProperty(newProperties[i]);
        }
        if (p == selectedPiece) {
            pieceInfoPanel.updatePropertyDisplay();
        }
    }


    /**
     *  Description of the Method
     *
     * @param  handle       Description of Parameter
     * @param  newPosition  Description of Parameter
     */
    public void updatePiece(int handle, Position newPosition) {
        Piece p = (Piece) pieces.get(new Integer(handle));
        if (p == null) {
            return;
        }
        p.setPosition(newPosition);
        board.repaintBoard();
    }


    /**
     *  Description of the Method
     *
     * @param  handle  Description of Parameter
     */
    public void removePiece(int handle) {
        Piece p = (Piece) pieces.get(new Integer(handle));
        if (p == null) {
            return;
        }
        Piece ghost = addGhostPiece(p.getPosition(), p.getType());
        if (selectedPiece == p) {
            selectPiece(ghost);
        }
        destroyPiece(handle, false);
        board.repaintBoard();
    }


    /**
     *  Description of the Method
     *
     * @param  handle  Description of Parameter
     */
    public void destroyPiece(int handle) {
        destroyPiece(handle, true);
    }


    /**
     *  Adds a feature to the PieceAction attribute of the Game object
     *
     * @param  handle  The feature to be added to the PieceAction attribute
     * @param  a       The feature to be added to the PieceAction attribute
     */
    public void addPieceAction(int handle, Action a) {
        Piece p = (Piece) pieces.get(new Integer(handle));
        if (p == null) {
            return;
        }
        p.addAction(a);
    }


    /**
     *  Description of the Method
     *
     * @param  handle  Description of Parameter
     * @param  a       Description of Parameter
     */
    public void removePieceAction(int handle, Action a) {
        Piece p = (Piece) pieces.get(new Integer(handle));
        if (p == null) {
            return;
        }
        p.removeAction(a);
    }


    /**
     *  Description of the Method
     *
     * @param  id       Description of Parameter
     * @param  ok       Description of Parameter
     * @param  message  Description of Parameter
     */
    public void actionReply(long id, boolean ok, String message) {
        Piece p = (Piece) actionExecutingPieces.get(new Long(id));
        if (p == null) {
            // instantaneous actions are caught here
            return;
        }
        if ((message != null) && (!message.equals(""))) {
            unitMessage(p.getHandle(), message);
        }
        actionExecutingPieces.remove(new Long(id));
        actionIds.remove(new Integer(p.getHandle()));
        if (ok) {
            executeNextAction(p);
        } else {
            p.abortAllExecutingActions();
        }
        if (p == selectedPiece) {
            pieceInfoPanel.updateActionDisplay();
        }
        board.repaintBoard();
    }


    /**
     *  Description of the Method
     */
    public void connectionLost() {
        Toolkit.getDefaultToolkit().beep();
        gecco.client.dialogs.Error.display("Connection to server lost. "
                 + "Please try to connect again.", this);
        disconnectFromGame();
    }


    /**
     *  Description of the Method
     *
     * @param  progress  Description of Parameter
     */
    public void mapLoadingProgressUpdate(double progress) {
        LoadingInfo.update(progress);
    }


    /*
     * -----------------------------------------------------------
     * Animator interface methods. For comments, see Animator.java
     * -----------------------------------------------------------
     */

    /**
     *  Description of the Method
     *
     * @param  p  Description of Parameter
     */
    public void repaintAnimation(AnimatedPiece p) {
        board.repaintBoard();
    }


    /**
     *  Description of the Method
     *
     * @param  p  Description of Parameter
     */
    public void animationDone(AnimatedPiece p) {
        destroyPiece(p.getHandle(), false);
    }


    /*
     * ------------------------------
     * External game control methods.
     * ------------------------------
     */

    /**
     *  Aborts the currently executing action of the given piece. Optionally,
     *  all queued actions are aborted, if not the next action in queue is
     *  executed.
     *
     * @param  p         The piece.
     * @param  abortAll  Indiactes whether or not all executing actions should
     *      be aborted.
     */
    public void abortExecutingAction(Piece p, boolean abortAll) {
        if (p == null) {
            return;
        }
        Long id = (Long) actionIds.get(new Integer(p.getHandle()));
        if (id == null) {
            // piece has executing action
            return;
        }
        server.abortAction(id.longValue());
        actionExecutingPieces.remove(id);
        actionIds.remove(new Integer(p.getHandle()));
        if (abortAll) {
            p.abortAllExecutingActions();
        } else {
            executeNextAction(p);
        }
        if (p == selectedPiece) {
            pieceInfoPanel.updateActionDisplay();
        }
        board.repaintBoard();
    }


    /**
     *  Aborts the currently executing action of the given piece. The next
     *  action in queue is then executed.
     *
     * @param  p  Description of Parameter
     */
    public void abortExecutingAction(Piece p) {
        abortExecutingAction(p, false);
    }


    /**
     *  Returns the menu that displays all ghost pieces that are available. Sets
     *  up the behavior so that the selected ghost is placed on the board.
     *
     * @return    The GhostMenu value
     */
    JPopupMenu getGhostMenu() {
        JPopupMenu ghostMenu = new JPopupMenu();
        Enumeration e = Symbols.getAllPieceTypes();
        while (e.hasMoreElements()) {
            String type = (String) e.nextElement();
            Image symbol = Symbols.getPieceSymbol(type);
            JMenuItem item = new JMenuItem(type, new ImageIcon(symbol));
            item.setActionCommand(type);
            item.addActionListener(
                new ActionListener() {
                    /**
                     *  Description of the Method
                     *
                     * @param  e  Description of Parameter
                     */
                    public void actionPerformed(ActionEvent e) {
                        String pieceType = e.getActionCommand();
                        Position position =
                                board.getPositionAt(ghostMenuClick);
                        addGhostPiece(position, pieceType);
                    }
                });
            ghostMenu.add(item);
        }
        return ghostMenu;
    }


    /**
     *  Returns the menu that contains actions for the ghosts.
     *
     * @return    The GhostActionMenu value
     */
    JPopupMenu getGhostActionMenu() {
        if (ghostActionMenu != null) {
            return ghostActionMenu;
        }
        ghostActionMenu = new JPopupMenu();
        JMenuItem item = new JMenuItem("Remove ghost");
        item.addActionListener(
            new ActionListener() {
                /**
                 *  Description of the Method
                 *
                 * @param  e  Description of Parameter
                 */
                public void actionPerformed(ActionEvent e) {
                    if ((selectedPiece != null) &&
                            (selectedPiece instanceof Ghost)) {
                        destroyPiece(selectedPiece.getHandle(),
                                false);
                    }
                }
            });
        ghostActionMenu.add(item);
        return ghostActionMenu;
    }


    /*
     * ------------------------------
     * Internal game control methods.
     * ------------------------------
     */

    /**
     *  Description of the Method
     *
     * @param  p  Description of Parameter
     */
    void executeNextAction(Piece p) {
        if (p == null) {
            return;
        }
        Action nextAction = p.executeNextAction();
        if (nextAction == null) {
            return;
        }
        long nextId = server.doAction(p.getHandle(), nextAction);
        actionExecutingPieces.put(new Long(nextId), p);
        actionIds.put(new Integer(p.getHandle()), new Long(nextId));
        if (selectedPiece == p) {
            pieceInfoPanel.updateActionDisplay();
        }
    }


    /**
     *  Description of the Method
     *
     * @param  p  Description of Parameter
     */
    void selectPiece(Piece p) {
        if (selectedPiece != null) {
            selectedPiece.setSelected(false);
        }
        if (p != null) {
            p.setSelected(true);
        }
        selectedPiece = p;
        unselectAction();
        pieceInfoPanel.setPiece(selectedPiece);
        board.repaintBoard();
    }


    /**
     *  Description of the Method
     */
    void unselectPiece() {
        selectPiece(null);
    }


    /**
     *  Description of the Method
     *
     * @param  a  Description of Parameter
     */
    void selectAction(Action a) {
        selectedAction = a;
        board.setBoardCursor(new Cursor(Cursor.CROSSHAIR_CURSOR));
        if (a == null) {
            board.setBoardCursor(new Cursor(Cursor.DEFAULT_CURSOR));
        }
    }


    /**
     *  Description of the Method
     */
    void unselectAction() {
        selectAction(null);
    }


    /**
     *  Removes the piece with the given handle (if any) from the game.
     *
     * @param  animateExplosion  Should there be an explosion?
     * @param  handle            Description of Parameter
     */
    void destroyPiece(int handle, boolean animateExplosion) {
        Piece p = (Piece) pieces.remove(new Integer(handle));
        if (p == null) {
            return;
        }
        if (animateExplosion) {
            --lastExtraPieceId;
            Explosion explosion = new Explosion(lastExtraPieceId,
                    p.getPosition(),
                    p.getName(), p.getType(),
                    this);
            pieces.put(new Integer(explosion.getHandle()), explosion);
            explosion.startAnimation();
        }
        if (selectedPiece == p) {
            unselectPiece();
        }
        board.repaintBoard();
    }


    /**
     *  Adds a ghost piece with the specified properties. The newly created
     *  ghost symbol is returned. If there is already a ghost piece with the
     *  same type at the exact same location, no ghost will be added. In the
     *  latter case, the existing ghost piece is returned. In the ghost
     *  functionality is disabled (useGhosts flag), nothing is done and null is
     *  returned.
     *
     * @param  pos   The feature to be added to the GhostPiece attribute
     * @param  type  The feature to be added to the GhostPiece attribute
     * @return       Description of the Returned Value
     */
    Piece addGhostPiece(Position pos, String type) {
        if (!useGhosts) {
            return null;
        }

        // make sure there is no already a similar ghost piece there
        Iterator pieces = this.pieces.values().iterator();
        while (pieces.hasNext()) {
            Piece p = (Piece) pieces.next();
            if ((p.getPosition().equals(pos))
                     && (p instanceof Ghost)
                     && (p.getType().equals(type))) {
                return p;
            }
        }

        // create a new ghost piece and put it there
        --lastExtraPieceId;
        Ghost ghost = new Ghost(lastExtraPieceId, pos, "Ghost", type);
        this.pieces.put(new Integer(ghost.getHandle()), ghost);
        board.repaintBoard();
        return ghost;
    }


    /**
     *  Creates the menu that displays the actions of a piece. The appropriate
     *  behavior of the menu is set up.
     *
     * @param  p  Description of Parameter
     * @return    Description of the Returned Value
     */
    JPopupMenu createActionMenu(Piece p) {
        JPopupMenu menu = new JPopupMenu();
        Vector actionMenuItems = new Vector();

        Enumeration actions = p.getActions();
        while (actions.hasMoreElements()) {
            Action a = (Action) actions.nextElement();
            ActionMenuItem item = new ActionMenuItem(a);
            item.addActionListener(actionMenuListener);
            actionMenuItems.add(item);
        }

        if (actionMenuItems.size() <= 0) {
            JMenuItem noActions = new JMenuItem("No actions available");
            noActions.setEnabled(false);
            menu.add(noActions);
        } else {
            JMenuItem stopAction = new JMenuItem("Stop ongoing action");
            stopAction.addActionListener(
                new ActionListener() {
                    /**
                     *  Description of the Method
                     *
                     * @param  e  Description of Parameter
                     */
                    public void actionPerformed(ActionEvent e) {
                        abortExecutingAction(selectedPiece);
                    }
                });
            if ((selectedPiece != null)
                     && (selectedPiece.getExecutingAction() == null)) {
                stopAction.setEnabled(false);
            }
            menu.add(stopAction);
            menu.addSeparator();
            Enumeration e = actionMenuItems.elements();
            while (e.hasMoreElements()) {
                menu.add((JMenuItem) e.nextElement());
            }
        }

        return menu;
    }


    /**
     *  Description of the Method
     */
    void quit() {
        disconnectFromGame();
        board = null;
        System.exit(0);
    }


    /**
     *  Description of the Method
     */
    void disconnectFromGame() {
        // abort all actions of all pieces
        Iterator i = pieces.values().iterator();
        while (i.hasNext()) {
            Piece p = (Piece) i.next();
            abortExecutingAction(p, true);
        }

        // disconnect and refresh layout etc.
        server.disconnect();
        unselectPiece();
        pieceInfoPanel.setPiece(null);
        pieces.clear();
        actionExecutingPieces = new Hashtable();
        actionIds = new Hashtable();
        board.clearMap();
        board.repaintBoard();
        setTitle(gameTitle);
    }


    /**
     *  Description of the Method
     */
    void connectToGame() {
        // connect to server
        if (lastHost == null) {
            lastHost = new HostPort("somecomputer.example.com", 7000);
        }
        HostPort host =
                ConnectionDialog.showDialog(this, lastHost.getHost(), lastHost.getPort());
        if (host == null) {
            return;
        }
        lastHost = new HostPort(host);
        disconnectFromGame();
        boolean couldConnect = server.connect(host.getHost(),
                host.getPort());
        if (!couldConnect) {
            gecco.client.dialogs.Error.display("Could not connect to that server.",
                    this);
            return;
        }

        // select role
        StringTokenizer roles = server.getAvailableRoles();
        String[] roleArray = new String[roles.countTokens()];
        for (int i = 0; i < roleArray.length; ++i) {
            roleArray[i] = roles.nextToken();
        }
        String role = RoleSelectionDialog.showDialog(this, roleArray);
        if (role == null) {
            server.disconnect();
            return;
        }

        // join as role
        boolean roleOK = server.joinAsRole(role);
        if (!roleOK) {
            gecco.client.dialogs.Error.display("Could not use the role " + role
                     + ". Please try again.", this);
            server.disconnect();
            return;
        }

        // load map
        map = new Map(server.getMapSize());
        Thread t =
            new Thread() {
                /**
                 *  Main processing method for the Game object
                 */
                public void run() {
                    map.loadMap(server.getMap());
                    LoadingInfo.hide();
                }
            };
        t.start();
        LoadingInfo.display("Loading map, please wait...", this);
        Point mapUpperLeft = server.getMapUpperLeft();

        // launch game
        setTitle(gameTitle + " - " + role);
        board.loadMap(map, mapUpperLeft);
        server.startGame();
        board.repaintBoard();
    }


    /**
     *  Main method for program start.
     *
     * @param  argv  The command line arguments
     */
    public static void main(String[] argv) {
        Game game = new Game("GECCO");
        game.setVisible(true);
        game.connectToGame();
    }


    /**
     *  A menu item of the action menu.
     *
     * @author     d98-kha
     * @created    June 27, 2001
     */
    class ActionMenuItem extends JMenuItem {
        /**
         *  Description of the Field
         */
        Action action;


        /**
         *  Constructor for the ActionMenuItem object
         *
         * @param  a  Description of Parameter
         */
        public ActionMenuItem(Action a) {
            super(a.getName());
            action = a;
        }


        /**
         *  Gets the MyAction attribute of the ActionMenuItem object
         *
         * @return    The MyAction value
         */
        public Action getMyAction() {
            return (Action) action.clone();
        }
    }
}
