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

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 java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.net.*;
import java.util.*;
import javax.swing.*;

/**
 *  The board is what is displayed to the user of the game. It paints the map
 *  and the pieces. The board needs to know of a Game object, whose pieces are
 *  to be painted on the board. <p>
 *
 *  The board contains scrollbars used for scrolling when the map does not fit
 *  the screen. It also contain zoom buttons for zooming in or out on the board.
 *  The zoom buttons use some gif images in the icons catalog (zoomin.gif and
 *  zoomout.gif). <p>
 *
 *  All painting is done in an offscreen buffer and is then copied to the
 *  screen. Use method repaintBoard, to repaint the board to reflect changes in
 *  the game.
 *
 * @author     Andreas Enblom
 * @created    June 27, 2001
 */
public class Board extends JPanel {

    /**
     *  Description of the Field
     */
    Game game;
    /**
     *  Description of the Field
     */
    JComponent board;
    /**
     *  Description of the Field
     */
    JScrollBar horizontalScrollbar, verticalScrollbar;
    /**
     *  Description of the Field
     */
    AdjustmentListener horScrollListener, vertScrollListener;
    /**
     *  Description of the Field
     */
    JButton zoomInButton, zoomOutButton;

    /**
     *  Description of the Field
     */
    Image offscreenBuffer;
    /**
     *  Description of the Field
     */
    Point boardUpperLeft = new Point(0, 0);
    /**
     *  Description of the Field
     */
    double boardWidth = 1;
    /**
     *  Description of the Field
     */
    double boardHeight = 1;
    /**
     *  Description of the Field
     */
    int zoomFactor = 1;
    // always a power of two

            /**
     *  Description of the Field
     */
    Map map;
    /**
     *  Description of the Field
     */
    Point mapUpperLeft = new Point(0, 0);
    /**
     *  Description of the Field
     */
    Dimension mapSize = new Dimension(1, 1);
    /**
     *  The width of the area on which the map and the pieces are painted.
     */
    public final static int WIDTH = 400;
    /**
     *  The height of the area on which the map and the pieces are painted.
     */
    public final static int HEIGHT = 400;


    /**
     * @param  game  The Game object whose pieces are supposed to be painted on
     *      this board.
     */
    public Board(Game game) {
        this.game = game;

        // offscreen buffer
        offscreenBuffer = new BufferedImage(Board.WIDTH, Board.HEIGHT,
                BufferedImage.TYPE_3BYTE_BGR);
        Graphics gr = offscreenBuffer.getGraphics();
        gr.setColor(Color.white);
        gr.fillRect(0, 0, Board.WIDTH, Board.HEIGHT);
        gr.setColor(Color.black);

        // board
        board =
            new JComponent() {
                /**
                 *  Gets the PreferredSize attribute of the Board object
                 *
                 * @return    The PreferredSize value
                 */
                public Dimension getPreferredSize() {
                    return new Dimension(Board.WIDTH, Board.HEIGHT);
                }


                /**
                 *  Gets the MaximumSize attribute of the Board object
                 *
                 * @return    The MaximumSize value
                 */
                public Dimension getMaximumSize() {
                    return getPreferredSize();
                }


                /**
                 *  Gets the MinimumSize attribute of the Board object
                 *
                 * @return    The MinimumSize value
                 */
                public Dimension getMinimumSize() {
                    return getPreferredSize();
                }


                /**
                 *  Description of the Method
                 *
                 * @param  g  Description of Parameter
                 */
                public void paint(Graphics g) {
                    repaintBoardArea(new Rectangle(0, 0, Board.WIDTH,
                            Board.HEIGHT));
                    if (offscreenBuffer != null) {
                        g.drawImage(offscreenBuffer, 0, 0, this);
                    }
                }
            };
        board.setBackground(Color.white);

        // scrollbars
        horizontalScrollbar = new JScrollBar(Scrollbar.HORIZONTAL);
        horScrollListener =
            new AdjustmentListener() {
                /**
                 *  Description of the Method
                 *
                 * @param  e  Description of Parameter
                 */
                public void adjustmentValueChanged(AdjustmentEvent e) {
                    int offset = e.getValue() - boardUpperLeft.x;
                    boardUpperLeft.translate(offset, 0);
                    map.setVisibleArea(new Point(boardUpperLeft.x -
                            mapUpperLeft.x,
                            boardUpperLeft.y -
                            mapUpperLeft.y),
                            zoomFactor,
                            new Dimension((int)
                            Math.ceil(boardWidth),
                            (int)
                            Math.ceil(boardHeight)));
                    board.repaint();
                }
            };

        verticalScrollbar = new JScrollBar(Scrollbar.VERTICAL);
        vertScrollListener =
            new AdjustmentListener() {
                /**
                 *  Description of the Method
                 *
                 * @param  e  Description of Parameter
                 */
                public void adjustmentValueChanged(AdjustmentEvent e) {
                    int offset = e.getValue() - boardUpperLeft.y;
                    boardUpperLeft.translate(0, offset);
                    map.setVisibleArea(new Point(boardUpperLeft.x -
                            mapUpperLeft.x,
                            boardUpperLeft.y -
                            mapUpperLeft.y),
                            zoomFactor,
                            new Dimension((int)
                            Math.ceil(boardWidth),
                            (int)
                            Math.ceil(boardHeight)));
                    board.repaint();
                }
            };

        // zoom buttons
        zoomInButton =
            new JButton() {
                /**
                 *  Gets the PreferredSize attribute of the Board object
                 *
                 * @return    The PreferredSize value
                 */
                public Dimension getPreferredSize() {
                    int size = this.getMinimumSize().height;
                    return new Dimension(size, size);
                }
            };
        zoomInButton.setIcon(new ImageIcon(
                ClassLoader.getSystemClassLoader().
                getResource("gecco/client/icons/zoomin.gif")));
        zoomInButton.addActionListener(
            new ActionListener() {
                /**
                 *  Description of the Method
                 *
                 * @param  e  Description of Parameter
                 */
                public void actionPerformed(ActionEvent e) {
                    zoom(false);
                    board.repaint();
                }
            });
        zoomOutButton =
            new JButton() {
                /**
                 *  Gets the PreferredSize attribute of the Board object
                 *
                 * @return    The PreferredSize value
                 */
                public Dimension getPreferredSize() {
                    int size = this.getMinimumSize().height;
                    return new Dimension(size, size);
                }
            };
        zoomOutButton.setIcon(new ImageIcon(
                ClassLoader.getSystemClassLoader().
                getResource("gecco/client/icons/zoomout.gif")));
        zoomOutButton.addActionListener(
            new ActionListener() {
                /**
                 *  Description of the Method
                 *
                 * @param  e  Description of Parameter
                 */
                public void actionPerformed(ActionEvent e) {
                    zoom(true);
                    board.repaint();
                }
            });

        // layout
        Box lower = new Box(BoxLayout.X_AXIS);
        zoomInButton.setAlignmentY(Component.TOP_ALIGNMENT);
        zoomOutButton.setAlignmentY(Component.TOP_ALIGNMENT);
        horizontalScrollbar.setAlignmentY(Component.TOP_ALIGNMENT);
        lower.add(zoomInButton);
        lower.add(zoomOutButton);
        lower.add(horizontalScrollbar);
        JPanel p = new JPanel(new BorderLayout());
        p.add(board, BorderLayout.CENTER);
        p.add(verticalScrollbar, BorderLayout.EAST);
        p.add(lower, BorderLayout.SOUTH);
        setLayout(new BorderLayout());
        add(p, BorderLayout.NORTH);
        add(Box.createVerticalGlue(), BorderLayout.CENTER);
    }


    /**
     *  Set the cursor for the map area of the board.
     *
     * @param  cursor  The new BoardCursor value
     */
    public void setBoardCursor(Cursor cursor) {
        board.setCursor(cursor);
    }


    /**
     *  Converts a screen point to the Position corresponding to that point.
     *  Things like zoom factor and scrollbar position are taken into account.
     *
     * @param  p  Description of Parameter
     * @return    The PositionAt value
     */
    public Position getPositionAt(Point p) {
        double xscale = boardWidth / (double) WIDTH;
        double yscale = boardHeight / (double) HEIGHT;
        double x = (p.x * xscale) + boardUpperLeft.x;
        double y = (p.y * yscale) + boardUpperLeft.y;
        return new Position(x, y);
    }


    /**
     *  Converts the given Position of the map to the actual screen Point in the
     *  map-and-pieces component's coordinate system.
     *
     * @param  p  Description of Parameter
     * @return    The ScreenPoint value
     */
    public Point getScreenPoint(Position p) {
        double xscale = (double) WIDTH / boardWidth;
        double yscale = (double) HEIGHT / boardHeight;
        double x = (p.x - boardUpperLeft.x) * xscale;
        double y = (p.y - boardUpperLeft.y) * yscale;
        return new Point((int) Math.rint(x), (int) Math.rint(y));
    }


    /**
     *  Returns the bounding rectangle of the given piece. The rectangle is
     *  given in actual screen coordinates (i.e. the coordinates of the
     *  map-and-pieces component).
     *
     * @param  piece  Description of Parameter
     * @return        The PieceBounds value
     */
    public Rectangle getPieceBounds(Piece piece) {
        Image symbol = piece.getSymbol();
        if (symbol == null) {
            return new Rectangle(0, 0, 1, 1);
        }
        int width = symbol.getWidth(board);
        int height = symbol.getHeight(board);
        Point p = getScreenPoint(piece.getPosition());
        int x = p.x;
        int y = p.y;
        return new Rectangle(x - width / 2, y - height / 2, width, height);
    }


    /**
     *  Sets the map of the board with the given point as upper left corner.
     *
     * @param  m             Description of Parameter
     * @param  mapUpperLeft  Description of Parameter
     */
    public void loadMap(Map m, Point mapUpperLeft) {
        map = m;
        this.mapUpperLeft = new Point(mapUpperLeft);
        mapSize = new Dimension(m.getSize());
        boardUpperLeft = new Point(mapUpperLeft);
        boardWidth = WIDTH;
        boardHeight = HEIGHT;
        zoomFactor = 1;
        zoom(0);
    }


    /**
     *  Description of the Method
     */
    public void clearMap() {
        if (map == null) {
            return;
        }
        map.clear();
        zoomInButton.setEnabled(false);
        zoomOutButton.setEnabled(false);
    }


    /**
     *  Updates the given point of the map to the given color. Colors are given
     *  as integers as decribed in java.awt.Color.
     *
     * @param  p      Description of Parameter
     * @param  color  Description of Parameter
     */
    public void mapUpdate(Point p, int color) {
        if (map == null) {
            return;
        }
        Point updatePoint = new Point(p);
        updatePoint.translate(-mapUpperLeft.x, -mapUpperLeft.y);
        map.updatePoint(updatePoint, color);
    }


    /**
     *  Makes the given point of the map invisible. If it has already been set
     *  as invisible, nothing is done.
     *
     * @param  p  Description of Parameter
     */
    public void invisibilizeMapPoint(Point p) {
        if (map == null) {
            return;
        }
        Point updatePoint = new Point(p);
        updatePoint.translate(-mapUpperLeft.x, -mapUpperLeft.y);
        map.invisibilizePoint(updatePoint);
    }


    /**
     *  Adds the given mouse listener the the area where the map and the pieces
     *  are painted. Use this method to assign the preferred behavior to the
     *  board (such as mouse clicks for selecting pieces).
     *
     * @param  ml  The feature to be added to the BoardMouseListener attribute
     */
    public void addBoardMouseListener(MouseListener ml) {
        board.addMouseListener(ml);
    }


    /**
     *  Adds the given MouseMotionListener to the area of the board where the
     *  map and the pieces are painted.
     *
     * @param  mml  The feature to be added to the BoardMouseMotionListener
     *      attribute
     */
    public void addBoardMouseMotionListener(MouseMotionListener mml) {
        board.addMouseMotionListener(mml);
    }


    /**
     *  Repaints the part of the board where the map and pieces are.
     */
    public void repaintBoard() {
        board.repaint();
    }


    /**
     *  Zooms in or out on the board.
     *
     * @param  out  Indicates whether or not the board should be zoomed out.
     */
    public void zoom(boolean out) {
        int magnification = (int) Math.rint(Math.log(zoomFactor) / Math.log(2));
        if (out) {
            zoom(magnification - 1);
        } else {
            zoom(magnification + 1);
        }
    }


    /**
     *  Zoom the board to the given magnification. If the magnification is zero
     *  the board is initilized the proper values according to the map.
     *
     * @param  magnification  The magnification; zero is no magnification and
     *      n+1 is twice as much as n.
     */
    public void zoom(int magnification) {
        int oldZoomFactor = zoomFactor;
        zoomFactor = (int) Math.pow(2, magnification);
        int maxZoomFactor = Math.min(WIDTH, HEIGHT);
        zoomFactor = Math.min(maxZoomFactor, zoomFactor);
        zoomFactor = Math.max(1, zoomFactor);
        zoomOutButton.setEnabled(zoomFactor > 1);
        zoomInButton.setEnabled(zoomFactor < maxZoomFactor);

        // set board location and size
        double newBoardWidth = (double) WIDTH * (1.0 / (double) zoomFactor);
        double newBoardHeight = (double) HEIGHT * (1.0 / (double) zoomFactor);
        Position center = game.getPreferredCenter();
        int newX = (int) Math.ceil(center.x - newBoardWidth / 2);
        int newY = (int) Math.ceil(center.y - newBoardHeight / 2);
        newX = Math.min(newX,
                mapUpperLeft.x + mapSize.width
                 - (int) Math.ceil(newBoardWidth));
        newX = Math.max(newX, mapUpperLeft.x);
        newY = Math.min(newY,
                mapUpperLeft.y + mapSize.height
                 - (int) Math.ceil(newBoardHeight));
        newY = Math.max(newY, mapUpperLeft.y);

        boardUpperLeft = new Point(newX, newY);
        boardWidth = newBoardWidth;
        boardHeight = newBoardHeight;

        // fix scrollbars
        horizontalScrollbar.removeAdjustmentListener(horScrollListener);
        horizontalScrollbar.setValues(boardUpperLeft.x, (int) boardWidth,
                mapUpperLeft.x,
                mapUpperLeft.x + mapSize.width);
        horizontalScrollbar.setBlockIncrement((int) boardWidth);
        horizontalScrollbar.addAdjustmentListener(horScrollListener);
        verticalScrollbar.removeAdjustmentListener(vertScrollListener);
        verticalScrollbar.setValues(boardUpperLeft.y, (int) boardHeight,
                mapUpperLeft.y,
                mapUpperLeft.y + mapSize.height);
        verticalScrollbar.setBlockIncrement((int) boardHeight);
        verticalScrollbar.addAdjustmentListener(vertScrollListener);

        map.setVisibleArea(new Point(boardUpperLeft.x - mapUpperLeft.x,
                boardUpperLeft.y - mapUpperLeft.y),
                zoomFactor,
                new Dimension((int) Math.ceil(boardWidth),
                (int) Math.ceil(boardHeight)));
    }


    /**
     *  Repaint the given area of the board. The coordinates are given relative
     *  to the component on which the map and pieces are painted.
     *
     * @param  r  Description of Parameter
     */
    void repaintBoardArea(Rectangle r) {
        if ((offscreenBuffer == null) || (map == null)) {
            return;
        }
        Graphics g = offscreenBuffer.getGraphics();

        // clear the area
        Color oldColor = g.getColor();
        g.setColor(Color.white);
        g.fillRect(r.x, r.y, r.width, r.height);
        g.setColor(oldColor);

        // repaint the map
        Rectangle oldClip = g.getClipBounds();
        g.setClip(r);
        map.paint(g);
        g.setClip(oldClip);

        // draw lines between the queued actions of the selected piece
        Enumeration positions = game.getSelectedPiecePositions();
        if (positions != null) {
            Point last = null;
            while (positions.hasMoreElements()) {
                Position pos = (Position) positions.nextElement();
                Point p = getScreenPoint(pos);
                if (last != null) {
                    g.setColor(Color.red);
                    g.drawLine(last.x, last.y, p.x, p.y);
                    g.setColor(oldColor);
                }
                last = p;
            }
        }

        // repaint all pieces within
        Enumeration pieces = game.getPiecesIntersecting(r);
        while (pieces.hasMoreElements()) {
            Piece p = (Piece) pieces.nextElement();
            p.paint(g, getPieceBounds(p));
        }
    }
}

