package net.sf.rails.ui.swing.hexmap;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Stroke;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.GeneralPath;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import javax.swing.JComponent;
import javax.swing.JLayeredPane;
import javax.swing.SwingUtilities;
import javax.swing.ToolTipManager;

import net.sf.rails.common.Config;
import net.sf.rails.common.parser.ConfigurationException;
import net.sf.rails.game.HexSide;
import net.sf.rails.game.HexSidesSet;
import net.sf.rails.game.MapHex;
import net.sf.rails.game.MapManager;
import net.sf.rails.game.Phase;
import net.sf.rails.game.Tile;
import net.sf.rails.ui.swing.GameUIManager;
import net.sf.rails.ui.swing.ImageLoader;
import net.sf.rails.ui.swing.ORUIManager;
import net.sf.rails.util.Util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;


/**
 * Base class that stores common info for HexMap independant of Hex
 * orientations. The hex map manages several layers. Content is seperated in
 * layers in order to ensure good performance in case of only some aspects of
 * the map need to be redrawn.
 *
 * In order to avert race conditions during layer drawing, the critical code is
 * synchronized on the hex map instance as monitor object.
 */
public abstract class HexMap implements MouseListener, MouseMotionListener {

    private static final Logger log = LoggerFactory.getLogger(HexMap.class);

    /**
     * class for managing sets of rectangles. Apart from several convenience
     * methods, this class aims at keeping the set as minimal as possible.
     */
    private static class RectangleSet {
        private List<Rectangle> rs = ImmutableList.of();

        /**
         * @param rOp Rectangle to be added to the set. Only added if not
         * contained in a rectangle of the set. If added, all of the set's
         * rectangles which are a sub-area of this rectangle are dropped (in
         * order to keep the rectangle list as small as possible).
         */
        public void add(Rectangle rOp) {
            // exit if rectangle already contained in set of rectangles
            for (Rectangle r : rs) {
                if (r.contains(rOp)) return;
            }

            // build new set (do not include rectangles contained by new
            // rectangle)
            ImmutableList.Builder<Rectangle> newRs = ImmutableList.builder();
            for (Rectangle r : rs) {
                if (!rOp.contains(r)) newRs.add(r);
            }
            newRs.add(rOp);
            rs = newRs.build();
        }

        /**
         * As a side-effect, the area defined by the given rectangle is removed
         * from the area defined by the set of rectangles. This might lead to
         * splitting the set's rectangles if only parts of their areas become
         * removed.
         *
         * @return The intersection between the given rectangle and the set of
         * rectangles. Returns null if the intersection is empty.
         */
        public Rectangle getIntersectionAndRemoveFromSet(Rectangle rOp) {
            Rectangle intersection = null;
            RectangleSet newRs = new RectangleSet();
            for (Rectangle r : rs) {
                Rectangle intersectionPart = null;

                // check for the most common case: set's rectangle is a sub-area
                // of the given rectangle (common because repaint creates
                // unions)
                // avoid further (complex) processing for this case
                if (rOp.contains(r)) {
                    intersectionPart = r;
                } else if (r.intersects(rOp)) {
                    // update intersection region
                    intersectionPart = r.intersection(rOp);

                    // adjust rectangle: potentially split into 4 sub-rectangles
                    // ***************************
                    // * | 3 | *
                    // * ************* *
                    // * 1 * rOp * 2 *
                    // * ************* *
                    // * | 4 | *
                    // ***************************

                    // region 1
                    if (r.x < rOp.x && (r.x + r.width) > rOp.x) {
                        newRs.add(new Rectangle(r.x, r.y, (rOp.x - r.x),
                                r.height));
                    }
                    // region 2
                    if ((r.x + r.width) > (rOp.x + rOp.width)
                        && r.x < (rOp.x + rOp.width)) {
                        newRs.add(new Rectangle((rOp.x + rOp.width), r.y,
                                (r.x + r.width - rOp.x - rOp.width), r.height));
                    }
                    // region 3
                    if (r.y < rOp.y) {
                        int x1 = Math.max(r.x, rOp.x);
                        int x2 = Math.min(r.x + r.width, rOp.x + rOp.width);
                        if (x1 < x2)
                            newRs.add(new Rectangle(x1, r.y, x2 - x1, rOp.y
                                                                      - r.y));
                    }
                    // region 4
                    if ((r.y + r.height) > (rOp.y + rOp.height)) {
                        int x1 = Math.max(r.x, rOp.x);
                        int x2 = Math.min(r.x + r.width, rOp.x + rOp.width);
                        if (x1 < x2)
                            newRs.add(new Rectangle(x1, (rOp.y + rOp.height),
                                    x2 - x1,
                                    (r.y + r.height - rOp.y - rOp.height)));
                    }
                }

                if (intersectionPart == null) {
                    // if no intersection part, this rectangle remains unchanged
                    // in the set
                    newRs.add(r);
                } else {
                    // expand the intersection region if intersection part found
                    if (intersection == null) {
                        intersection = (Rectangle) intersectionPart.clone();
                    } else {
                        intersection.add(intersectionPart);
                    }
                }
            }
            rs = newRs.rs;
            return intersection;
        }
    }

    private abstract static class HexLayer extends JComponent {
        private static final long serialVersionUID = 1L;
        protected final HexMap hexMap;

        private BufferedImage bufferedImage;
        /*
         * list of regions for which the layer's image buffer is dirty
         */
        private RectangleSet bufferDirtyRegions = new RectangleSet();

        protected abstract void paintImage(Graphics2D g);

        protected HexLayer(HexMap hexMap) {
            super();
            this.hexMap = hexMap;
        }


        @Override
        public final void repaint() {
            bufferDirtyRegions.add(new Rectangle(0, 0, getWidth(), getHeight()));
            super.repaint();
        }

        @Override
        public void repaint(Rectangle r) {
            bufferDirtyRegions.add(r);
            super.repaint(r);
        }

        @Override
        public final void paintComponent(Graphics g) {
            super.paintComponent(g);

            // avoid that paintComponent is processed concurrently
            synchronized (HexLayer.this) {

                // Abort if called too early or if bounds are invalid.
                Rectangle rectClip = g.getClipBounds();
                if (rectClip == null) return;

                // ensure that image buffer of this layer is valid
                if (bufferedImage == null
                    || bufferedImage.getWidth() != getWidth()
                    || bufferedImage.getHeight() != getHeight()) {
                    // create new buffer image
                    bufferedImage =
                            new BufferedImage(getWidth(), getHeight(),
                                    BufferedImage.TYPE_INT_ARGB);

                    // clear information of the image buffer's dirty regions
                    bufferDirtyRegions = new RectangleSet();

                    bufferDirtyRegions.add(new Rectangle(0, 0, getWidth(),
                            getHeight()));

                    // since the buffered image is empty, it has to be
                    // completely redrawn
                    rectClip = new Rectangle(0, 0, getWidth(), getHeight());
                }

                // determine which parts of the clip are dirty and have to be
                // redrawn
                Rectangle dirtyClipArea =
                        bufferDirtyRegions.getIntersectionAndRemoveFromSet(rectClip);
                if (dirtyClipArea != null) {
                    // buffer redraw is necessary
                    Graphics2D imageGraphics =
                            (Graphics2D) bufferedImage.getGraphics();

                    // apply the clip of the component's repaint to its image
                    // buffer
                    imageGraphics.setClip(dirtyClipArea.x, dirtyClipArea.y,
                            dirtyClipArea.width, dirtyClipArea.height);

                    // set the background to transparent so that only drawn
                    // parts of the
                    // buffer will be taken over
                    imageGraphics.setBackground(new Color(0, 0, 0, 0));
                    imageGraphics.setColor(Color.BLACK);

                    // clear the clip (for a non-virtual graphic, this would
                    // have been
                    // done by super.paintComponent)
                    imageGraphics.clearRect(dirtyClipArea.x, dirtyClipArea.y,
                            dirtyClipArea.width, dirtyClipArea.height);

                    // paint within the buffer
                    paintImage(imageGraphics);

                    imageGraphics.dispose();
                }

                // now buffer is valid and can be used
                BufferedImage bufferedRect =
                        bufferedImage.getSubimage(rectClip.x, rectClip.y,
                                rectClip.width, rectClip.height);
                g.drawImage(bufferedRect, rectClip.x, rectClip.y, null);
            }
        }
    }

    /**
     * Layer containing tiles
     */
    private static class TilesLayer extends HexLayer {
        private static final long serialVersionUID = 1L;

        private TilesLayer(HexMap hexMap) {
            super(hexMap);
        }

        @Override
        public void paintImage(Graphics2D g) {
            try {
                // Paint tiles
                for (GUIHex hex:hexMap.getHexes()) {
                    Rectangle hexrect = hex.getBounds();

                    if (g.hitClip(hexrect.x, hexrect.y, hexrect.width,
                            hexrect.height)) {
                        hex.paintTile(g);
                    }
                }

                // Paint the impassability bars
                for (GUIHex hex:hexMap.getHexes()) {
                    Rectangle hexrect = hex.getBounds();

                    if (g.hitClip(hexrect.x, hexrect.y, hexrect.width,
                            hexrect.height)) {
                        hex.paintBars(g);
                    }
                }

            } catch (NullPointerException ex) {
                // If we try to paint before something is loaded, just retry
                // later.
                log.debug("Premature call to TilesLayer.paintImage(Graphics g)");
            }
        }
    }

    /**
     * Layer containing visualization of train routes
     */
    private static class RoutesLayer extends HexLayer {
        private static final long serialVersionUID = 1L;

        private static Color colour1, colour2, colour3, colour4;
        static {
            try {
                colour1 = Util.parseColour(Config.get("route.colour.1", null));
                colour2 = Util.parseColour(Config.get("route.colour.2", null));
                colour3 = Util.parseColour(Config.get("route.colour.3", null));
                colour4 = Util.parseColour(Config.get("route.colour.4", null));
            } catch (ConfigurationException e) {} finally {
                if (colour1 == null) colour1 = Color.CYAN;
                if (colour2 == null) colour2 = Color.PINK;
                if (colour3 == null) colour3 = Color.ORANGE;
                if (colour4 == null) colour4 = Color.GRAY;
            }
        }
        private static final int STROKE_WIDTH = 5;
        private static final int STROKE_CAP = BasicStroke.CAP_ROUND;
        private static final int STROKE_JOIN = BasicStroke.JOIN_BEVEL;

        private RoutesLayer(HexMap hexMap) {
            super(hexMap);
        }

        private Rectangle getRoutesBounds(List<GeneralPath> p1,
                List<GeneralPath> p2) {
            int margin = (int) Math.ceil(STROKE_WIDTH * hexMap.getZoomFactor());

            List<Rectangle> pathRects = new ArrayList<Rectangle>();
            if (p1 != null) {
                for (GeneralPath p : p1)
                    pathRects.add(p.getBounds());
            }
            if (p2 != null) {
                for (GeneralPath p : p2)
                    pathRects.add(p.getBounds());
            }

            Rectangle r = null;
            for (Rectangle pathRect : pathRects) {
                // enlarge path rectangle with margin
                Rectangle pathMarginRect =
                        new Rectangle(pathRect.x - margin, pathRect.y - margin,
                                pathRect.width + margin * 2, pathRect.height
                                                             + margin * 2);
                if (r == null) {
                    r = pathMarginRect;
                } else {
                    r.add(pathMarginRect);
                }
            }
            return r;
        };

        @Override
        public void paintImage(Graphics2D g) {
            try {
                // Abort if called too early.
                Rectangle rectClip = g.getClipBounds();
                if (rectClip == null) {
                    return;
                }

                // paint train paths
                if (hexMap.getTrainPaths() != null) {
                    Stroke oldStroke = g.getStroke();
                    Color oldColor = g.getColor();
                    Stroke trainStroke =
                            new BasicStroke((int) (STROKE_WIDTH * hexMap.getZoomFactor()),
                                    STROKE_CAP, STROKE_JOIN);
                    g.setStroke(trainStroke);

                    Color[] trainColors =
                            new Color[] { colour1, colour2, colour3, colour4 };
                    int color = 0;
                    for (GeneralPath path:hexMap.getTrainPaths()) {
                        g.setColor(trainColors[color++ % trainColors.length]);
                        g.draw(path);
                    }
                    g.setStroke(oldStroke);
                    g.setColor(oldColor);
                }
            } catch (NullPointerException ex) {
                // If we try to paint before something is loaded, just retry
                // later.
                log.debug("Premature call to RoutesLayer.paintImage(Graphics g)");
            }
        }
    }

    /**
     * Layer containing marks on hexes (selected, selectable, highlighted).
     * Content may change very fast (due to mouse overs)
     */
    private static class MarksLayer extends HexLayer {

        private static final long serialVersionUID = 1L;

        private MarksLayer(HexMap hexMap) {
            super(hexMap);
        }

        @Override
        public void paintImage(Graphics2D g) {
            try {
                // Abort if called too early.
                Rectangle rectClip = g.getClipBounds();
                if (rectClip == null) {
                    return;
                }

                // Paint tiles
                for (GUIHex hex : hexMap.getHexes()) {
                    Rectangle hexrect = hex.getBounds();

                    if (g.hitClip(hexrect.x, hexrect.y, hexrect.width,
                            hexrect.height)) {
                        hex.paintMarks(g);
                    }
                }

            } catch (NullPointerException ex) {
                // If we try to paint before something is loaded, just retry
                // later.
                log.debug("Premature call to MarksLayer.paintImage(Graphics g)");
            }
        }
    }

    /**
     * Layer containing tokens and (if no background map is used) text
     * annotations
     */
    private static class TokensTextsLayer extends HexLayer {
        private static final long serialVersionUID = 1L;

        private TokensTextsLayer(HexMap hexMap) {
            super(hexMap);
        }

        private void drawLabel(Graphics2D g2, int index, int xCoordinate,
                int yCoordinate, boolean letter) {
            String label =
                    letter ? getLetterLabel(index) : hexMap.getNumberLabel(index);

            xCoordinate -= 4.0 * label.length();
            yCoordinate += 4.0;
            g2.drawString(label, xCoordinate, yCoordinate);

            // log.debug("Draw Label " + label + " for " + index + " at x = " +
            // xCoordinate + ", y = " + yCoordinate);
        }

        private String getLetterLabel(int index) {
            if (index > 26) {
                return "A" + String.valueOf((char) ('@' + (index - 26))); // For
                                                                          // 1825U1
                                                                          // row
                                                                          // "AA"
            } else {
                return String.valueOf((char) ('@' + index));
            }
        }

        @Override
        public void paintImage(Graphics2D g) {
            try {
                // Abort if called too early.
                Rectangle rectClip = g.getClipBounds();
                if (rectClip == null) {
                    return;
                }

                // Paint station tokens and texts
                for (GUIHex hex : hexMap.getHexes()) {
                    log.trace("hex ={}", hex);
                    Rectangle hexrect = hex.getBounds();

                    if (g.hitClip(hexrect.x, hexrect.y, hexrect.width,
                            hexrect.height)) {
                        hex.paintTokensAndText(g);
                    }
                }

                // paint coordinates
                boolean lettersGoHorizontal = hexMap.mapManager.getMapOrientation().lettersGoHorizontal();
                int xLeft = (int) hexMap.calcXCoordinates(hexMap.minimum.getCol(), -hexMap.coordinateXMargin);
                int xRight = (int) hexMap.calcXCoordinates(hexMap.maximum.getCol(), hexMap.coordinateXMargin);

                int yTop = (int) hexMap.calcYCoordinates(hexMap.minimum.getRow(), -hexMap.coordinateYMargin);
                int yBottom = (int) hexMap.calcYCoordinates(hexMap.maximum.getRow(), hexMap.coordinateYMargin);

                for (int iCol = hexMap.minimum.getCol(); iCol <= hexMap.maximum.getCol(); iCol++) {
                    int xCoordinate = (int) (hexMap.calcXCoordinates(iCol, 0));
                    drawLabel(g, iCol, xCoordinate, yTop, lettersGoHorizontal);
                    drawLabel(g, iCol, xCoordinate, yBottom,
                            lettersGoHorizontal);
                }

                for (int iRow = hexMap.minimum.getRow(); iRow <= hexMap.maximum.getRow(); iRow++) {
                    int yCoordinate = (int) (hexMap.calcYCoordinates(iRow, 0));
                    drawLabel(g, iRow, xLeft, yCoordinate,
                            !lettersGoHorizontal);
                    drawLabel(g, iRow, xRight, yCoordinate,
                            !lettersGoHorizontal);
                }

            } catch (NullPointerException ex) {
                // If we try to paint before something is loaded, just retry
                // later.
                log.debug("Premature call to TokensTextsLayer.paintImage(Graphics g)");
            }
        }
    }

    /**
     * The only "real" (=swing managed) layer that is used for tool tips
     */
    private static class ToolTipsLayer extends JComponent {
        private static final long serialVersionUID = 1L;
    }

    // static fields (defined by init method)
    private ORUIManager orUIManager;
    private MapManager mapManager;

    // layers
    private TilesLayer tilesLayer;
    private RoutesLayer routesLayer;
    private MarksLayer marksLayer;
    private TokensTextsLayer tokensTextsLayer;
    private ToolTipsLayer toolTipsLayer;
    private List<JComponent> layers;

    protected Map<MapHex, GUIHex> hex2gui;

    // dynamic variables


    protected double scale;
    private int zoomStep = 10; // can be overwritten in config
    private double zoomFactor = 1; // defined dynamically if zoomStep changed

    protected Dimension originalSize;
    private Dimension currentSize;

    private GUIHex selectedHex = null;

    /**
     * The hex over which the mouse pointer is currently situated
     */
    private GUIHex hexAtMousePosition = null;

    /** list of generalpath elements to indicate train runs */
    private List<GeneralPath> trainPaths;

    // Definitions used by subclasses
    protected static final double PEAK_MARGIN = 1.0;
    protected static final double FLAT_MARGIN = 0.80;
    protected static final double COORDINATE_PEAK_MARGIN = 0.80;
    protected static final double COORDINATE_FLAT_MARGIN = 0.60;

    // ("Abstract") Variables to be initialized by map type subclasses
    protected double tileXOffset;
    protected double tileYOffset;
    protected double coordinateXMargin;
    protected double coordinateYMargin;

    protected MapHex.Coordinates minimum;
    protected MapHex.Coordinates maximum;

    protected boolean displayMapImage;

    // Abstract Methods, implemented depending on the map type (EW or NS)
    protected abstract double calcXCoordinates(int col, double offset);

    protected abstract double calcYCoordinates(int row, double offset);

    protected abstract void setOriginalSize();


    public void init(ORUIManager orUIManager, MapManager mapManager) {

        this.orUIManager = orUIManager;
        this.mapManager = mapManager;

        displayMapImage = mapManager.isMapImageUsed();

        minimum = mapManager.getMinimum();
        maximum = mapManager.getMaximum();

        log.trace("HexMap init: minimum = {}, maximum = {}", minimum, maximum);

        // the following order of instantiation and list-adding defines the
        // layering
        // from the top to the bottom
        ImmutableList.Builder<JComponent> layerBuilder = ImmutableList.builder();
        toolTipsLayer = new ToolTipsLayer();
        layerBuilder.add(toolTipsLayer);
        tokensTextsLayer = new TokensTextsLayer(this);
        layerBuilder.add(tokensTextsLayer);
        marksLayer = new MarksLayer(this);
        layerBuilder.add(marksLayer);
        routesLayer = new RoutesLayer(this);
        layerBuilder.add(routesLayer);
        tilesLayer = new TilesLayer(this);
        layerBuilder.add(tilesLayer);
        layers = layerBuilder.build();

        initializeSettings();
        setScale();
        setupHexes();
        setOriginalSize();

        currentSize = (Dimension) originalSize.clone();
        setPreferredSize(originalSize);
        // always call zoom to adjust scaling
        zoom();
    }

    /**
     * defines settings from the config files
     */
    private void initializeSettings() {
        // define zoomStep from config
        String zoomStepSetting = Config.getGameSpecific(mapManager.getRoot().getGameName(), "map.zoomstep");
        if (Util.hasValue(zoomStepSetting)) {
            try {
                int newZoomStep = Integer.parseInt(zoomStepSetting);
                if (zoomStep != newZoomStep) {
                    zoomStep = newZoomStep;
                }
            } catch (NumberFormatException e) {
                // otherwise keep default defined above
            }
        }
    }

    public void addLayers(JLayeredPane p, int startingZOffset) {
        int z = startingZOffset;
        for (JComponent l : layers) {
            p.add(l, z++);
        }
    }

    protected void setupHexesGUI() {
        ImmutableMap.Builder<MapHex, GUIHex> hexMapBuilder =
                ImmutableMap.builder();

        for (MapHex hex:mapManager.getHexes()) {
            GUIHex guiHex = new GUIHex(this, hex, scale);
            hexMapBuilder.put(hex, guiHex);
        }
        hex2gui = hexMapBuilder.build();
    }

    protected void scaleHexesGUI() {
        for (GUIHex hex:hex2gui.values()) {
            hex.setDimensions(scale, zoomFactor);
        }
    }

    private String getNumberLabel(int index) {
        if (index < 0) {
            return String.valueOf(100 + index); // For 1825U1 column "99"
        } else {
            return String.valueOf(index);
        }
    }

    public void setupHexes() {
        setupHexesGUI();
        setupBars();
        addMouseListener(this);
        addMouseMotionListener(this);
    }

    public void setupBars() {
        for (MapHex hex : hex2gui.keySet()) {
            HexSidesSet barSides = hex.getImpassableSides();
            for (HexSide side:barSides) {
                if (side.getTrackPointNumber() < 3) {
                    hex2gui.get(hex).addBar(side);
                }
            }
        }
    }

    GUIHex getHexContainingPoint(Point point) {
        for (GUIHex hex : hex2gui.values()) {
            if (hex.contains(point)) {
                return hex;
            }
        }
        return null;
    }

    public GUIHex getHex(MapHex hex) {
        return hex2gui.get(hex);
    }

    public Collection<GUIHex>  getHexes() {
        return hex2gui.values();
    }

    public boolean hasMapImage() {
        return displayMapImage;
    }

    public boolean isTilePainted(Tile tile) {
        return !(displayMapImage && tile.isPrepainted());
    }

    public void zoom(boolean in) {
        if (in)
            zoomStep++;
        else
            zoomStep--;
        zoom();
    }

    public void setZoomStep(int zoomStep) {
        this.zoomStep = zoomStep;
        zoom();
    }

    public int getZoomStep() {
        return zoomStep;
    }

    public double getZoomFactor() {
        return zoomFactor;
    }

    private void zoom() {
        zoomFactor = ImageLoader.getInstance().getZoomFactor(zoomStep);
        log.trace("HexMap: zoomStep = {}", zoomStep);
        log.trace("HexMap: zoomFactor = {}", zoomFactor);
        setScale();
        scaleHexesGUI();
        currentSize.width = (int) (originalSize.width * zoomFactor);
        currentSize.height = (int) (originalSize.height * zoomFactor);
        setPreferredSize(currentSize);
    }

    protected void setScale() {
        scale = (16 * zoomFactor);
    }

    public Dimension getOriginalSize() {
        return originalSize;
    }

    public Dimension getCurrentSize() {
        return currentSize;
    }

    public void selectHex(GUIHex clickedHex) {
        log.debug("selecthex called for hex {}, selected was {}", clickedHex != null ? clickedHex.toText() : "null", selectedHex != null ? selectedHex.toText() : "null");

        if (selectedHex == clickedHex) return;
        if (selectedHex != null) {
            // Hexes with only invalids do not change state
            if (selectedHex.getState() != GUIHex.State.INVALIDS) {
                selectedHex.setState(GUIHex.State.SELECTABLE);
            }
        }

        if (clickedHex != null) {
            // Hexes with only invalids do not change state
            if (clickedHex.getState() != GUIHex.State.INVALIDS) {
                clickedHex.setState(GUIHex.State.SELECTED);
            }
        }
        selectedHex = clickedHex;

    }

    public GUIHex getSelectedHex() {
        return selectedHex;
    }

    public void setSelectedHex(GUIHex hex) {
        selectedHex = hex;
    }

    public List<GUIHex> getHexesByCurrentTileId(Tile tile) {
        ImmutableList.Builder<GUIHex> hexBuilder =
                ImmutableList.builder();
        for (MapHex hex : hex2gui.keySet()) {
            if (hex.getCurrentTile() == tile) {
                hexBuilder.add(hex2gui.get(hex));
            }
        }
        return hexBuilder.build();
    }


    // FIXME: Remove the code here, only used for reference during rewrite of token code
//    @SuppressWarnings("unchecked")
//    public <T extends LayToken> void setAllowedTokenLays(
//            List<T> allowedTokenLays) {
//
//        this.allowedTokenLays = (List<LayToken>) allowedTokenLays;
//        allowedTokensPerHex = new HashMap<MapHex, List<LayToken>>();
//        bonusTokenLayingEnabled = false;
//
//        /* Build the per-hex allowances map */
//        for (LayToken allowance : this.allowedTokenLays) {
//            List<MapHex> locations = allowance.getLocations();
//            if (locations == null) {
//                /*
//                 * The location may be null, which means: anywhere. This is
//                 * intended to be a temporary fixture, to be replaced by a
//                 * detailed allowed-tiles-per-hex specification later.
//                 */
//                // For now, allow all hexes having non-filled city stations
//                if (allowance instanceof LayBaseToken) {
//                    MapHex hex;
//                    for (GUIHex guiHex : hex2gui.values()) {
//                        hex = guiHex.getHexModel();
//                        if (hex.hasTokenSlotsLeft()) {
//                            allowTokenOnHex(hex, allowance);
//                        }
//                    }
//                } else {
//                    allowTokenOnHex(null, allowance);
//                }
//            } else {
//                for (MapHex location : locations) {
//                    allowTokenOnHex(location, allowance);
//                }
//            }
//            if (allowance instanceof LayBonusToken) {
//                bonusTokenLayingEnabled = true;
//            }
//        }
//    }
//
//    private void allowTokenOnHex(MapHex hex, LayToken allowance) {
//        if (!allowedTokensPerHex.containsKey(hex)) {
//            allowedTokensPerHex.put(hex, new ArrayList<LayToken>());
//        }
//        allowedTokensPerHex.get(hex).add(allowance);
//    }
//
//    public List<LayToken> getTokenAllowanceForHex(MapHex hex) {
//        List<LayToken> allowances = new ArrayList<LayToken>(2);
//        if (hex != null && allowedTokensPerHex.containsKey(hex)) {
//            allowances.addAll(allowedTokensPerHex.get(hex));
//        }
//        if (allowedTokensPerHex.containsKey(null)) {
//            allowances.addAll(allowedTokensPerHex.get(null));
//        }
//        return allowances;
//    }
//
//    public List<LayBaseToken> getBaseTokenAllowanceForHex(MapHex hex) {
//        List<LayBaseToken> allowances = new ArrayList<LayBaseToken>(2);
//        for (LayToken allowance : getTokenAllowanceForHex(hex)) {
//            if (allowance instanceof LayBaseToken) {
//                allowances.add((LayBaseToken) allowance);
//            }
//        }
//        return allowances;
//    }
//
//    public List<LayBonusToken> getBonusTokenAllowanceForHex(MapHex hex) {
//        List<LayBonusToken> allowances = new ArrayList<LayBonusToken>(2);
//        for (LayToken allowance : getTokenAllowanceForHex(hex)) {
//            if (allowance instanceof LayBonusToken) {
//                allowances.add((LayBonusToken) allowance);
//            }
//        }
//        return allowances;
//    }
//
    public List<GeneralPath> getTrainPaths() {
        return trainPaths;
    }

    public void setTrainPaths(List<GeneralPath> trainPaths) {
        Rectangle dirtyRect =
                routesLayer.getRoutesBounds(this.trainPaths, trainPaths);
        this.trainPaths = trainPaths;

        // only repaint if routes existed before or exist now
        if (dirtyRect != null) repaintRoutes(dirtyRect);
    }

    /**
     * Off-board tiles must be able to retrieve the current phase.
     *
     * @return The current Phase object.
     */
    public Phase getPhase() {
        if (orUIManager != null) {
            return orUIManager.getGameUIManager().getRoot().getPhaseManager().getCurrentPhase();
        }
        return null;
    }

    public MapManager getMapManager() {
        return mapManager;
    }

    public ORUIManager getOrUIManager() {
        return orUIManager;
    }

    /**
     * Mouse Listener methods (hexMap offers listener for all layers)
     */

    @Override
    public synchronized void mouseClicked(MouseEvent arg0) {
        Point point = arg0.getPoint();
        GUIHex clickedHex = getHexContainingPoint(point);
        boolean rightClick = SwingUtilities.isRightMouseButton(arg0);

        // if no action/correction was expected on the map panel
        if (!orUIManager.hexClicked(clickedHex, selectedHex, rightClick)) {

            // force the tool tip popup to appear immediately
            ToolTipManager ttm = ToolTipManager.sharedInstance();
            MouseEvent phantomME =
                    new MouseEvent(toolTipsLayer, MouseEvent.MOUSE_MOVED,
                            System.currentTimeMillis(), 0, arg0.getX(),
                            arg0.getY(), 0, false);

            int priorToolTipDelay = ttm.getInitialDelay();
            ttm.setInitialDelay(0);
            ttm.mouseMoved(phantomME);
            ttm.setInitialDelay(priorToolTipDelay);

            // int priorToolTipDelay = ttm.getInitialDelay();
            // ttm.mouseEntered(new MouseAdapter());
            // ToolTipManager.sharedInstance().setInitialDelay(0);
            // try {
            // this.wait(1);
            // } catch (InterruptedException e) {}
            // map = map;
        }
    }

    @Override
    public void mouseDragged(MouseEvent arg0) {}

    @Override
    public synchronized void mouseMoved(MouseEvent arg0) {
        Point point = arg0.getPoint();
        GUIHex newHex = getHexContainingPoint(point);

        // ignore if mouse has not entered a new hex
        if (hexAtMousePosition == newHex) return;

        // provide for hex highlighting
        if (hexAtMousePosition != null)
            hexAtMousePosition.removeHighlightRequest();
        if (newHex != null) newHex.addHighlightRequest();

        // display tool tip
        setToolTipText(newHex != null ? newHex.getToolTip() : null);

        hexAtMousePosition = newHex;
    }

    @Override
    public void mouseEntered(MouseEvent arg0) {}

    @Override
    public synchronized void mouseExited(MouseEvent arg0) {
        // provide for hex highlighting
        if (hexAtMousePosition != null) {
            hexAtMousePosition.removeHighlightRequest();
            hexAtMousePosition = null;
        }
    }

    @Override
    public void mousePressed(MouseEvent arg0) {}

    @Override
    public void mouseReleased(MouseEvent arg0) {}

    /**
     * Triggers for asynchronous repaint of specific layers If possible, these
     * triggers: - only apply for a specified area
     */

    public synchronized void repaintTiles(Rectangle r) {
        tilesLayer.repaint(r);
    }

    private synchronized void repaintRoutes(Rectangle r) {
        routesLayer.repaint(r);
    }

    public synchronized void repaintMarks(Rectangle r) {
        marksLayer.repaint(r);
    }

    public synchronized void repaintTokens(Rectangle r) {
        tokensTextsLayer.repaint(r);
    }

    /**
     * Do only call this method if you are sure that a complete repaint is
     * needed!
     */
    public synchronized void repaintAll(Rectangle r) {
        for (JComponent l : layers) {
            l.repaint(r);
        }
    }

    /**
     * JComponent methods delegating to the hexmap layers
     */

    public void setBounds(int x, int y, int width, int height) {
        for (JComponent l : layers) {
            l.setBounds(x, y, width, height);
        }
    }

    private void setPreferredSize(Dimension size) {
        for (JComponent l : layers) {
            l.setPreferredSize(size);
        }
    }

    private void setToolTipText(String text) {
        toolTipsLayer.setToolTipText(text);
    }

    public Dimension getSize() {
        // get size from top-most layer (all layers have the same size anyways)
        return layers.get(layers.size() - 1).getSize();
    }

    private void addMouseListener(MouseListener ml) {
        for (JComponent l : layers) {
            l.addMouseListener(ml);
        }
    }

    private void addMouseMotionListener(MouseMotionListener ml) {
        for (JComponent l : layers) {
            l.addMouseMotionListener(ml);
        }
    }

}