package pathgame.tilemap;

import java.util.ArrayList;
import java.util.Random;

public class TileMapGenerator
{
    private static long seed = 1;
    
    /** Returns a random generated map
     *
     * @param width the width of the map
     * @param height the height of the map
     * @param towns the amount of towns
     * @return a random generated map
     */
    public static TileMap getMap(int width, int height, int towns)
    {
        return getMap(width, height, seed++, towns);
    }
    
    /** Returns a random generated map
     *
     * @param width the width of the map
     * @param height the height of the map
     * @param seed the seed for the random number generator
     * @param towns the amount of towns
     * @return a random generated map
     */
    public static TileMap getMap(int width, int height, long seed, int towns)
    {
        Random r = new Random(seed);
        
        HighMap highMap = HighMap.generate(seed, width, height);
        
        TileMap map = new TileMap(width, height);
        for(int x = 0; x < width; x++)
        {
            for(int y = 0; y < height; y++)
            {
                if(highMap.get(x, y) < 0.15f)
                {
                    map.setTile(x, y, Tiles.DEEP_WATER);
                }
                else if(highMap.get(x, y) < 0.3f)
                {
                    map.setTile(x, y, Tiles.SHALLOW_WATER);
                }
                else if(highMap.get(x, y) < 0.7f)
                {
                    map.setTile(x, y, randomGrass(r));
                }
                else if(highMap.get(x, y) < 0.85f)
                {
                    map.setTile(x, y, Tiles.HILL);
                }
                else
                {
                    map.setTile(x, y, Tiles.MOUNTAIN);
                }
            }
        }
        
        generateHomeTown(map, r);
        
        int forestSize = ((width + height) / 2) / 10;
        forestSize *= forestSize;
        generateForest(map, r, forestSize, 10, 2, Tiles.FOREST);
        generateForest(map, r, forestSize, 5, 2, Tiles.SWAMP, Tiles.SWAMP, Tiles.SWAMP_DECO, Tiles.SWAMP_TREE);
        
        generateTowns(map, r, towns);
        generatePorts(map, r);
        generatePaths(map, r);
        
        removeBadSwampTree(map);
        return map;
    }
    
    private static Tile randomGrass(Random r)
    {
        if(r.nextFloat() < 0.3f)
        {
            return randomTile(Tiles.GRASS_VARIANTS, r);
        }
        return Tiles.GRASS;
    }
    
    private static void generateForest(TileMap map, Random r, int depth, int placements, int jumpRadius, Tile... t)
    {
        for(int i = 0; i < placements; i++)
        {
            int x = r.nextInt(map.getWidth());
            int y = r.nextInt(map.getHeight());
            while(map.getTile(x, y).getForestReplaceChance() < 1.0f)
            {
                x = r.nextInt(map.getWidth());
                y = r.nextInt(map.getHeight());
            }
            
            for(int j = 0; j < depth; j++)
            {
                int oldX = x;
                int oldY = y;

                x += r.nextInt(jumpRadius * 2 + 1) - jumpRadius;
                y += r.nextInt(jumpRadius * 2 + 1) - jumpRadius;
                x = Math.min(Math.max(x, 0), map.getWidth() - 1);
                y = Math.min(Math.max(y, 0), map.getHeight() - 1);
                
                if(r.nextFloat() < map.getTile(x, y).getForestReplaceChance())
                {
                    map.setTile(x, y, randomTile(t, r));
                    placeForest(map, r, x - 1, y, t);
                    placeForest(map, r, x + 1, y, t);
                    placeForest(map, r, x, y - 1, t);
                    placeForest(map, r, x, y + 1, t);
                }
                else
                {
                    x = oldX;
                    y = oldY;
                }
            }
        }
    }
    
    private static void removeBadSwampTree(TileMap map)
    {
        for(int x = 0; x < map.getWidth(); x++)
        {
            for(int y = 0; y < map.getHeight(); y++)
            {
               if(map.getTile(x, y) == Tiles.SWAMP_TREE && 
                       ((x > 0 && map.getTile(x - 1, y).getRenderType() != TileRenderType.SWAMP) || 
                       (y > 0 && map.getTile(x, y - 1).getRenderType() != TileRenderType.SWAMP)))
               {
                   map.setTile(x, y, Tiles.SWAMP);
               }
            }
        }
    }
    
    private static Tile randomTile(Tile[] tiles, Random r)
    {
        if(tiles.length == 1)
        {
            return tiles[0];
        }
        return tiles[r.nextInt(tiles.length)];
    }
    
    private static void placeForest(TileMap map, Random r, int x, int y, Tile... t)
    {
        if(x >= 0 && x < map.getWidth() && y >= 0 && y < map.getHeight())
        {
            if(r.nextFloat() * 2 < map.getTile(x, y).getForestReplaceChance())
            {
                map.setTile(x, y, randomTile(t, r));
            }
        }
    }
    
    private static void generateTowns(TileMap map, Random r, int towns)
    {
        int failCounter = 0;
        while(towns > 0 && failCounter < 100)
        {
            int x = r.nextInt(map.getWidth());
            int y = r.nextInt(map.getHeight());
            if(map.getTile(x, y).canHostTown() && checkNearbyTowns(map, x, y, 2))
            {
                map.setTile(x, y, Tiles.TOWN);
                towns--;
                failCounter = 0;
            }
            else
            {
                failCounter++;
            }
        }
    }
    
    private static boolean checkNearbyTowns(TileMap map, int x, int y, int radius)
    {
        int startX = Math.max(x - radius, 0);
        int startY = Math.max(y - radius, 0);
        int endX = Math.min(x + radius, map.getWidth() - 1);
        int endY = Math.min(y + radius, map.getHeight() - 1);
        for(int mx = startX; mx <= endX; mx++)
        {
            for(int my = startY; my <= endY; my++)
            {
                if(map.getTile(mx, my) == Tiles.TOWN)
                {
                    return false;
                }
            }
        }
        return true;
    }
    
    private static boolean isWater(TileMap map, int x, int y)
    {
        TileType type = map.getTile(x, y).getType();
        return type == TileType.DEEP_WATER || type == TileType.SHALLOW_WATER;
    }
    
    private static boolean isNeighbourWater(TileMap map, int x, int y)
    {
        return (x - 1 >= 0 && isWater(map, x - 1, y)) ||
                (x + 1 < map.getWidth() && isWater(map, x + 1, y)) ||
                (y - 1 >= 0 && isWater(map, x, y - 1)) ||
                (y + 1 < map.getHeight() && isWater(map, x, y + 1));
    }
    
    private static void generatePorts(TileMap map, Random r)
    {
        boolean[][] visited = new boolean[map.getWidth()][map.getHeight()];
        for(int x = 0; x < map.getWidth(); x++)
        {
            for(int y = 0; y < map.getHeight(); y++)
            {
                if(!visited[x][y] && isWater(map, x, y))
                {
                    getLake(map, r, x, y, visited);
                }
                visited[x][y] = true;
            }
        }
    }
    
    private static int waterSize;
    private static int waterMinX;
    private static int waterMinY;
    private static int waterMaxX;
    private static int waterMaxY;
    
    private static class Location
    {
        private final int x;
        private final int y;
        
        public Location(int x, int y)
        {
            this.x = x;
            this.y = y;
        }
        
        public double getQuaredDistance(int ox, int oy)
        {
            return (ox - x) * (ox - x) + (oy - y) * (oy - y);
        }
    }
    
    private static double minSquaredDistance(ArrayList<Location> locs, int x, int y)
    {
        double min = Double.MAX_VALUE;
        for(Location loc : locs)
        {
            double d = loc.getQuaredDistance(x, y);
            if(d < min)
            {
                min = d;
            }
        }
        return min;
    }
    
    private static void getLake(TileMap map, Random r, int x, int y, boolean[][] visited)
    {
        waterSize = 0;
        waterMinX = x;
        waterMinY = y;
        waterMaxX = x;
        waterMaxY = y;
        scanWaterTiles(map, x, y, visited);
        // water outlines can be a port too
        waterMinX = Math.max(0, waterMinX - 1);
        waterMinY = Math.max(0, waterMinY - 1);
        waterMaxX = Math.min(map.getWidth() - 1, waterMaxX + 1);
        waterMaxY = Math.min(map.getHeight() - 1, waterMaxY + 1);
        
        ArrayList<Location> locs = new ArrayList<>();
        
        //System.out.println(String.format("Lake Size: %d, (%d, %d) -> (%d, %d)", 
        //        waterSize, waterMinX, waterMinY, waterMaxX, waterMaxY));
        int ports = waterSize / 30;
        int diffX = waterMaxX - waterMinX + 1;
        int diffY = waterMaxY - waterMinY + 1;
        int failCounter = 0;
        while(ports > 0 && failCounter < 100)
        {
            int rx = waterMinX + r.nextInt(diffX);
            int ry = waterMinY + r.nextInt(diffY);
            if(map.getTile(rx, ry).canHostTown() && isNeighbourWater(map, rx, ry) && minSquaredDistance(locs, rx, ry) > 25)
            {
                locs.add(new Location(rx, ry));
                map.setTile(rx, ry, Tiles.PORT);
                ports--;
                failCounter = 0;
            }
            else
            {
                failCounter++;
            }
        }
    }
    
    private static void scanWaterTiles(TileMap map, int x, int y, boolean[][] visited)
    {
        if(!visited[x][y] && isWater(map, x, y))
        {
            visited[x][y] = true;
            waterSize++;
            waterMinX = Math.min(x, waterMinX);
            waterMinY = Math.min(y, waterMinY);
            waterMaxX = Math.max(x, waterMaxX);
            waterMaxY = Math.max(y, waterMaxY);
            
            if(x - 1 >= 0)
            {
                scanWaterTiles(map, x - 1, y, visited);
            }
            if(x + 1 < map.getWidth())
            {
                scanWaterTiles(map, x + 1, y, visited);
            }
            if(y - 1 >= 0)
            {
                scanWaterTiles(map, x, y - 1, visited);
            }
            if(y + 1 < map.getHeight())
            {
                scanWaterTiles(map, x, y + 1, visited);
            }
        }
    }
    
    private static void generateHomeTown(TileMap map, Random r)
    {
        int failCounter = 0;
        while(failCounter < 100)
        {
            int x = r.nextInt(map.getWidth());
            int y = r.nextInt(map.getHeight());
            if(map.getTile(x, y).canHostTown())
            {
                map.setTile(x, y, Tiles.HOME_TOWN);
                map.setHomeTown(x, y);
                return;
            }
            else
            {
                failCounter++;
            }
        }
        map.setTile(0, 0, Tiles.HOME_TOWN);
        map.setHomeTown(0, 0);
    }
    
    private static boolean isPath(TileMap map, int x, int y)
    {
        return x >= 0 && y >= 0 && x < map.getWidth() && y < map.getHeight() && map.getTile(x, y).isPath();
    }
    
    private static void generatePaths(TileMap map, Random r)
    {
        int paths = (map.getHeight() + map.getWidth()) / 6 + 2;
        
        // generate paths with random direction
        for(int i = 0; i < paths; i++)
        {
            int x = r.nextInt(map.getWidth());
            int y = r.nextInt(map.getHeight());
            while(!map.getTile(x, y).canHostPath())
            {
                x = r.nextInt(map.getWidth());
                y = r.nextInt(map.getHeight());
            }

            float dx = (r.nextFloat() * 0.75f + 0.25f) * (r.nextBoolean() ? -1 : 1);
            float dy = (r.nextFloat() * 0.75f + 0.25f) * (r.nextBoolean() ? -1 : 1);
            generatePathDiretion(map, r, x, y, dx, dy);
        }
        // destroy path 2x2 blocks
        destroyPathBlocks(map);
        destroyPathBlocks(map);
        
        // swap paths depending on neighbours
        for(int x = 0; x < map.getWidth(); x++)
        {
            for(int y = 0; y < map.getHeight(); y++)
            {
                if(map.getTile(x, y).isPath())
                {
                    map.setTile(x, y, Tiles.getPath(
                            isPath(map, x, y - 1), isPath(map, x + 1, y), 
                            isPath(map, x, y + 1), isPath(map, x - 1, y)));
                }
            }
        }
    }
    
    private static void destroyPathBlocks(TileMap map)
    {
        for(int x = 0; x < map.getWidth() - 1; x++)
        {
            for(int y = 0; y < map.getHeight() - 1; y++)
            {
                if(map.getTile(x, y).isPath() && map.getTile(x + 1, y).isPath() && 
                        map.getTile(x, y + 1).isPath() && map.getTile(x + 1, y + 1).isPath())
                {
                    if(!isPath(map, x - 1, y) && !isPath(map, x, y - 1))
                    {
                        map.setTile(x, y, Tiles.GRASS);
                        continue;
                    }
                    if(!isPath(map, x + 1, y - 1) && !isPath(map, x + 2, y))
                    {
                        map.setTile(x + 1, y, Tiles.GRASS);
                        continue;
                    }
                    if(!isPath(map, x - 1, y + 1) && !isPath(map, x, y + 2))
                    {
                        map.setTile(x, y + 1, Tiles.GRASS);
                        continue;
                    }
                    if(!isPath(map, x + 2, y + 1) && !isPath(map, x + 1, y + 2))
                    {
                        map.setTile(x + 1, y + 1, Tiles.GRASS);
                    }
                }
            }
        }
    }
    
    private static void generatePathDiretion(TileMap map, Random r, float x, float y, float dx, float dy)
    {
        while(true)
        {
            int tileX = (int) x;
            int tileY = (int) y;
            
            if(tileX < 0 || tileY < 0 || tileX >= map.getWidth() || tileY >= map.getHeight() || !map.getTile(tileX, tileY).canHostPath())
            {
                break;
            }
            
            map.setTile(tileX, tileY, Tiles.PATH_N_E_S_W);
            
            while(tileX == (int) x && tileY == (int) y)
            {
                if(r.nextBoolean())
                {
                    x += dx;
                }
                else
                {
                    y += dy;
                }
            }
        }
    }
}
