// • ▌ ▄ ·.  ▄▄▄·  ▄▄ • ▪   ▄▄· ▄▄▄▄·  ▄▄▄·  ▐▄▄▄  ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀  █▪▀▀▀ ▀  ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀  ▀  ▀ ▀▀  █▪ ▀▀▀
//      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.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;

public class HeightMap {

    // Class variables

    public static final float SCALEVALUE = 1.0f / 256f;
    // 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 final float outsetX;
    private final float outsetZ;
    private int[][] pixelColorValues;

    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.outsetX = rs.getFloat("outsetX");
        this.outsetZ = rs.getFloat("outsetZ");


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

        // We needed to flip the image as OpenGL and Shadowbane both use the bottom left corner as origin.

        // this.heightmapImage = MapLoader.flipImage(this.heightmapImage);

        // Calculate the data we do not load from table

        float numOfBuckets = this.heightmapImage.getWidth() - 1;
        float calculatedWidth = this.fullExtentsX / numOfBuckets;
        this.bucketWidthX = calculatedWidth;
        this.bucketWidthY = this.bucketWidthX; // This makes no sense.

        // 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;
        this.outsetX = 128;
        this.outsetZ = 128;

        // 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;
        this.outsetX = 0;
        this.outsetZ = 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) {

        Vector2f parentLoc = new Vector2f(-1, -1);

        if (currentZone == null)
            return 0;

        currentZone = getNextZoneWithTerrain(currentZone);

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

        Zone parentZone = getNextZoneWithTerrain(currentZone.getParent());
        HeightMap heightMap = currentZone.getHeightMap();

        if ((heightMap == null) || (currentZone == ZoneManager.getSeaFloor()))
            return currentZone.getAbsY();

        Vector2f zoneLoc = ZoneManager.worldToZoneSpace(worldLoc, currentZone);
        Vector3fImmutable localLocFromCenter = ZoneManager.worldToLocal(worldLoc, currentZone);

        if ((parentZone != null) && (parentZone.getHeightMap() != null))
            parentLoc = ZoneManager.worldToZoneSpace(worldLoc, parentZone);

        float interaltitude = currentZone.getHeightMap().getInterpolatedTerrainHeight(zoneLoc);

        float worldAltitude = currentZone.worldAltitude;

        float realWorldAltitude = interaltitude + worldAltitude;

        //OUTSET

        if (parentZone != null) {

            float parentXRadius = currentZone.getBounds().getHalfExtents().x;
            float parentZRadius = currentZone.getBounds().getHalfExtents().y;

            float offsetX = Math.abs((localLocFromCenter.x / parentXRadius));
            float offsetZ = Math.abs((localLocFromCenter.z / parentZRadius));

            float bucketScaleX = heightMap.outsetX / parentXRadius;
            float bucketScaleZ = heightMap.outsetZ / parentZRadius;

            float outsideGridSizeX = 1 - bucketScaleX; //32/256
            float outsideGridSizeZ = 1 - bucketScaleZ;
            float weight;

            double scale;

            if (offsetX > outsideGridSizeX && offsetX > offsetZ) {
                weight = (offsetX - outsideGridSizeX) / bucketScaleX;
                scale = Math.atan2((.5 - weight) * 3.1415927, 1);

                float scaleChild = (float) ((scale + 1) * .5);
                float scaleParent = 1 - scaleChild;

                float parentAltitude = parentZone.getHeightMap().getInterpolatedTerrainHeight(parentLoc);
                float parentCenterAltitude = parentZone.getHeightMap().getInterpolatedTerrainHeight(ZoneManager.worldToZoneSpace(currentZone.getLoc(), parentZone));

                parentCenterAltitude += currentZone.getYCoord();
                parentCenterAltitude += interaltitude;

                float firstScale = parentAltitude * scaleParent;
                float secondScale = parentCenterAltitude * scaleChild;
                float outsetALt = firstScale + secondScale;

                outsetALt += currentZone.getParent().worldAltitude;
                realWorldAltitude = outsetALt;

            } else if (offsetZ > outsideGridSizeZ) {

                weight = (offsetZ - outsideGridSizeZ) / bucketScaleZ;
                scale = Math.atan2((.5 - weight) * 3.1415927, 1);

                float scaleChild = (float) ((scale + 1) * .5);
                float scaleParent = 1 - scaleChild;
                float parentAltitude = parentZone.getHeightMap().getInterpolatedTerrainHeight(parentLoc);
                float parentCenterAltitude = parentZone.getHeightMap().getInterpolatedTerrainHeight(ZoneManager.worldToZoneSpace(currentZone.getLoc(), parentZone));

                parentCenterAltitude += currentZone.getYCoord();
                parentCenterAltitude += interaltitude;
                float firstScale = parentAltitude * scaleParent;
                float secondScale = parentCenterAltitude * scaleChild;
                float outsetALt = firstScale + secondScale;

                outsetALt += currentZone.getParent().worldAltitude;
                realWorldAltitude = outsetALt;
            }
        }

        return realWorldAltitude;
    }

    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 > this.fullExtentsX)
            return -1;

        if (zoneLoc.y < 0 || zoneLoc.y > this.fullExtentsY)
            return -1;

        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 > maxX)
            gridX = maxX;
        if (gridY > maxY)
            gridY = maxY;

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

}