// • ▌ ▄ ·.  ▄▄▄·  ▄▄ • ▪   ▄▄· ▄▄▄▄·  ▄▄▄·  ▐▄▄▄  ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀  █▪▀▀▀ ▀  ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀  ▀  ▀ ▀▀  █▪ ▀▀▀
//      Magicbane Emulator Project © 2013 - 2022
//                www.magicbane.com

package engine.InterestManagement;

import engine.Enum;
import engine.gameManager.ConfigManager;
import engine.gameManager.DbManager;
import engine.gameManager.ZoneManager;
import engine.math.Bounds;
import engine.math.Vector2f;
import engine.math.Vector3fImmutable;
import engine.objects.Zone;
import org.pmw.tinylog.Logger;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;

import static java.lang.Math.abs;

public class HeightMap {

    // Class variables

    public static float SCALEVALUE = 1.0f / 256;
    // Heightmap data for all zones.

    public static final HashMap<Integer, HeightMap> heightmapByLoadNum = new HashMap<>();

    // Bootstrap Tracking

    public static int heightMapsCreated = 0;
    public static HeightMap PlayerCityHeightMap;

    // Heightmap data for this heightmap

    public BufferedImage heightmapImage;

    private final int heightMapID;
    private final int maxHeight;
    private final int fullExtentsX;
    private final int fullExtentsY;

    private float bucketWidthX;
    private float bucketWidthY;
    private final int zoneLoadID;
    private float seaLevel = 0;
    private int[][] pixelColorValues;

    public float zone_minBlend;
    public float zone_maxBlend;

    public HeightMap(ResultSet rs) throws SQLException {

        this.heightMapID = rs.getInt("heightMapID");
        this.maxHeight = rs.getInt("maxHeight");
        int halfExtentsX = rs.getInt("xRadius");
        int halfExtentsY = rs.getInt("zRadius");
        this.zoneLoadID = rs.getInt("zoneLoadID");
        this.seaLevel = rs.getFloat("seaLevel");

        this.zone_minBlend = rs.getFloat("outsetZ");
        this.zone_maxBlend = rs.getFloat("outsetX");

        // Cache the full extents to avoid the calculation

        this.fullExtentsX = halfExtentsX * 2;
        this.fullExtentsY = halfExtentsY * 2;

        this.heightmapImage = null;
        File imageFile = new File(ConfigManager.DEFAULT_DATA_DIR + "heightmaps/" + this.heightMapID + ".bmp");

        // early exit if no image file was found.  Will log in caller.

        if (!imageFile.exists())
            return;

        // load the heightmap image.

        try {
            this.heightmapImage = ImageIO.read(imageFile);
        } catch (IOException e) {
            Logger.error("***Error loading heightmap data for heightmap " + this.heightMapID + e);
        }

        // Calculate the data we do not load from table

        float numOfBucketsX = this.heightmapImage.getWidth() - 1;
        float calculatedWidthX = this.fullExtentsX / numOfBucketsX;
        this.bucketWidthX = calculatedWidthX;

        float numOfBucketsY = this.heightmapImage.getHeight() - 1;
        float calculatedWidthY = this.fullExtentsY / numOfBucketsY;
        this.bucketWidthY = calculatedWidthY;

        // Generate pixel array from image data

        generatePixelData(this);

        HeightMap.heightmapByLoadNum.put(this.zoneLoadID, this);

        heightMapsCreated++;
    }

    //Created for PlayerCities
    public HeightMap() {

        this.heightMapID = 999999;
        this.maxHeight = 5; // for real...
        int halfExtentsX = (int) Enum.CityBoundsType.ZONE.extents;
        int halfExtentsY = (int) Enum.CityBoundsType.ZONE.extents;
        this.zoneLoadID = 0;
        this.seaLevel = 0;

        // Cache the full extents to avoid the calculation

        this.fullExtentsX = halfExtentsX * 2;
        this.fullExtentsY = halfExtentsY * 2;

        this.heightmapImage = null;

        // Calculate the data we do not load from table

        this.bucketWidthX = halfExtentsX;
        this.bucketWidthY = halfExtentsY;

        this.pixelColorValues = new int[this.fullExtentsX][this.fullExtentsY];

        for (int y = 0; y < this.fullExtentsY; y++) {
            for (int x = 0; x < this.fullExtentsX; x++) {
                pixelColorValues[x][y] = 255;
            }
        }

    }

    public HeightMap(Zone zone) {

        this.heightMapID = 999999;
        this.maxHeight = 0;
        int halfExtentsX = (int) zone.getBounds().getHalfExtents().x;
        int halfExtentsY = (int) zone.getBounds().getHalfExtents().y;
        this.zoneLoadID = 0;
        this.seaLevel = 0;

        // Cache the full extents to avoid the calculation

        this.fullExtentsX = halfExtentsX * 2;
        this.fullExtentsY = halfExtentsY * 2;

        this.heightmapImage = null;

        // Calculate the data we do not load from table

        this.bucketWidthX = halfExtentsX;
        this.bucketWidthY = halfExtentsY;

        this.pixelColorValues = new int[this.fullExtentsX][this.fullExtentsY];

        for (int y = 0; y < this.fullExtentsY; y++) {
            for (int x = 0; x < this.fullExtentsX; x++) {
                pixelColorValues[x][y] = 0;
            }
        }

    }

    public static void GeneratePlayerCityHeightMap() {

        HeightMap.PlayerCityHeightMap = new HeightMap();

    }

    public static void GenerateCustomHeightMap(Zone zone) {

        HeightMap heightMap = new HeightMap(zone);

        HeightMap.heightmapByLoadNum.put(zone.getLoadNum(), heightMap);

    }

    public static Zone getNextZoneWithTerrain(Zone zone) {

        Zone nextZone = zone;

        if (zone.getHeightMap() != null)
            return zone;

        if (zone.equals(ZoneManager.getSeaFloor()))
            return zone;

        while (nextZone.getHeightMap() == null)
            nextZone = nextZone.getParent();

        return nextZone;
    }

    public static float getWorldHeight(Zone currentZone, Vector3fImmutable worldLoc) {

        Zone heightMapZone;
        Zone parentZone;
        float worldHeight;
        float parentHeight;

        //  Seafloor is rather flat.

        if (currentZone == ZoneManager.getSeaFloor())
            return currentZone.worldAltitude;

        // Retrieve the next zone with a heightmap attached.
        // Zones without a heightmap use the next zone up the
        // tree to calculate heights from.

        heightMapZone = getNextZoneWithTerrain(currentZone);

        // Transform world loc into zone space coordinate system

        Vector2f zoneLoc = ZoneManager.worldToZoneSpace(worldLoc, heightMapZone);

        // Interpolate height for this position using pixel array.

        float interpolatedTerrainHeight = heightMapZone.getHeightMap().getInterpolatedTerrainHeight(zoneLoc);

        // Heightmap blending is based on distance to edge of zone.

        if (Bounds.collide(worldLoc, heightMapZone.maxBlend) == true) {
            worldHeight = interpolatedTerrainHeight + heightMapZone.worldAltitude;
            return worldHeight;
        }

        // We will need the parent height if we got this far into the method

        parentZone = HeightMap.getNextZoneWithTerrain(heightMapZone.getParent());
        parentHeight = HeightMap.getWorldHeight(parentZone, worldLoc);

        if (Bounds.collide(worldLoc, heightMapZone.minBlend) == true) {

            // How far into blend zone are we?

            Bounds blendBounds = Bounds.borrow();
            zoneLoc.x = abs(zoneLoc.x);
            zoneLoc.y = abs(zoneLoc.x);
            blendBounds.setBounds(new Vector2f(heightMapZone.absX, heightMapZone.absZ), zoneLoc, 0.0f);


        }

        // Position returned from Heightmap engine is relative to zone world height

        return interpolatedTerrainHeight + heightMapZone.worldAltitude;

    }

    public static float getWorldHeight(Vector3fImmutable worldLoc) {

        Zone currentZone = ZoneManager.findSmallestZone(worldLoc);

        if (currentZone == null)
            return 0;
        return getWorldHeight(currentZone, worldLoc);

    }

    public static void loadAlHeightMaps() {

        // Load the heightmaps into staging hashmap keyed by HashMapID

        DbManager.HeightMapQueries.LOAD_ALL_HEIGHTMAPS();

        //generate static player city heightmap.

        HeightMap.GeneratePlayerCityHeightMap();

        // Clear all heightmap image data as it's no longer needed.

        for (HeightMap heightMap : HeightMap.heightmapByLoadNum.values()) {
            heightMap.heightmapImage = null;
        }

        Logger.info(HeightMap.heightmapByLoadNum.size() + " Heightmaps cached.");
    }

    public static boolean isLocUnderwater(Vector3fImmutable currentLoc) {

        float localAltitude = HeightMap.getWorldHeight(currentLoc);
        Zone zone = ZoneManager.findSmallestZone(currentLoc);

        return localAltitude < zone.getSeaLevel();
    }

    private static void generatePixelData(HeightMap heightMap) {

        Color color;

        // Generate altitude lookup table for this heightmap

        heightMap.pixelColorValues = new int[heightMap.heightmapImage.getWidth()][heightMap.heightmapImage.getHeight()];

        for (int y = 0; y < heightMap.heightmapImage.getHeight(); y++) {
            for (int x = 0; x < heightMap.heightmapImage.getWidth(); x++) {

                color = new Color(heightMap.heightmapImage.getRGB(x, y));
                heightMap.pixelColorValues[x][y] = color.getRed();
            }
        }

    }

    public Vector2f getGridSquare(Vector2f zoneLoc) {

        if (zoneLoc.x < 0)
            zoneLoc.setX(0);

        if (zoneLoc.x >= this.fullExtentsX)
            zoneLoc.setX(this.fullExtentsX);

        if (zoneLoc.y < 0)
            zoneLoc.setY(0);

        if (zoneLoc.y > this.fullExtentsY)
            zoneLoc.setY(this.fullExtentsY);

        // Flip Y coordinates

        zoneLoc.setY(this.fullExtentsY - zoneLoc.y);

        float xBucket = (zoneLoc.x / this.bucketWidthX);
        float yBucket = (zoneLoc.y / this.bucketWidthY);

        return new Vector2f(xBucket, yBucket);
    }

    public float getInterpolatedTerrainHeight(Vector2f zoneLoc) {

        Vector2f gridSquare;

        if (zoneLoc.x < 0)
            zoneLoc.x = 0;

        if (zoneLoc.y < 0)
            zoneLoc.y = 0;

        if (zoneLoc.x > this.fullExtentsX)
            zoneLoc.x = this.fullExtentsX;

        if (zoneLoc.y > this.fullExtentsY)
            zoneLoc.y = this.fullExtentsY;

        int maxX = (int) (this.fullExtentsX / this.bucketWidthX);
        int maxY = (int) (this.fullExtentsY / this.bucketWidthY);

        gridSquare = getGridSquare(zoneLoc);

        int gridX = (int) gridSquare.x;
        int gridY = (int) gridSquare.y;

        if (gridX < 0)
            gridX = 0;
        if (gridY < 0)
            gridY = 0;

        if (gridX >= maxX)
            gridX = maxX - 1;
        if (gridY >= maxY)
            gridY = maxY - 1;

        float offsetX = (gridSquare.x - gridX);
        float offsetY = gridSquare.y - gridY;

        //get height of the 4 vertices.

        float topLeftHeight = 0;
        float topRightHeight = 0;
        float bottomLeftHeight = 0;
        float bottomRightHeight = 0;

        int nextY = gridY + 1;
        int nextX = gridX + 1;

        if (nextY > maxY)
            nextY = gridY;

        if (nextX > maxX)
            nextX = gridX;

        topLeftHeight = pixelColorValues[gridX][gridY];
        topRightHeight = pixelColorValues[nextX][gridY];
        bottomLeftHeight = pixelColorValues[gridX][nextY];
        bottomRightHeight = pixelColorValues[nextX][nextY];

        float interpolatedHeight;

        interpolatedHeight = topRightHeight * (1 - offsetY) * (offsetX);
        interpolatedHeight += (bottomRightHeight * offsetY * offsetX);
        interpolatedHeight += (bottomLeftHeight * (1 - offsetX) * offsetY);
        interpolatedHeight += (topLeftHeight * (1 - offsetX) * (1 - offsetY));

        interpolatedHeight *= (float) this.maxHeight * SCALEVALUE;  // Scale height

        return interpolatedHeight;
    }

    public float getBucketWidthX() {
        return bucketWidthX;
    }

    public float getBucketWidthY() {
        return bucketWidthY;
    }

    public int getHeightMapID() {
        return heightMapID;
    }

    public BufferedImage getHeightmapImage() {
        return heightmapImage;
    }

    public float getSeaLevel() {
        return seaLevel;
    }

}