package net.sf.freecol.client.gui.images;

import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.util.Random;

import net.sf.freecol.client.gui.ImageLibrary;
import net.sf.freecol.common.model.Direction;
import net.sf.freecol.common.model.Tile;
import net.sf.freecol.common.resources.ImageCache;
import net.sf.freecol.common.resources.ImageResource;
import net.sf.freecol.common.util.ImageUtils;

/**
 * Creates a transition between base tiles. For example, this transition will make the prairie graphics blend into the grassland graphics.
 */
public final class BaseTileTransitionImageCreator {
    
    private final ImageLibrary lib;
    private final ImageCache imageCache;
    
    public BaseTileTransitionImageCreator(ImageLibrary lib, ImageCache imageCache) {
        this.lib = lib;
        this.imageCache = imageCache;
    }
    

    /**
     * Returns a transparent image for making a transition between
     * the given tiles.
     *  
     * @param tile The tile that should get a transition.
     * @param direction The direction to get the bordering tile from.
     * @param useNiceCorners Determines if the corners of the base transitions
     *      should be rendered nicely (takes more time).
     * @param useVariations Uses variations of the transition.
     * @return The image, or {@code null} if there is no transition that
     *      should be drawn.
     */
    public BufferedImage getBaseTileTransitionImage(Tile tile, Direction direction, boolean useNiceCorners, boolean useVariations) {
        /*
         * 1. Transitions between tiles in NE and SW directions are made using only the
         * graphics for the other tile (using a mask in order to make it fade out).
         *  
         * 2. When making transitions in NW and SE directions, we need to make the transition
         * using not only the graphics for the other tile -- but also with the transitions
         * mentioned in point 1 included. This is a costly operation, however, so it can be
         * activated using the "useNiceCorners" parameter.
         */
        
        final Tile borderingTile = tile.getNeighbourOrNull(direction);
        
        /*
         * If true, we need to render the transition as mentioned in point 2 of the previous
         * comment.
         */
        final boolean renderSpecialTransition = useNiceCorners && (direction == Direction.NW || direction == Direction.SE);
        
        if (borderingTile == null
                || !borderingTile.isExplored()
                || !tile.isExplored()) {
            // No transition needed in these cases.
            return null;
        }
        
        if (tile.getType() == borderingTile.getType()
                && !renderSpecialTransition) {
            // No transition needed in these cases.
            return null;
        }
        
        final ImageResource terrainImageResource = getTerrainOrBeachImageResource(tile, borderingTile);
        if (terrainImageResource == null) {
            // Handles missing resource gracefully.
            return null;
        }
        
        final ImageResource tileImageResource = ImageCache.getImageResource(ImageLibrary.getTerrainImageKey(tile.getType()));
        if (tileImageResource == null) {
            // Handles missing resource gracefully.
            return null;
        }
        if (terrainImageResource.getCachingKey().equals(tileImageResource.getCachingKey())
                && !renderSpecialTransition) {
            /*
             * No reason to make transitions between tiles with the same graphics.. 
             */
            return null;
        }
        
        final boolean beachTransitionNe;
        final boolean beachTransitionSw;
        if (renderSpecialTransition) {
            beachTransitionNe = shouldIncludeSpecialBeachTransitionInDirection(tile, borderingTile, Direction.NE);
            beachTransitionSw = shouldIncludeSpecialBeachTransitionInDirection(tile, borderingTile, Direction.SW);
        } else {
            beachTransitionNe = false;
            beachTransitionSw = false;
        }
        
        final ImageResource terrainMaskResource = lib.getTerrainMaskResource(direction);
        final int maskVariationNumber;
        if (useVariations && terrainMaskResource.getNumberOfVariations() > 1) {
            // Choose the same variation for both sides of the transition.
            final Tile transitionSeedTile = (direction == Direction.NW || direction == Direction.NE) ? tile : borderingTile;
            final int seed = ImageLibrary.variationSeedUsing(transitionSeedTile.getX(), transitionSeedTile.getY());
            
            // This never picks the last variation as that's just a simple, almost completely straight line, transition.
            maskVariationNumber = new Random(seed).nextInt(terrainMaskResource.getNumberOfVariations() - 1);
        } else {
            maskVariationNumber = terrainMaskResource.getNumberOfVariations() - 1;
        }
        
        /*
         * The transitionKey is used for caching, and needs to be unique for every transition.
         */
        final String transitionKey;
        if (renderSpecialTransition) {
            final Tile neBorderingTile = borderingTile.getNeighbourOrNull(Direction.NE);
            final Tile swBorderingTile = borderingTile.getNeighbourOrNull(Direction.SW);

            transitionKey = terrainImageResource.getCachingKey()
                    + ","
                    + (neBorderingTile != null && neBorderingTile.getType() != null ? neBorderingTile.getType().getId() : "null")
                    + ","
                    + (swBorderingTile != null && swBorderingTile.getType() != null ? swBorderingTile.getType().getId() : "null")
                    + ","
                    + maskVariationNumber
                    + ","
                    + beachTransitionNe
                    + beachTransitionSw
                    + "$baseTransition$gen";
        } else {
            transitionKey = terrainImageResource.getCachingKey() + "," + maskVariationNumber + "$baseTransition$gen";
        }
        
        /*
         * Using direction.ordinal() as the cached variation in order to get different
         * image cache hash values.
         */
        final BufferedImage transitionImage = imageCache.getCachedImageOrGenerate(transitionKey, lib.getTileSize(), false, direction.ordinal(), () -> {
            
            /*
             * Uses variation 0 to get the same variation every time as this reduces the amount
             * of memory used.
             */
            final BufferedImage terrainImage = imageCache.getCachedImage(terrainImageResource, lib.getTileSize(), false, 0);
            
            final BufferedImage transitionTileImage;
            if (!renderSpecialTransition) {
                transitionTileImage = terrainImage;
            } else {
                /*
                 * When doing the transitions for NW and SE, the base terrain should
                 * be the blended version of the neighbouring tiles. That is, the
                 * result after applying the transitions for NE and SW.
                 */
                
                transitionTileImage = new BufferedImage(terrainImage.getWidth(), terrainImage.getHeight(), BufferedImage.TYPE_INT_ARGB);
                final Graphics2D tranitionG2d = transitionTileImage.createGraphics();
                
                if (tile.isLand() || borderingTile.isLand()) {
                    tranitionG2d.drawImage(terrainImage, 0, 0, null);
                } else {
                    /*
                     * This handles the case where neither the tile nor the borderingTile are land tiles,
                     * but they are adjacent to land tiles.
                     * 
                     *  In this case, we need to include the beach in the transition. This only happens for
                     *  corners, but we are never-the-less using a mask for the entire edge. The reason being
                     *  that the terrain mask applied last to the transitionTileImage (before returning it)
                     *  reduces the edges to only the relevant two corners.
                     */
                    if (beachTransitionNe) {
                        tranitionG2d.drawImage(getSpecialBeachBaseTileTransitionImage(Direction.NE), 0, 0, null);
                    }
                    if (beachTransitionSw) {
                        tranitionG2d.drawImage(getSpecialBeachBaseTileTransitionImage(Direction.SW), 0, 0, null);
                    }
                }
                
                /*+
                 * These two includes the neighboring tiles to the bordering tiles, so that
                 * we get the correct blended transition for the cornes.
                 * 
                 * This is only needed for corners, but we are never-the-less using a mask for
                 * the entire edge. The reason being that the terrain mask applied last to the
                 * transitionTileImage (before returning it) reduces the edges to only the
                 * relevant two corners.
                 */
                final BufferedImage transitionImage2 = getBaseTileTransitionImage(borderingTile, Direction.NE, false, false);
                if (transitionImage2 != null) {
                    tranitionG2d.drawImage(transitionImage2, 0, 0, null);
                }
                final BufferedImage transitionImage3 = getBaseTileTransitionImage(borderingTile, Direction.SW, false, false);
                if (transitionImage3 != null) {
                    tranitionG2d.drawImage(transitionImage3, 0, 0, null);
                }
                
                tranitionG2d.dispose();
            }
            
            final BufferedImage terrainMaskImage = imageCache.getCachedImage(terrainMaskResource, lib.getTileSize(), false, maskVariationNumber);
            return ImageUtils.imageWithAlphaFromMask(transitionTileImage, terrainMaskImage);
        });

        return transitionImage;
    }

    /**
     * Checks if the beach should be included when making the transition for a corner.
     * 
     * @param tile The tile that should get a transition.
     * @param borderingTile The main tile used for making the transition (the neighboring tiles
     *      are used as well when making corners).
     * @param direction The direction to the neighboring tile that should be checked in order to
     *      determine if a beach corner transition is needed.
     * @return {@code true} if the beach should be rendered at the corner, and {@code false} otherwise.
     */
    private boolean shouldIncludeSpecialBeachTransitionInDirection(Tile tile, Tile borderingTile, Direction direction) {
        if (tile.isLand() || borderingTile.isLand()) {
            return false;
        }
        final Tile tileNeighbour = tile.getNeighbourOrNull(direction);
        final Tile borderingTileNeighbour = borderingTile.getNeighbourOrNull(direction);
        return tileNeighbour != null && tileNeighbour.isLand()
                || borderingTileNeighbour != null && borderingTileNeighbour.isLand();
    }

    /**
     * Gets the beach overlay to use when making base tile transitions with beach.
     */
    private BufferedImage getSpecialBeachBaseTileTransitionImage(Direction direction) {
        final String transitionKey = "specialBeachTransition." + direction.toString() + "$gen";
        final BufferedImage resultImage = imageCache.getCachedImageOrGenerate(transitionKey, lib.getTileSize(), false, direction.ordinal(), () -> {
            final BufferedImage beachImage = imageCache.getCachedImage(ImageLibrary.getBeachCenterImageResource(), lib.getTileSize(), false, 0);
            return ImageUtils.imageWithAlphaFromMask(beachImage, getSpecialBeachBaseTileTransitionMask(direction));
        });
        
        return resultImage;
    }
    
    private BufferedImage getSpecialBeachBaseTileTransitionMask(Direction direction) {
        final String key = "image.mask.special.beach." + direction.toString().toLowerCase();
        return this.imageCache.getSizedImage(key, lib.getTileSize(), false);
    }

    /**
     * Gets the terrain image resource, or the beach, in order to create transitions.
     */
    private ImageResource getTerrainOrBeachImageResource(Tile tile, final Tile borderingTile) {
        if (borderingTile == null) {
            return null;
        }
        final ImageResource terrainImageResource;
        final boolean notABeachTransition = borderingTile.isLand() || !borderingTile.isLand() && !tile.isLand();;
        if (notABeachTransition) {
            terrainImageResource = ImageCache.getImageResource(ImageLibrary.getTerrainImageKey(borderingTile.getType()));
        } else {
            terrainImageResource = ImageLibrary.getBeachCenterImageResource();
        }
        return terrainImageResource;
    }

}
