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


// MB Dev Notes:
// The Terrain class handles lookups into the Heightmap data.
// It supports all current maps along with the differences in
// their parenting configuration.
//
// Heightmap images are stored on disk in /mb.data/heightmaps/

public class Terrain {
    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 Vector2f blend_values = new Vector2f();
    public Vector2f blend_ratio = new Vector2f();
    public int heightmap;
    Zone zone;

    public Terrain(Zone zone) {

        this.zone = zone;
        this.heightmap = this.zone.template.terrain_image;

        // Configure PLANAR zones to use the same 16x16 pixel image
        // that all similar terrains share.  (See JSON)

        if (this.zone.template.terrain_type.equals("PLANAR"))
            this.heightmap = 1006300;  // all 0

        // Load pixel data for this terrain from cache

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

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

        // Configure terrain based on zone properties

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

        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;

        // Blending configuration. These ratios are used to calculate
        // the blending area between child and parent terrains when
        // they are stitched together.

        this.blend_values.x = this.zone.template.max_blend;
        this.blend_values.y = this.zone.template.min_blend;

        Vector2f major_blend = new Vector2f(this.blend_values.x / this.zone.major_radius,
                this.blend_values.y / this.zone.major_radius);

        Vector2f minor_blend = new Vector2f(this.blend_values.x / this.zone.minor_radius,
                this.blend_values.y / this.zone.minor_radius);

        if (major_blend.y > 0.4f)
            blend_ratio.x = major_blend.y;
        else
            blend_ratio.x = Math.min(major_blend.x, 0.4f);

        if (minor_blend.y > 0.4f)
            blend_ratio.y = minor_blend.y;
        else
            blend_ratio.y = Math.min(minor_blend.x, 0.4f);

        // Scale coefficient for this terrain

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

    public static Zone getNextZoneWithTerrain(Zone zone) {

        // Not all zones have a terrain.  Some are for display only
        // and heights returned are from the parent heightmap.  This
        // is controlled in the JSON via the has_terrain_gen field.

        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 zone, Vector3fImmutable world_loc) {

        // Retrieve the next zone up in the tree with a terrain defined.

        Zone terrainZone = getNextZoneWithTerrain(zone);
        Zone parentZone = getNextZoneWithTerrain(zone.parent);

        // Transform world loc into zone space coordinate system

        Vector2f terrainLoc = ZoneManager.worldToTerrainSpace(world_loc, terrainZone);
        Vector2f parentLoc = ZoneManager.worldToTerrainSpace(world_loc, parentZone);

        // Offset from origin needed for blending function

        Vector2f terrainOffset = ZoneManager.worldToZoneOffset(world_loc, terrainZone);

        // Interpolate height for this position in both terrains

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

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

        // Blend between terrains

        float blendCoefficient = terrainZone.terrain.getTerrainBlendCoefficient(terrainOffset);

        float terrainHeight = interpolatedChildHeight * blendCoefficient;
        terrainHeight += interpolatedParentTerrainHeight * (1 - blendCoefficient);

        return terrainHeight;

    }

    public static float getWorldHeight(Vector3fImmutable world_loc) {

        Zone currentZone = ZoneManager.findSmallestZone(world_loc);

        return getWorldHeight(currentZone, world_loc);

    }

    public Vector2f getTerrainCell(Vector2f terrain_loc) {

        // Calculate terrain cell with offset

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

        // Clamp values when standing directly on pole

        terrain_cell.x = Math.max(0, Math.min(this.cell_count.x - 1, terrain_cell.x));
        terrain_cell.y = Math.max(0, Math.min(this.cell_count.y - 1, terrain_cell.y));

        return terrain_cell;
    }

    public float getInterpolatedTerrainHeight(Vector2f terrain_loc) {

        float interpolatedHeight;

        // Early exit for guild zones

        if (this.zone.guild_zone)
            return 5.0f;

        // Determine terrain and offset from top left vertex

        Vector2f terrain_cell = getTerrainCell(terrain_loc);

        int pixel_x = (int) Math.floor(terrain_cell.x);
        int pixel_y = (int) Math.floor(terrain_cell.y);

        Vector2f pixel_offset = new Vector2f(terrain_cell.x % 1, terrain_cell.y % 1);

        // 4 surrounding vertices from the pixel array.

        short top_left_pixel = terrain_pixel_data[pixel_x][pixel_y];
        short top_right_pixel = terrain_pixel_data[pixel_x + 1][pixel_y];
        short bottom_left_pixel = terrain_pixel_data[pixel_x][pixel_y + 1];
        short bottom_right_pixel = terrain_pixel_data[pixel_x + 1][pixel_y + 1];

        // Interpolate between the 4 vertices

        interpolatedHeight = top_left_pixel * (1 - pixel_offset.x) * (1 - pixel_offset.y);
        interpolatedHeight += top_right_pixel * (1 - pixel_offset.y) * (pixel_offset.x);
        interpolatedHeight += (bottom_left_pixel * (1 - pixel_offset.x) * pixel_offset.y);
        interpolatedHeight += (bottom_right_pixel * pixel_offset.y * pixel_offset.x);

        interpolatedHeight *= this.terrain_scale; // Scale height

        return interpolatedHeight;

    }

    public float getTerrainBlendCoefficient(Vector2f zone_offset) {

        // Normalize terrain offset

        Vector2f normalizedOffset = new Vector2f(Math.abs(zone_offset.x) / this.zone.template.major_radius,
                Math.abs(zone_offset.y) / this.zone.template.minor_radius);

        float blendCoefficient;

        if (normalizedOffset.x <= 1 - blend_ratio.x || normalizedOffset.x <= normalizedOffset.y) {

            if (normalizedOffset.y < 1 - blend_ratio.y)
                return 1;

            blendCoefficient = (normalizedOffset.y - (1 - blend_ratio.y)) / blend_ratio.y;
        } else
            blendCoefficient = (normalizedOffset.x - (1 - blend_ratio.x)) / blend_ratio.x;

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

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