// • ▌ ▄ ·.  ▄▄▄·  ▄▄ • ▪   ▄▄· ▄▄▄▄·  ▄▄▄·  ▐▄▄▄  ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀  █▪▀▀▀ ▀  ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀  ▀  ▀ ▀▀  █▪ ▀▀▀
//      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.FastMath;
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 final HashMap<Integer, HeightMap> heightmapByLoadNum = new HashMap<>();
    // Heightmap data for all zones.
    public static float SCALEVALUE = 1.0f / 255;

    // Bootstrap Tracking
    public static int heightMapsCreated = 0;
    public static HeightMap PlayerCityHeightMap;

    // Heightmap data for this heightmap
    public final int heightMapID;
    public final int maxHeight;
    public final int fullExtentsX;
    public final int fullExtentsY;
    public final int zoneLoadID;
    public BufferedImage heightmapImage;
    public float bucketWidthX;
    public float bucketWidthY;
    public float seaLevel = 0;
    public 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.halfExtents;
        int halfExtentsY = (int) Enum.CityBoundsType.ZONE.halfExtents;
        this.zoneLoadID = 0;
        this.seaLevel = 0;
        this.zone_minBlend = 0;
        this.zone_maxBlend = 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 interpolatedParentTerrainHeight;

        //  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);
        interpolatedTerrainHeight += heightMapZone.worldAltitude;

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

        if (Bounds.collide(worldLoc, heightMapZone.maxBlend) == true)
            return interpolatedTerrainHeight;

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

        return interpolatePLANAR(worldLoc, heightMapZone, zoneLoc, interpolatedTerrainHeight);

    }

    private static float interpolatePLANAR(Vector3fImmutable worldLoc, Zone heightMapZone, Vector2f zoneLoc, float interpolatedTerrainHeight) {
        Zone parentZone;
        float interpolatedParentTerrainHeight;
        parentZone = HeightMap.getNextZoneWithTerrain(heightMapZone.getParent());
        interpolatedParentTerrainHeight = HeightMap.getWorldHeight(parentZone, worldLoc);
        interpolatedParentTerrainHeight += parentZone.worldAltitude;

        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);

        float maxBlendArea = (heightMapZone.maxBlend.getHalfExtents().x) *
                (heightMapZone.maxBlend.getHalfExtents().y);
        float currentArea = (blendBounds.getHalfExtents().x) *
                (blendBounds.getHalfExtents().y);
        float zoneArea = (heightMapZone.getBounds().getHalfExtents().x) *
                (heightMapZone.getBounds().getHalfExtents().y);

        blendBounds.release();

        float blendDelta = zoneArea - maxBlendArea;
        float currentDelta = zoneArea - currentArea;

        float percentage;

        if (currentDelta != 0 && blendDelta != 0)
            percentage = currentDelta / blendDelta;
        else
            percentage = 0.0f;

        interpolatedTerrainHeight = FastMath.LERP(percentage, interpolatedTerrainHeight, interpolatedParentTerrainHeight);
        return interpolatedTerrainHeight;
    }

    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) {

        // Clamp values.

        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;

        gridSquare = getGridSquare(zoneLoc);

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

        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;

        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;
    }

}