/**
 *  Copyright (C) 2002-2012   The FreeCol Team
 *
 *  This file is part of FreeCol.
 *
 *  FreeCol 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.
 *
 *  FreeCol 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 FreeCol.  If not, see <http://www.gnu.org/licenses/>.
 */

package net.sf.freecol.client.gui;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Transparency;
import java.awt.font.TextLayout;
import java.awt.image.BufferedImage;
import java.util.Map.Entry;
import java.util.logging.Logger;

import javax.swing.ImageIcon;
import javax.swing.JComponent;

import net.sf.freecol.common.FreeColException;
import net.sf.freecol.common.model.FoundingFather;
import net.sf.freecol.common.model.FreeColGameObjectType;
import net.sf.freecol.common.model.GoodsType;
import net.sf.freecol.common.model.Map.Direction;
import net.sf.freecol.common.model.LostCityRumour;
import net.sf.freecol.common.model.Nation;
import net.sf.freecol.common.model.Ownable;
import net.sf.freecol.common.model.Player;
import net.sf.freecol.common.model.ResourceType;
import net.sf.freecol.common.model.Settlement;
import net.sf.freecol.common.model.SettlementType;
import net.sf.freecol.common.model.Tension;
import net.sf.freecol.common.model.Tile;
import net.sf.freecol.common.model.TileType;
import net.sf.freecol.common.model.Unit;
import net.sf.freecol.common.model.Unit.Role;
import net.sf.freecol.common.model.UnitType;
import net.sf.freecol.common.resources.ResourceManager;


/**
 * Holds various images that can be called upon by others in order to display
 * certain things.
 */
public final class ImageLibrary {

    private static final Logger logger = Logger.getLogger(ImageLibrary.class.getName());

    public static final String UNIT_SELECT = "unitSelect.image",
                               DELETE = "delete.image",
                               PLOWED = "model.improvement.plow.image",
                               TILE_TAKEN = "tileTaken.image",
                               TILE_OWNED_BY_INDIANS = "nativeLand.image",
                               LOST_CITY_RUMOUR = "lostCityRumour.image",
                               DARKNESS = "halo.dark.image";


    /**
     * Draw a (usually small) background image into a (usually larger)
     * space specified by a component, tiling the image to fill up the
     * space.  If the image is not available, just fill with the background
     * colour.
     *
     * @param resource The name of the <code>ImageResource</code> to tile with.
     * @param g The <code>Graphics</code> to draw to.
     * @param c The <code>JComponent</code> that defines the space.
     * @param insets Optional <code>Insets</code> to apply.
     */
    public static void drawTiledImage(String resource, Graphics g,
                                      JComponent c, Insets insets) {
        int width = c.getWidth();
        int height = c.getHeight();
        Image image = ResourceManager.getImage(resource);
        int dx, dy, xmin, ymin;

        if (insets == null) {
            xmin = 0;
            ymin = 0;
        } else {
            xmin = insets.left;
            ymin = insets.top;
            width -= insets.left + insets.right;
            height -= insets.top + insets.bottom;
        }
        if (image != null && (dx = image.getWidth(null)) > 0
            && (dy = image.getHeight(null)) > 0) {
            int xmax, ymax;
            xmax = xmin + width;
            ymax = ymin + height;
            for (int x = xmin; x < xmax; x += dx) {
                for (int y = ymin; y < ymax; y += dy) {
                    g.drawImage(image, x, y, null);
                }
            }
        } else {
            g.setColor(c.getBackground());
            g.fillRect(xmin, ymin, width, height);
        }
    }


    /**
     * The scaling factor used when creating this
     * <code>ImageLibrary</code>. The value
     * <code>1</code> is used if this object is not
     * a result of a scaling operation.
     */
    private final float scalingFactor;

    /**
     * The constructor to use.
     *
     */
    public ImageLibrary() {
        this(1);
    }


    public ImageLibrary(float scalingFactor) {
        this.scalingFactor = scalingFactor;
    }

    /**
     * Returns the alarm chip with the given color.
     *
     * @param alarm The alarm level.
     * @param visited a <code>boolean</code> value
     * @param scale a <code>double</code> value
     * @return The alarm chip.
     */
    public Image getAlarmChip(Tension.Level alarm, final boolean visited, double scale) {
        if (visited) {
            return ResourceManager.getChip("alarmChip.visited."
                                           + alarm.toString().toLowerCase(), scale);
        } else {
            return ResourceManager.getChip("alarmChip." + alarm.toString().toLowerCase(), scale);
        }
    }

    /**
     * Returns the beach corner image at the given index.
     *
     * @param index The index of the image to return.
     * @param x an <code>int</code> value
     * @param y an <code>int</code> value
     * @return The image at the given index.
     */
    public Image getBeachCornerImage(int index, int x, int y) {
        return ResourceManager.getImage("model.tile.beach.corner" + index
                                        + (isEven(x, y) ? "_even" : "_odd"), scalingFactor);
    }

    /**
     * Returns the beach edge image at the given index.
     *
     * @param index The index of the image to return.
     * @param x an <code>int</code> value
     * @param y an <code>int</code> value
     * @return The image at the given index.
     */
    public Image getBeachEdgeImage(int index, int x, int y) {
        return ResourceManager.getImage("model.tile.beach.edge" + index
                                        + (isEven(x, y) ? "_even" : "_odd"), scalingFactor);
    }

    public Image getBonusImage(ResourceType type) {
        return getBonusImage(type, scalingFactor);
    }

    public Image getBonusImage(ResourceType type, double scale) {
        return ResourceManager.getImage(type.getId() + ".image", scale);
    }

    /**
     * Returns the bonus-image for the given tile.
     *
     * @param tile
     * @return the bonus-image for the given tile.
     */
    public Image getBonusImage(Tile tile) {
        if (tile.hasResource()) {
            return getBonusImage(tile.getTileItemContainer().getResource().getType());
        } else {
            return null;
        }
    }

    /**
     * Returns the bonus-ImageIcon at the given index.
     *
     * @param type The type of the bonus-ImageIcon to return.
     * @return <code>ImageIcon</code>
     */
    public ImageIcon getBonusImageIcon(ResourceType type) {
        return new ImageIcon(getBonusImage(type));
    }

    /**
     * Returns the border terrain-image for the given type.
     *
     * @param type The type of the terrain-image to return.
     * @param direction a <code>Direction</code> value
     * @param x The x-coordinate of the location of the tile that is being
     *            drawn.
     * @param y The x-coordinate of the location of the tile that is being
     *            drawn.
     * @return The terrain-image at the given index.
     */
    public Image getBorderImage(TileType type, Direction direction, int x, int y) {
        String key = (type == null) ? "model.tile.unexplored" : type.getId();
        return ResourceManager.getImage(key + ".border_" + direction
                                        + (isEven(x, y) ?  "_even" : "_odd")
                                        + ".image", scalingFactor);
    }


    /**
     * Returns true if the tile with the given coordinates is to be
     * considered "even". This is useful to select different images
     * for the same tile type in order to prevent big stripes or
     * a checker-board effect.
     *
     * @param x an <code>int</code> value
     * @param y an <code>int</code> value
     * @return a <code>boolean</code> value
     */
    private boolean isEven(int x, int y) {
        return ((y % 8 <= 2) || ((x + y) % 2 == 0 ));
    }

    /**
     * Returns the coat-of-arms image for the given Nation.
     *
     * @param nation The nation.
     * @return the coat-of-arms of this nation
     */
    public Image getCoatOfArmsImage(Nation nation) {
        return getCoatOfArmsImage(nation, scalingFactor);
    }

    public Image getCoatOfArmsImage(Nation nation, double scale) {
        return ResourceManager.getImage(nation.getId() + ".image", scale);
    }

    /**
     * Returns the coat-of-arms image for the given Nation.
     *
     * @param nation The nation.
     * @return the coat-of-arms of this nation
     */
    public ImageIcon getCoatOfArmsImageIcon(Nation nation) {
        return ResourceManager.getImageIcon(nation.getId() + ".image");
    }

    /**
     * Returns the color of the given player.
     *
     * @param player a <code>Player</code> value
     * @return The color of the given player.
     */
    public Color getColor(Player player) {
        return ResourceManager.getColor(player.getNationID() + ".color");
    }

    /**
     * Returns the color chip with the given color.
     *
     * @param ownable an <code>Ownable</code> value
     * @param scale a <code>double</code> value
     * @return The color chip with the given color.
     */
    public Image getColorChip(Ownable ownable, double scale) {
        return ResourceManager.getChip(ownable.getOwner().getNationID() + ".chip", scale);
    }

    /**
     * Returns the scaled terrain-image for a terrain type (and position 0, 0).
     *
     * @param type The type of the terrain-image to return.
     * @param scale The scale of the terrain image to return.
     * @return The terrain-image
     */
    public Image getCompoundTerrainImage(TileType type, double scale) {
        // Currently used for hills and mountains
        Image terrainImage = getTerrainImage(type, 0, 0, scale);
        Image overlayImage = getOverlayImage(type, 0, 0, scale);
        Image forestImage = type.isForested() ? getForestImage(type, scale) : null;
        if (overlayImage == null && forestImage == null) {
            return terrainImage;
        } else {
            GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment()
                .getDefaultScreenDevice().getDefaultConfiguration();
            int width = terrainImage.getWidth(null);
            int height = terrainImage.getHeight(null);
            if (overlayImage != null) {
                height = Math.max(height, overlayImage.getHeight(null));
            }
            if (forestImage != null) {
                height = Math.max(height, forestImage.getHeight(null));
            }
            BufferedImage compositeImage = gc.createCompatibleImage(width, height, Transparency.TRANSLUCENT);
            Graphics2D g = compositeImage.createGraphics();
            g.drawImage(terrainImage, 0, height - terrainImage.getHeight(null), null);
            if (overlayImage != null) {
                g.drawImage(overlayImage, 0, height - overlayImage.getHeight(null), null);
            }
            if (forestImage != null) {
                g.drawImage(forestImage, 0, height - forestImage.getHeight(null), null);
            }
            g.dispose();
            return compositeImage;
        }
    }


    /**
     * Converts an image to grayscale
     *
     * @param image Source image to convert
     * @return The image in grayscale
     */
    /*
    private ImageIcon convertToGrayscale(Image image) {
        int width = image.getWidth(null);
        int height = image.getHeight(null);

        ColorConvertOp filter = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), null);
        BufferedImage srcImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        srcImage.createGraphics().drawImage(image, 0, 0, null);
        return new ImageIcon(filter.filter(srcImage, null));
    }
    */

    /**
     * Returns the height of the terrain-image including overlays and
     * forests for the given terrain type.
     *
     * @param type The type of the terrain-image.
     * @return The height of the terrain-image at the given index.
     */
    public int getCompoundTerrainImageHeight(TileType type) {
        int height = getTerrainImageHeight(type);
        if (type != null) {
            Image overlayImage = getOverlayImage(type, 0, 0);
            if (overlayImage != null) {
                height = Math.max(height, overlayImage.getHeight(null));
            }
            if (type.isForested()) {
                height = Math.max(height, getForestImage(type).getHeight(null));
            }
        }
        return height;
    }

    /**
     * Returns the forest image for a terrain type.
     *
     * @param type The type of the terrain-image to return.
     * @return The image at the given index.
     */
    public Image getForestImage(TileType type) {
        return getForestImage(type, scalingFactor);
    }

    public Image getForestImage(TileType type, double scale) {
        return ResourceManager.getImage(type.getId() + ".forest", scale);
    }

    /**
     * Returns the portrait of this Founding Father.
     *
     * @param father a <code>FoundingFather</code> value
     * @return an <code>Image</code> value
     */
    public Image getFoundingFatherImage(FoundingFather father) {
        return ResourceManager.getImage(father.getId() + ".image");
    }

    /**
     * Returns the goods-image at the given index.
     *
     * @param goodsType The type of the goods-image to return.
     * @return The goods-image at the given index.
     */
    public Image getGoodsImage(GoodsType goodsType) {
        return getGoodsImage(goodsType, scalingFactor);
    }

    public Image getGoodsImage(GoodsType goodsType, double scale) {
        return ResourceManager.getImage(goodsType.getId() + ".image", scale);
    }

    /**
     * Returns the goods-image for a goods type.
     *
     * @param goodsType The type of the goods-image to return.
     * @return The goods-image at the given index.
     */
    public ImageIcon getGoodsImageIcon(GoodsType goodsType) {
        return ResourceManager.getImageIcon(goodsType.getId() + ".image");
    }

    public Image getImage(FreeColGameObjectType type) {
        return ResourceManager.getImage(type.getId() + ".image", scalingFactor);
    }

    public Image getImage(FreeColGameObjectType type, double scale) {
        return ResourceManager.getImage(type.getId() + ".image", scale);
    }

    /**
     * Returns the appropriate ImageIcon for Object.
     *
     * @param display The Object to display.
     * @return The appropriate ImageIcon.
     */
    public ImageIcon getImageIcon(Object display, boolean small) {
        Image image = null;
        if (display == null) {
            return new ImageIcon();
        } else if (display instanceof GoodsType) {
            GoodsType goodsType = (GoodsType) display;
            try {
                image = this.getGoodsImage(goodsType);
            } catch (Exception e) {
                logger.warning("could not find image for goods " + goodsType);
            }
        } else if (display instanceof Unit) {
            Unit unit = (Unit) display;
            try {
                image = this.getUnitImageIcon(unit).getImage();
            } catch (Exception e) {
                logger.warning("could not find image for unit " + unit.toString());
            }
        } else if (display instanceof UnitType) {
            UnitType unitType = (UnitType) display;
            try {
                image = this.getUnitImageIcon(unitType).getImage();
            } catch (Exception e) {
                logger.warning("could not find image for unit " + unitType);
            }
        } else if (display instanceof Settlement) {
            Settlement settlement = (Settlement) display;
            try {
                image = this.getSettlementImage(settlement);
            } catch (Exception e) {
                logger.warning("could not find image for settlement " + settlement);
            }
        } else if (display instanceof LostCityRumour) {
            try {
                image = this.getMiscImage(ImageLibrary.LOST_CITY_RUMOUR);
            } catch (Exception e) {
                logger.warning("could not find image for lost city rumour");
            }
        } else if (display instanceof Player) {
            image = this.getCoatOfArmsImage(((Player) display).getNation());
        }
        if (image != null && small) {
            return new ImageIcon(image.getScaledInstance((image.getWidth(null) / 3) * 2,
                                                         (image.getHeight(null) / 3) *2,
                                                         Image.SCALE_SMOOTH));
        } else {
            return (image != null) ? new ImageIcon(image) : null;
        }
    }

    /**
     * Returns the image with the given id.
     *
     * @param id The id of the image to return.
     * @return The image.
     */
    public Image getMiscImage(String id) {
        return getMiscImage(id, scalingFactor);
    }

    public Image getMiscImage(String id, double scale) {
        return ResourceManager.getImage(id, scale);
    }

    /**
     * Returns the image with the given id.
     *
     * @param id The id of the image to return.
     * @return The image.
     */
    public ImageIcon getMiscImageIcon(String id) {
        return new ImageIcon(getMiscImage(id));
    }

    /**
     * Returns the mission chip with the given color.
     *
     * @param ownable an <code>Ownable</code> value
     * @param expertMission Indicates whether or not the missionary is an
     *            expert.
     * @param scale a <code>double</code> value
     * @return The color chip with the given color.
     */
    public Image getMissionChip(Ownable ownable, boolean expertMission, double scale) {
        if (expertMission) {
            return ResourceManager.getChip(ownable.getOwner().getNationID()
                                           + ".mission.expert.chip", scale);
        } else {
            return ResourceManager.getChip(ownable.getOwner().getNationID()
                                           + ".mission.chip", scale);
        }
    }

    /**
     * Returns the monarch-image for the given tile.
     *
     * @param nation The nation this monarch rules.
     * @return the monarch-image for the given nation.
     */
    public Image getMonarchImage(Nation nation) {
        return ResourceManager.getImage(nation.getId() + ".monarch.image");
    }

    /**
     * Returns the monarch-image icon for the given Nation.
     *
     * @param nation The nation this monarch rules.
     * @return the monarch-image for the given nation.
     */
    public ImageIcon getMonarchImageIcon(Nation nation) {
        return ResourceManager.getImageIcon(nation.getId() + ".monarch.image");
    }

    /**
     * Returns the overlay-image for the given type.
     *
     * @param type The type of the terrain-image to return.
     * @param x The x-coordinate of the location of the tile that is being
     *            drawn.
     * @param y The x-coordinate of the location of the tile that is being
     *            drawn.
     * @return The terrain-image at the given index.
     */
    public Image getOverlayImage(TileType type, int x, int y) {
        return getOverlayImage(type, x, y, scalingFactor);
    }

    public Image getOverlayImage(TileType type, int x, int y, double scale) {
        String key = type.getId() + ".overlay" + ((x + y) % 2) + ".image";
        if (ResourceManager.hasResource(key)) {
            return ResourceManager.getImage(key, scale);
        } else {
            return null;
        }
    }

    /**
     * Gets an image to represent the path of the given <code>Unit</code>.
     *
     * @param u The <code>Unit</code>
     * @return The <code>Image</code>.
     */
    public Image getPathImage(Unit u) {
        if (u == null) {
            return null;
        } else {
            return ResourceManager.getImage("path." + getPathType(u) + ".image");
        }
    }

    /**
     * Gets an image to represent the path of the given <code>Unit</code>.
     *
     * @param u The <code>Unit</code>
     * @return The <code>Image</code>.
     */
    public Image getPathNextTurnImage(Unit u) {
        if (u == null) {
            return null;
        } else {
            return ResourceManager.getImage("path." + getPathType(u) + ".nextTurn.image");
        }
    }

    /**
     * Returns the river image at the given index.
     *
     * @param index The index of the image to return.
     * @return The image at the given index.
     */
    public Image getRiverImage(int index) {
        return getRiverImage(index, scalingFactor);
    }

    public Image getRiverImage(int index, double scale) {
        return ResourceManager.getImage("model.tile.river" + index, scale);
    }

    /**
     * Returns the river mouth terrain-image for the direction and magnitude.
     *
     * @param direction a <code>Direction</code> value
     * @param magnitude an <code>int</code> value
     * @param x The x-coordinate of the location of the tile that is being
     *            drawn (ignored).
     * @param y The x-coordinate of the location of the tile that is being
     *            drawn (ignored).
     * @return The terrain-image at the given index.
     */
    public Image getRiverMouthImage(Direction direction, int magnitude, int x, int y) {
        String key = "model.tile.delta_" + direction + (magnitude == 1 ? "_small" : "_large");
        return ResourceManager.getImage(key, scalingFactor);
    }

    public ImageIcon getScaledBonusImageIcon(ResourceType type, float scale) {
        return new ImageIcon(getBonusImage(type, scale));
    }

    /**
     * Returns the scaled goods-ImageIcon for a goods type.
     *
     * @param type The type of the goods-ImageIcon to return.
     * @param scale The scale of the goods-ImageIcon to return.
     * @return The goods-ImageIcon at the given index.
     */
    public ImageIcon getScaledGoodsImageIcon(GoodsType type, double scale) {
        return new ImageIcon(getGoodsImage(type, scale));
    }

    /**
     * Gets a scaled version of this <code>ImageLibrary</code>.
     * @param scalingFactor The factor used when scaling. 2 is twice
     *      the size of the original images and 0.5 is half.
     * @return A new <code>ImageLibrary</code>.
     * @throws FreeColException
     */
    public ImageLibrary getScaledImageLibrary(float scalingFactor) throws FreeColException {
        return new ImageLibrary(scalingFactor);
    }

    /**
     * Returns the scaling factor used when creating this ImageLibrary.
     * @return 1 unless {@link #getScaledImageLibrary} was used to create
     *      this object.
     */
    public float getScalingFactor() {
        return scalingFactor;
    }

    /**
     * Returns the graphics that will represent the given settlement.
     *
     * @param settlement The settlement whose graphics type is needed.
     * @return The graphics that will represent the given settlement.
     */
    public Image getSettlementImage(Settlement settlement) {
        return getSettlementImage(settlement, scalingFactor);
    }

    /**
     * Returns the graphics that will represent the given settlement.
     *
     * @param settlement The settlement whose graphics type is needed.
     * @param scale a <code>double</code> value
     * @return The graphics that will represent the given settlement.
     */
    public Image getSettlementImage(Settlement settlement, double scale) {
        return ResourceManager.getImage(settlement.getImageKey(), scale);
    }

    /**
     * Returns the graphics that will represent the given settlement.
     *
     * @param settlementType The type of settlement whose graphics type is needed.
     * @return The graphics that will represent the given settlement.
     */
    public Image getSettlementImage(SettlementType settlementType) {
        return getSettlementImage(settlementType, scalingFactor);
    }

    public Image getSettlementImage(SettlementType settlementType, double scale) {
        return ResourceManager.getImage(settlementType.getId() + ".image", scale);
    }

    /**
     * Returns the terrain-image for the given type.
     *
     * @param type The type of the terrain-image to return.
     * @param x The x-coordinate of the location of the tile that is being
     *            drawn.
     * @param y The x-coordinate of the location of the tile that is being
     *            drawn.
     * @return The terrain-image at the given index.
     */
    public Image getTerrainImage(TileType type, int x, int y) {
        return getTerrainImage(type, x, y, scalingFactor);
    }

    public Image getTerrainImage(TileType type, int x, int y, double scale) {
        String key = (type == null) ? "model.tile.unexplored" : type.getId();
        return ResourceManager.getImage(key + ".center" + (isEven(x, y) ? "0" : "1")
                                        + ".image", scale);
    }

    /**
     * Returns the height of the terrain-image for a terrain type.
     *
     * @param type The type of the terrain-image.
     * @return The height of the terrain-image at the given index.
     */
    public int getTerrainImageHeight(TileType type) {
        return getTerrainImage(type, 0, 0).getHeight(null);
    }

    /**
     * Returns the width of the terrain-image for a terrain type.
     *
     * @param type The type of the terrain-image.
     * @return The width of the terrain-image at the given index.
     */
    public int getTerrainImageWidth(TileType type) {
        return getTerrainImage(type, 0, 0).getWidth(null);
    }

    /**
     * Returns the ImageIcon that will represent the given unit.
     *
     * @param unit The unit whose graphics type is needed.
     * @return an <code>ImageIcon</code> value
     */
    public ImageIcon getUnitImageIcon(Unit unit) {
        return getUnitImageIcon(unit.getType(), unit.getRole(), unit.hasNativeEthnicity(), false, scalingFactor);
    }

    public ImageIcon getUnitImageIcon(Unit unit, boolean grayscale) {
        return getUnitImageIcon(unit.getType(), unit.getRole(), unit.hasNativeEthnicity(), grayscale, scalingFactor);
    }

    public ImageIcon getUnitImageIcon(Unit unit, boolean grayscale, double scale) {
        return getUnitImageIcon(unit.getType(), unit.getRole(), unit.hasNativeEthnicity(), grayscale, scale);
    }

    public ImageIcon getUnitImageIcon(Unit unit, double scale) {
        return getUnitImageIcon(unit.getType(), unit.getRole(), unit.hasNativeEthnicity(), false, scale);
    }

    /**
     * Returns the ImageIcon that will represent a unit of the given type.
     *
     * @param unitType an <code>UnitType</code> value
     * @return an <code>ImageIcon</code> value
     */
    public ImageIcon getUnitImageIcon(UnitType unitType) {
        return getUnitImageIcon(unitType, Role.DEFAULT, false, false, scalingFactor);
    }

    public ImageIcon getUnitImageIcon(UnitType unitType, boolean grayscale) {
        return getUnitImageIcon(unitType, Role.DEFAULT, false, grayscale, scalingFactor);
    }

    public ImageIcon getUnitImageIcon(UnitType unitType, boolean grayscale, double scale) {
        return getUnitImageIcon(unitType, Role.DEFAULT, false, grayscale, scale);
    }

    public ImageIcon getUnitImageIcon(UnitType unitType, double scale) {
        return getUnitImageIcon(unitType, Role.DEFAULT, false, false, scale);
    }

    public ImageIcon getUnitImageIcon(UnitType unitType, Role role) {
        return getUnitImageIcon(unitType, role, false, false, scalingFactor);
    }


    public ImageIcon getUnitImageIcon(UnitType unitType, Role role, boolean grayscale) {
        return getUnitImageIcon(unitType, role, false, grayscale, scalingFactor);
    }

    /**
     * Returns the ImageIcon that will represent a unit with the given specifics.
     *
     * @param unitType the type of unit to be represented
     * @param role unit has equipment that affects its abilities/appearance
     * @param nativeEthnicity draws the unit with native skin tones
     * @param grayscale draws the icon in an inactive/disabled-looking state
     * @return an <code>ImageIcon</code> value
     */
    public ImageIcon getUnitImageIcon(UnitType unitType, Role role, boolean nativeEthnicity, boolean grayscale, double scale) {
        // units that can only be native don't need the .native key part
        if (unitType.getId().equals("model.unit.indianConvert")
            || unitType.getId().equals("model.unit.brave")) {
            nativeEthnicity = false;
        }
        else for (Entry<String, Boolean> entry : unitType.getAbilitiesRequired().entrySet()) {
            if (entry.getKey().equals("model.ability.native") && entry.getValue() == true) {
                nativeEthnicity = false;
            }
        }
        
        // try to get an image matching the key
        final String key = unitType.getId()
            + (role == Role.DEFAULT ? "" : "." + role.getId())
            + (nativeEthnicity ? ".native" : "")
            + ".image";
        Image image = null;
        if (grayscale) {
            image = ResourceManager.getGrayscaleImage(key, scale);
        } else {
            image = ResourceManager.getImage(key, scale);
        }
        
        if (image == null) {
            // log and attempt fallback
            logger.finest("No image found for image for " + key);
            if (nativeEthnicity == true) {
                // try non-native variant
                return getUnitImageIcon(unitType, role, false, grayscale, scale);
            
            // FIXME: these require the game specification, which ImageLibrary doesn't yet have access to
/*          } else if (role != Role.DEFAULT && !unitType.getId().equals("model.unit.freeColonist")) {
                // try a free colonist with the same role
                unitType = getGame().getSpecification().getUnitType("model.unit.freeColonist");
                return getUnitImageIcon(unitType, role, false, grayscale, scale);
            } else {
                // give up, draw a standard unit icon
                unitType = getGame().getSpecification().getUnitType("model.unit.freeColonist");
                return getUnitImageIcon(unitType, Role.DEFAULT, false, grayscale, scale);
*/          }
            
            logger.warning("Failed to retrieve image for " + key);
            return null;
        }
        return new ImageIcon(image);
    }

    public ImageIcon getUnitImageIcon(UnitType unitType, Role role, boolean grayscale, double scale) {
        return getUnitImageIcon(unitType, role, false, grayscale, scale);
    }

    /**
     * Gets an image to represent the path of the given <code>Unit</code>.
     *
     * @param u The <code>Unit</code>
     * @return The <code>Image</code>.
     *
    private Image getPathIllegalImage(Unit u) {
        if (u == null || u.isNaval()) {
            return (Image) UIManager.get("path.naval.illegal.image");
        } else if (u.isMounted()) {
            return (Image) UIManager.get("path.horse.illegal.image");
        } else if (u.getType() == Unit.WAGON_TRAIN || u.getType() == Unit.TREASURE_TRAIN || u.getType() == Unit.ARTILLERY || u.getType() == Unit.DAMAGED_ARTILLERY) {
            return (Image) UIManager.get("path.wagon.illegal.image");
        } else {
            return (Image) UIManager.get("path.foot.illegal.image");
        }
    }
    */

    public ImageIcon getUnitImageIcon(UnitType unitType, Role role, double scale) {
        return getUnitImageIcon(unitType, role, false, false, scale);
    }

    
    private String getPathType(Unit unit) {
        if (unit.isNaval()) {
            return "naval";
        } else if (unit.isMounted()) {
            return "horse";
        } else if (unit.getType().hasSkill() || unit.isUndead()) {
            return "foot";
        } else {
            return "wagon";
        }
    }
    
    
    /**
     * Create a "chip" with the given text and colors.
     *
     * @param text a <code>String</code> value
     * @param border a <code>Color</code> value
     * @param background a <code>Color</code> value
     * @param foreground a <code>Color</code> value
     * @return an <code>Image</code> value
     */
    public Image createChip(String text, Color border, Color background, Color foreground) {
        // Draw it and put it in the cache
        Font font = ResourceManager.getFont("SimpleFont", Font.BOLD,
                (float) Math.rint(12 * getScalingFactor()));
        // hopefully, this is big enough
        BufferedImage bi = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2 = bi.createGraphics();
        TextLayout label = new TextLayout(text, font, g2.getFontRenderContext());
        float padding = 6 * getScalingFactor();
        int width = (int) (label.getBounds().getWidth() + padding);
        int height = (int) (label.getAscent() + label.getDescent() + padding);
        g2.setColor(border);
        g2.fillRect(0, 0, width, height);
        g2.setColor(background);
        g2.fillRect(1, 1, width - 2, height - 2);
        g2.setColor(foreground);
        label.draw(g2, (float) (padding/2 - label.getBounds().getX()), label.getAscent() + padding/2);
        g2.dispose();
        return bi.getSubimage(0, 0, width, height);
    }



}
