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

package engine.InterestManagement;

import engine.gameManager.ZoneManager;
import engine.math.Vector2f;
import engine.math.Vector3fImmutable;
import engine.objects.Zone;
import org.pmw.tinylog.Logger;

import java.util.HashMap;

import static java.lang.Math.PI;

public class Terrain {

    // Class variables

    public static final HashMap<Integer, short[][]> _heightmap_pixel_cache = new HashMap<>();
    public short[][] terrain_pixel_data;
    public Vector2f terrain_size = new Vector2f();
    public Vector2f cell_size = new Vector2f();
    public Vector2f cell_count = new Vector2f();
    public float terrain_scale;
    public float min_blend;
    public float max_blend;
    public int heightmap;
    Zone zone;

    public Terrain(Zone zone) {

        this.zone = zone;

        this.heightmap = this.zone.terrain_image;

        // Configure PLANAR

        if (this.zone.terrain_type.equals("PLANAR"))
            this.heightmap = 1006300;

        this.terrain_size.x = this.zone.major_radius * 2;
        this.terrain_size.y = this.zone.minor_radius * 2;

        this.terrain_pixel_data = Terrain._heightmap_pixel_cache.get(heightmap);

        if (terrain_pixel_data == null)
            Logger.error("Pixel map empty for zone: " + zone.getObjectUUID() + ":" + zone.zoneName);

        this.cell_count.x = this.terrain_pixel_data.length - 1;
        this.cell_count.y = this.terrain_pixel_data[0].length - 1;

        this.cell_size.x = terrain_size.x / this.cell_count.x;
        this.cell_size.y = terrain_size.y / this.cell_count.y;

        this.max_blend = this.zone.max_blend / this.zone.major_radius;
        this.min_blend = this.zone.min_blend / this.zone.minor_radius;

        this.terrain_scale = this.zone.terrain_max_y / 255f;

    }

    public static Zone getNextZoneWithTerrain(Zone zone) {

        Zone terrain_zone = zone;

        if (zone == null)
            return ZoneManager.seaFloor;

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

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

        while (terrain_zone.terrain == null)
            terrain_zone = terrain_zone.parent;

        return terrain_zone;
    }

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

        Zone terrainZone;
        Zone parentZone;

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

        terrainZone = getNextZoneWithTerrain(currentZone);
        parentZone = getNextZoneWithTerrain(currentZone.parent);

        // Transform world loc into zone space coordinate system

        Vector2f terrainLoc = ZoneManager.worldToZoneSpace(worldLoc, terrainZone);
        Vector2f parentLoc = ZoneManager.worldToZoneSpace(worldLoc, parentZone);

        // Interpolate height for this position in terrain

        float interpolatedChildHeight = terrainZone.terrain.getInterpolatedTerrainHeight(terrainLoc);
        interpolatedChildHeight += terrainZone.worldAltitude;

        // Interpolate height for this position in parent

        float interpolatedParentTerrainHeight = parentZone.terrain.getInterpolatedTerrainHeight(parentLoc);
        interpolatedParentTerrainHeight += parentZone.worldAltitude;

        // Blend between heights

        return interpolatedChildHeight + interpolatedParentTerrainHeight * (1 - terrainZone.terrain.terrainBlend(terrainLoc));

    }

    public static float getWorldHeight(Vector3fImmutable worldLoc) {

        Zone currentZone = ZoneManager.findSmallestZone(worldLoc);

        if (currentZone == null)
            return 0;

        return getWorldHeight(currentZone, worldLoc);

    }

    public Vector2f getTerrainCell(Vector2f terrainLoc) {

        Vector2f terrain_cell = new Vector2f(terrainLoc.x / this.cell_size.x, terrainLoc.y / this.cell_size.y);

        // Clamp values when standing directly on max pole

        if (terrain_cell.x >= this.cell_count.x)
            terrain_cell.x = terrain_cell.x - 1;

        if (terrain_cell.x >= this.cell_count.y)
            terrain_cell.y = terrain_cell.y - 1;

        return terrain_cell;
    }

    public float getInterpolatedTerrainHeight(Vector2f terrainLoc) {

        float interpolatedHeight;

        Vector2f terrain_cell = getTerrainCell(terrainLoc);

        int gridX = (int) Math.floor(terrain_cell.x);
        int gridY = (int) Math.floor(terrain_cell.y);

        float offsetX = terrain_cell.x % 1;
        float offsetY = terrain_cell.y % 1;

        //get 4 surrounding vertices from the pixel array.

        float topLeftHeight;
        float topRightHeight;
        float bottomLeftHeight;
        float bottomRightHeight;

        topLeftHeight = terrain_pixel_data[gridX][gridY];
        topRightHeight = terrain_pixel_data[gridX + 1][gridY];
        bottomLeftHeight = terrain_pixel_data[gridX][gridY + 1];
        bottomRightHeight = terrain_pixel_data[gridX + 1][gridY + 1];

        // Interpolate between the 4 vertices

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

        interpolatedHeight *= this.terrain_scale; // Scale height

        return interpolatedHeight;
    }

    public float terrainBlend(Vector2f terrainLoc) {

        // Normalize terrain loc

        Vector2f normalizedLoc = new Vector2f(terrainLoc.x / this.terrain_size.x,
                terrainLoc.y / terrain_size.y);

        float minp = this.zone.min_blend / this.zone.major_radius;
        float maxp = this.zone.max_blend / this.zone.major_radius;

        float minpy = this.zone.min_blend / this.zone.minor_radius;
        float maxpy = this.zone.max_blend / this.zone.minor_radius;

        float xval;

        if (minp > 0.4f)
            xval = minp;
        else
            xval = Math.min(maxp, 0.4f);

        float yval;

        if (minpy > 0.4f)
            yval = minpy;
        else
            yval = Math.min(maxpy, 0.4f);

        float value;

        if (normalizedLoc.x <= 1 - xval || normalizedLoc.x <= normalizedLoc.y) {

            if (normalizedLoc.y < 1 - yval)
                return 1;

            value = (normalizedLoc.y - (1 - yval)) / yval;
        } else
            value = (normalizedLoc.x - (1 - xval)) / xval;

        value = (float) Math.atan((0.5f - value) * PI);

        return (value + 1) * 0.5f;
    }
}