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

package engine.math;

import engine.InterestManagement.WorldGrid;
import engine.gameManager.ZoneManager;
import engine.net.client.msg.PlaceAssetMsg.PlacementInfo;
import engine.objects.*;
import engine.server.MBServerStatics;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * This class contains all methods of storing bounds
 * information within MagicBane and performing collision
 * detection against them.
 * <p>
 * These objects are essentially an AABB, given rotations
 * in MagicBane for placed objects come in a quantum of 90.
 */

public class Bounds {

    private static final LinkedBlockingQueue<Bounds> boundsPool = new LinkedBlockingQueue<>();
    public static HashMap<Integer, MeshBounds> meshBoundsCache = new HashMap<>();

    private Vector2f origin = new Vector2f();
    private Vector2f halfExtents = new Vector2f();
    private float rotation;
    private float rotationDegrees = 0;
    private Quaternion quaternion;
    private boolean flipExtents;

    private ArrayList<Regions> regions = new ArrayList<>();
    private ArrayList<Colliders> colliders = new ArrayList<>();

    // Default constructor

    public Bounds() {

        origin.zero();
        halfExtents.zero();
        rotation = 0.0f;
        flipExtents = false;
    }

    public static Bounds borrow() {
        Bounds outBounds;

        outBounds = boundsPool.poll();

        if (outBounds == null)
            outBounds = new Bounds();

        return outBounds;
    }

    // Identity Bounds at location
    public static void zero(Bounds bounds) {
        bounds.origin.zero();
        bounds.halfExtents.zero();
        bounds.rotation = 0.0f;
        bounds.flipExtents = false;
    }

    public static boolean collide(Vector3fImmutable location, Bounds targetBounds) {

        if (targetBounds == null)
            return false;

        boolean collisionState = false;
        Bounds identityBounds = Bounds.borrow();
        identityBounds.setBounds(location);

        collisionState = collide(targetBounds, identityBounds, 0.0f);
        identityBounds.release();
        return collisionState;
    }

    public static boolean collide(Vector3fImmutable location, Building targetBuilding) {

        boolean collisionState = false;
        Bounds targetBounds = targetBuilding.getBounds();

        if (targetBounds == null)
            return false;
        Bounds identityBounds = Bounds.borrow();
        identityBounds.setBounds(location);

        collisionState = collide(targetBounds, identityBounds, 0.1f);
        identityBounds.release();
        return collisionState;
    }

    public static boolean collide(Bounds sourceBounds, Bounds targetBounds, float threshold) {

        float deltaX;
        float deltaY;
        float extentX;
        float extentY;
        float sourceExtentX;
        float sourceExtentY;
        float targetExtentX;
        float targetExtentY;

        deltaX = Math.abs(sourceBounds.origin.x - targetBounds.origin.x);
        deltaY = Math.abs(sourceBounds.origin.y - targetBounds.origin.y);

        if (sourceBounds.flipExtents) {
            sourceExtentX = sourceBounds.halfExtents.y;
            sourceExtentY = sourceBounds.halfExtents.x;
        } else {
            sourceExtentX = sourceBounds.halfExtents.x;
            sourceExtentY = sourceBounds.halfExtents.y;
        }
        if (targetBounds.flipExtents) {
            targetExtentX = targetBounds.halfExtents.y;
            targetExtentY = targetBounds.halfExtents.x;
        } else {
            targetExtentX = targetBounds.halfExtents.x;
            targetExtentY = targetBounds.halfExtents.y;
        }

        extentX = sourceExtentX + targetExtentX;
        extentY = sourceExtentY + targetExtentY;

        // Return false on overlapping edge cases
        if ((Math.abs(deltaX + threshold) < extentX))
            if ((Math.abs(deltaY + threshold) < extentY))
                return true;

        return false;

    }

    public static boolean collide(PlacementInfo sourceInfo, Building targetBuilding) {

        Bounds sourceBounds;
        Bounds targetBounds;

        boolean collisionState = false;

        // Early exit sanity check.  Can't quite collide against nothing

        if ((sourceInfo == null) || (targetBuilding == null))
            return false;

        sourceBounds = Bounds.borrow();
        sourceBounds.setBounds(sourceInfo);

        // WARNING: DO NOT EVER RELEASE THESE WORLDOBJECT BOUNDS
        // THEY ARE NOT IMMUTABLE

        targetBounds = targetBuilding.getBounds();

        // If target building has no bounds, we certainly cannot collide.
        // Note: We remove and release bounds objects to the pool when
        // buildings are destroyed.

        if (targetBounds == null)
            return false;

        collisionState = collide(sourceBounds, targetBounds, .1f);

        // Release bounds and return collision state

        sourceBounds.release();
        return collisionState;
    }

    public static boolean collide(Bounds bounds, Vector3fImmutable start, Vector3fImmutable end) {
        boolean collide = false;
        for (Colliders collider : bounds.colliders) {

            collide = linesTouching(collider.startX, collider.startY, collider.endX, collider.endY, start.x, start.z, end.x, end.z);

            if (collide)
                break;


        }

        return collide;
    }

    //used for wall collision with players.
    public static Vector3fImmutable PlayerBuildingCollisionPoint(PlayerCharacter player, Vector3fImmutable start, Vector3fImmutable end) {
        Vector3fImmutable collidePoint = null;

        //player can fly over walls when at max altitude. skip collision checks.
        if (player.getAltitude() >= 60)
            return null;


        float distance = player.getLoc().distance2D(end);
        // Players should not be able to move more than 2000 units at a time, stop them dead in their tracks if they do. (hacks)
        if (distance > 2000)
            return player.getLoc();


        HashSet<AbstractWorldObject> awoList = WorldGrid.getObjectsInRangePartial(player, distance + 1000, MBServerStatics.MASK_BUILDING);
        float collideDistance = 0;
        float lastDistance = -1;


        for (AbstractWorldObject awo : awoList) {

            Building building = (Building) awo;


            //player is inside building region, skip collision check. we only do collision from the outside.
            if (player.region != null && player.region.parentBuildingID == building.getObjectUUID())
                continue;
            if (building.getBounds().colliders == null)
                continue;

            for (Colliders collider : building.getBounds().colliders) {

                //links are what link together buildings, allow players to run through them only if they are in a building already.
                if (collider.isLink() && player.region != null)
                    continue;
                if (collider.getDoorID() != 0 && building.isDoorOpen(collider.getDoorID()))
                    continue;

                Vector3fImmutable tempCollidePoint = lineIntersection(collider.startX, collider.startY, collider.endX, collider.endY, start.x, start.z, end.x, end.z);

                //didnt collide, skip distance checks.
                if (tempCollidePoint == null)
                    continue;

                //first collision detection, inititialize all variables.
                if (lastDistance == -1) {
                    collideDistance = start.distance2D(tempCollidePoint);
                    lastDistance = collideDistance;
                    collidePoint = tempCollidePoint;
                } else
                    //get closest collide point.
                    collideDistance = start.distance2D(tempCollidePoint);

                if (collideDistance < lastDistance) {
                    lastDistance = collideDistance;
                    collidePoint = tempCollidePoint;
                }
            }
        }


        //
        if (collidePoint != null) {

            if (collideDistance >= 2)
                collidePoint = player.getFaceDir().scaleAdd(-2f, new Vector3fImmutable((float) collidePoint.getX(), end.y, (float) collidePoint.getZ()));
            else
                collidePoint = player.getLoc();
        }


        return collidePoint;
    }

    public static boolean linesTouching(float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4) {
        float denominator = ((x2 - x1) * (y4 - y3)) - ((y2 - y1) * (x4 - x3));
        float numerator1 = ((y1 - y3) * (x4 - x3)) - ((x1 - x3) * (y4 - y3));
        float numerator2 = ((y1 - y3) * (x2 - x1)) - ((x1 - x3) * (y2 - y1));

        // Detect coincident lines (has a problem, read below)
        if (denominator == 0)
            return numerator1 == 0 && numerator2 == 0;

        float r = numerator1 / denominator;
        float s = numerator2 / denominator;

        return (r >= 0 && r <= 1) && (s >= 0 && s <= 1);
    }

    public static Vector3fImmutable lineIntersection(float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4) {

        // calculate the distance to intersection point
        float uA = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1));
        float uB = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1));

        // if uA and uB are between 0-1, lines are colliding
        if (uA >= 0 && uA <= 1 && uB >= 0 && uB <= 1) {
            return new Vector3fImmutable(x1 + (uA * (x2 - x1)), 0, y1 + (uA * (y2 - y1)));
        }
        return null;
    }

    private static boolean calculateFlipExtents(Bounds bounds) {

        int degrees;
        double radian = 0;
        if (bounds.quaternion != null) {
            radian = bounds.quaternion.angleY;
        }

        degrees = (int) Math.toDegrees(radian);
        bounds.rotationDegrees = degrees;
        if (degrees < 0)
            degrees += 360;
        return (degrees >= 85 && degrees <= 95) ||
                (degrees >= 265 && degrees <= 275);

    }

    public static Vector3f getRotatedPoint(Vector3f point, float centerX, float centerZ, float angle) {

        //TRANSLATE TO ORIGIN
        float x1 = point.getX() - centerX;
        float y1 = point.getZ() - centerZ;

        //APPLY ROTATION
        float temp_x1 = (float) (x1 * Math.cos(angle) - y1 * Math.sin(angle));
        float temp_z1 = (float) (x1 * Math.sin(angle) + y1 * Math.cos(angle));

        temp_x1 += centerX;
        temp_z1 += centerZ;

        return new Vector3f(temp_x1, point.y, temp_z1);

    }

    public void release() {
        Bounds.zero(this);
        boundsPool.add(this);

    }

    // Method detects overlap of two given Bounds objects.
    // Just your generic AABB collision algorythm.

    public void setBounds(Vector2f origin, Vector2f extents, float rotation) {

        this.origin.set(origin);
        this.halfExtents.set(extents);
        this.rotation = rotation;

        this.flipExtents = Bounds.calculateFlipExtents(this);

    }

    public void setBounds(PlacementInfo sourceInfo) {

        Blueprint sourceBlueprint;

        sourceBlueprint = Blueprint.getBlueprint(sourceInfo.getBlueprintUUID());
        this.origin.set(sourceInfo.getLoc().x, sourceInfo.getLoc().z);
        this.halfExtents.set(sourceBlueprint.getExtents());

        this.quaternion = new Quaternion(sourceInfo.getRot().x, sourceInfo.getRot().y, sourceInfo.getRot().z, sourceInfo.getW());
        this.rotation = sourceInfo.getRot().y;
        this.flipExtents = Bounds.calculateFlipExtents(this);

    }

    public void setBounds(Bounds sourceBounds) {

        origin.set(sourceBounds.origin);
        halfExtents.set(sourceBounds.halfExtents);
        this.rotation = sourceBounds.rotation;
        this.flipExtents = sourceBounds.flipExtents;

    }

    public void setBounds(AbstractCharacter sourcePlayer) {

        this.origin.set(sourcePlayer.getLoc().x, sourcePlayer.getLoc().z);
        this.halfExtents.set(.5f, .5f);
        this.rotation = 0;
        this.flipExtents = false;

    }

    public void setBounds(Vector3fImmutable sourceLocation) {

        this.origin.set(sourceLocation.x, sourceLocation.z);
        this.halfExtents.set(.5f, .5f);
        this.rotation = 0;
        this.flipExtents = false;

    }

    public void setBounds(Vector3fImmutable sourceLocation, float halfExtent) {

        this.origin.set(sourceLocation.x, sourceLocation.z);
        this.halfExtents.set(halfExtent, halfExtent);
        this.rotation = 0;
        this.flipExtents = false;

    }

    public void setBounds(Building building) {

        Blueprint blueprint;
        MeshBounds meshBounds;
        int halfExtentX;
        int halfExtentY;
        // Need a blueprint for proper bounds

        blueprint = building.getBlueprint();

        this.quaternion = new Quaternion(building.getRot().x, building.getRot().y, building.getRot().z, building.getw());

        // Calculate Bounds for non-blueprint objects

        if (blueprint == null) {

            // If a mesh is a non-blueprint structure then we calculate
            // it's bounding box based upon defaults from original source
            // lookup.

            meshBounds = meshBoundsCache.get(building.getMeshUUID());
            this.origin.set(building.getLoc().x, building.getLoc().z);


            // Magicbane uses halfExtents

            if (meshBounds == null) {
                halfExtentX = 1;
                halfExtentY = 1;
            } else {

                float halfExtent = Math.max((meshBounds.maxX - meshBounds.minX) / 2, (meshBounds.maxZ - meshBounds.minZ) / 2);
                halfExtentX = Math.round(halfExtent);
                halfExtentY = Math.round(halfExtent);
            }


            // The rotation is reset after the new aabb is calculated.

            this.rotation = building.getRot().y;

            // Caclculate and set the new half halfExtents for the rotated bounding box
            // and reset the rotation to 0 for this bounds.

            this.halfExtents.set(halfExtentX, (halfExtentY));
            this.rotation = 0;

            this.setRegions(building);
            this.setColliders(building);
            return;
        }

        this.origin.set(building.getLoc().x, building.getLoc().z);
        this.rotation = building.getRot().y;
        this.halfExtents.set(blueprint.getExtents());
        this.flipExtents = Bounds.calculateFlipExtents(this);

        this.setRegions(building);
        this.setColliders(building);

    }

    public void modify(float x, float y, float extents) {
        this.origin.x = x;
        this.origin.y = y;
        this.halfExtents.x = extents;
        this.halfExtents.y = extents;
    }

    /**
     * @return the origin
     */
    public Vector2f getOrigin() {
        return origin;
    }

    /**
     * @return the halfExtents
     */
    public Vector2f getHalfExtents() {
        return halfExtents;
    }

    /**
     * @return the rotation
     */
    public float getRotation() {
        return rotation;
    }

    /**
     * @param rotation the rotation to set
     */
    public void setRotation(float rotation) {
        this.rotation = rotation;
    }

    public void setColliders(Building building) {
        //Collidables are for player movement collision
        ArrayList<StaticColliders> tempList = StaticColliders._staticColliders.get(building.getMeshUUID());

        ArrayList<Colliders> tempColliders = new ArrayList<>();
        if (tempList != null) {

            for (StaticColliders staticCollider : tempList) {

                ArrayList<Vector3f> regionPoints = new ArrayList<>();

                Vector3f colliderStart = new Vector3f(staticCollider.getStartX(), 0, staticCollider.getStartY());
                Vector3f colliderEnd = new Vector3f(staticCollider.getEndX(), 0, staticCollider.getEndY());
                Vector3f worldStart = ZoneManager.convertLocalToWorld(building, colliderStart, this);
                Vector3f worldEnd = ZoneManager.convertLocalToWorld(building, colliderEnd, this);
                tempColliders.add(new Colliders(worldStart.x, worldStart.z, worldEnd.x, worldEnd.z, staticCollider.getDoorID(), staticCollider.isLink()));
            }

        }


        this.colliders = tempColliders;
    }

    public ArrayList<Regions> getRegions() {
        return regions;
    }

    public void setRegions(Building building) {
        //Collidables are for player movement collision
        ArrayList<BuildingRegions> tempList = BuildingRegions._staticRegions.get(building.getMeshUUID());

        ArrayList<Regions> tempRegions = new ArrayList<>();
        if (tempList != null) {

            for (BuildingRegions buildingRegion : tempList) {

                ArrayList<Vector3f> regionPoints = new ArrayList<>();

                Vector3f centerPoint = ZoneManager.convertLocalToWorld(building, buildingRegion.center, this);
                for (Vector3f point : buildingRegion.getRegionPoints()) {
                    Vector3f rotatedPoint = ZoneManager.convertLocalToWorld(building, point, this);
                    regionPoints.add(rotatedPoint);
                }
                tempRegions.add(new Regions(regionPoints, buildingRegion.getLevel(), buildingRegion.getRoom(), buildingRegion.isOutside(), buildingRegion.isExitRegion(), buildingRegion.isStairs(), centerPoint, building.getObjectUUID()));
            }

        }


        this.regions = tempRegions;
    }

    public void setRegions(ArrayList<Regions> regions) {
        this.regions = regions;
    }

    public float getRotationDegrees() {
        return rotationDegrees;
    }

    public boolean isFlipExtents() {
        return flipExtents;
    }

    public Quaternion getQuaternion() {
        return quaternion;
    }

}