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


package engine.mobileAI.utilities;

import engine.Enum;
import engine.Enum.GameObjectType;
import engine.Enum.ModType;
import engine.Enum.SourceType;
import engine.InterestManagement.WorldGrid;
import engine.exception.MsgSendException;
import engine.gameManager.BuildingManager;
import engine.gameManager.ChatManager;
import engine.gameManager.MovementManager;
import engine.gameManager.ZoneManager;
import engine.math.Bounds;
import engine.math.Vector3f;
import engine.math.Vector3fImmutable;
import engine.mobileAI.Threads.MobAIThread;
import engine.net.client.msg.MoveToPointMsg;
import engine.objects.*;
import engine.server.MBServerStatics;
import org.pmw.tinylog.Logger;

import java.util.ArrayList;
import java.util.concurrent.ThreadLocalRandom;

import static engine.math.FastMath.sqr;
import static engine.math.FastMath.sqrt;

public class MovementUtilities {
private static final int cellGap = 1;

    public static boolean inRangeOfBindLocation(Mob agent) {


        if (agent.isPlayerGuard()) {

            Mob guardCaptain = null;
            if (agent.getContract() != null)
                guardCaptain = agent;
            else
                guardCaptain = (Mob) agent.guardCaptain;

            if (guardCaptain != null) {
                Building barracks = guardCaptain.building;

                if (barracks != null) {
                    City city = barracks.getCity();

                    if (city != null) {
                        Building tol = city.getTOL();

                        //Guards recall distance = 814.
                        if (tol != null) {
                            return !(agent.getLoc().distanceSquared2D(tol.getLoc()) > sqr(Enum.CityBoundsType.ZONE.halfExtents));
                        }

                    }
                }
            }

            return true;
        }

        Vector3fImmutable sl = new Vector3fImmutable(agent.getLoc().getX(), 0, agent.getLoc().getZ());
        Vector3fImmutable tl = new Vector3fImmutable(agent.getTrueBindLoc().x, 0, agent.getTrueBindLoc().z);

        float distanceSquaredToTarget = sl.distanceSquared2D(tl); //distance to center of target
        float zoneRange = 250;

        if (agent.getParentZone() != null) {
            if (agent.getParentZone().bounds != null)
                zoneRange = agent.getParentZone().bounds.getHalfExtents().x * 2;
        }

        if (zoneRange > 300)
            zoneRange = 300;

        if (agent.getSpawnRadius() > zoneRange)
            zoneRange = agent.getSpawnRadius();


        return distanceSquaredToTarget < sqr(MobAIThread.AI_DROP_AGGRO_RANGE + zoneRange);

    }

    public static boolean inRangeToAggro(Mob agent, PlayerCharacter target) {

        Vector3fImmutable sl = agent.getLoc();
        Vector3fImmutable tl = target.getLoc();

        float distanceSquaredToTarget = sl.distanceSquared2D(tl) - sqr(agent.calcHitBox() + target.calcHitBox()); //distance to center of target
        float range = MobAIThread.AI_BASE_AGGRO_RANGE;

        if (agent.isPlayerGuard())
            range = 150;

        return distanceSquaredToTarget < sqr(range);

    }

    public static boolean inRangeDropAggro(Mob agent, AbstractCharacter target) {

        Vector3fImmutable sl = agent.getLoc();
        Vector3fImmutable tl = target.getLoc();

        float distanceSquaredToTarget = sl.distanceSquared2D(tl) - sqr(agent.calcHitBox() + target.calcHitBox()); //distance to center of target

        float range = agent.getRange() + 150;

        if (range > 200)
            range = 200;


        return distanceSquaredToTarget < sqr(range);

    }

    public static Vector3fImmutable GetMoveLocation(Mob aiAgent, AbstractCharacter aggroTarget) {

        // Player isnt moving and neither is mob.  Just return
        // the mobile's current location.  Ain't goin nowhere!
        // *** Refactor: Check to ensure methods calling us
        // all don't sent move messages when not moving.

        if ((aggroTarget.isMoving() == false))
            return aggroTarget.getLoc();

        if (aggroTarget.getEndLoc().x != 0) {

            float aggroTargetDistanceSquared = aggroTarget.getLoc().distanceSquared2D(aggroTarget.getEndLoc());
            float aiAgentDistanceSquared = aiAgent.getLoc().distanceSquared2D(aggroTarget.getEndLoc());

            if (aiAgentDistanceSquared >= aggroTargetDistanceSquared)
                return aggroTarget.getEndLoc();
            else {
                float distanceToMove = sqrt(aggroTargetDistanceSquared + aiAgentDistanceSquared) * .5f;

                return aggroTarget.getFaceDir().scaleAdd(distanceToMove, aggroTarget.getLoc());

            }
        }

        // One of us is moving so let's calculate our destination loc for this
        // simulation frame.  We will simply project our position onto the
        // character's movement vector and return the closest point.

        return aiAgent.getLoc().ClosestPointOnLine(aggroTarget.getLoc(), aggroTarget.getEndLoc());
    }

    public static void moveToLocation(Mob agent, Vector3fImmutable newLocation, float offset, boolean isWalking) {
        try {

            //don't move farther than 30 units from player.
            if (offset > 30)
                offset = 30;
            Vector3fImmutable newLoc = Vector3fImmutable.getRandomPointInCircle(newLocation, offset);


            agent.setFaceDir(newLoc.subtract2D(agent.getLoc()).normalize());

            aiMove(agent, newLoc, isWalking);
        } catch (Exception e) {
            Logger.error(e.toString());
        }
    }


    public static boolean canMove(Mob agent) {
        if (agent.getMobBase() != null && Enum.MobFlagType.SENTINEL.elementOf(agent.getMobBase().getFlags()))
            return false;

        return (agent.isAlive() && !agent.getBonuses().getBool(ModType.Stunned, SourceType.None) && !agent.getBonuses().getBool(ModType.CannotMove, SourceType.None));
    }

    public static Vector3fImmutable randomPatrolLocation(Mob agent, Vector3fImmutable center, float radius) {

        //Determing where I want to move.
        return new Vector3fImmutable((center.x - radius) + ((ThreadLocalRandom.current().nextFloat() + .1f * 2) * radius),
                center.y,
                (center.z - radius) + ((ThreadLocalRandom.current().nextFloat() + .1f * 2) * radius));
    }

    public static Long estimateMovementTime(Mob agent) {
        if (agent.getEndLoc().x == 0 && agent.getEndLoc().y == 0)
            return 0L;

        return (long) ((agent.getLoc().distance2D(agent.getEndLoc()) * 1000) / agent.getSpeed());
    }

    public static void aiMove(Mob agent, Vector3fImmutable vect, boolean isWalking) {

        //update our walk/run state.
        if (isWalking && !agent.isWalk()) {
            agent.setWalkMode(true);
            MovementManager.sendRWSSMsg(agent);
        } else if (!isWalking && agent.isWalk()) {
            agent.setWalkMode(false);
            MovementManager.sendRWSSMsg(agent);
        }

        MoveToPointMsg msg = new MoveToPointMsg();


//		Regions currentRegion = Mob.InsideBuildingRegion(agent);
//
//		if (currentRegion != null){
//
//
//			if (currentRegion.isGroundLevel()){
//				agent.setInBuilding(0);
//				agent.setInFloorID(-1);
//			}else{
//				agent.setInBuilding(currentRegion.getLevel());
//				agent.setInFloorID(currentRegion.getRoom());
//			}
//		}else{
//			agent.setInBuilding(-1);
//			agent.setInFloorID(-1);
//			agent.setInBuildingID(0);
//		}
//		agent.setLastRegion(currentRegion);


        Vector3fImmutable startLoc = null;
        Vector3fImmutable endLoc = null;

//		if (agent.getLastRegion() != null){
//			Building inBuilding = Building.getBuildingFromCache(agent.getInBuildingID());
//			if (inBuilding != null){
//				startLoc = ZoneManager.convertWorldToLocal(inBuilding, agent.getLoc());
//				endLoc = ZoneManager.convertWorldToLocal(inBuilding, vect);
//			}
//		}else{
//			agent.setBuildingID(0);
//			agent.setInBuildingID(0);
//			startLoc = agent.getLoc();
//			endLoc = vect;
//		}

        startLoc = agent.getLoc();
        endLoc = vect;

        msg.setSourceType(GameObjectType.Mob.ordinal());
        msg.setSourceID(agent.getObjectUUID());
        msg.setStartCoord(startLoc);
        msg.setEndCoord(endLoc);
        msg.setInBuildingFloor(-1);
        msg.setInBuilding(-1);
        msg.setStartLocType(0);
        msg.setInBuildingUUID(0);


        try {
            MovementManager.movement(msg, agent);
        } catch (MsgSendException e) {
            // TODO Figure out how we want to handle the msg send exception
            e.printStackTrace();
        }
    }

    public static Vector3fImmutable GetDestinationToCharacter(Mob aiAgent, AbstractCharacter character) {

        if (!character.isMoving())
            return character.getLoc();


        float agentDistanceEndLoc = aiAgent.getLoc().distanceSquared2D(character.getEndLoc());
        float characterDistanceEndLoc = character.getLoc().distanceSquared2D(character.getEndLoc());

        if (agentDistanceEndLoc > characterDistanceEndLoc)
            return character.getEndLoc();

        return character.getLoc();
    }

    public static boolean updateMovementToCharacter(Mob aiAgent, AbstractCharacter aggroTarget) {

        if (aiAgent.destination.equals(Vector3fImmutable.ZERO))
            return true;

        if (!aiAgent.isMoving())
            return true;


        if (aggroTarget.isMoving()) {
            return !aiAgent.destination.equals(aggroTarget.getEndLoc()) && !aiAgent.destination.equals(aggroTarget.getLoc());
        } else {
            if (aiAgent.destination.equals(aggroTarget.getLoc()))
                return false;
        }

        return false;
    }
    public static void pathfind(AbstractCharacter character, Vector3fImmutable goal){
        try {

            if(character.region == null && Regions.getRegionAtLocation(goal) != null) {//mover not inside a building
                Building building = BuildingManager.getBuildingAtLocation(goal);
                ArrayList<Regions> entrances = new ArrayList<>();
                for (Regions region : building.getBounds().getRegions()) {
                    if (region.exit && region.level == 0)
                        entrances.add(region);
                }
                    Regions cheapest = null;
                    for(Regions entrance : entrances){
                        if(cheapest == null) {
                            cheapest = entrance;
                            continue;
                        }
                        if(getCost(new Vector3fImmutable(entrance.center),character.loc,goal) < getCost(new Vector3fImmutable(cheapest.center),character.loc,goal))
                            cheapest = entrance;
                    }

                    goal = new Vector3fImmutable(cheapest.center.x, cheapest.center.y, cheapest.center.z);

            }

            ArrayList<Vector3fImmutable> path = getOptimizedPath(getPath(character.loc, goal), getPath(goal, character.loc));
            if (path.isEmpty()) {
                ((Mob) character).destination = goal;
                return; //no points to walk to
            }

            ((Mob) character).destination = path.get(0);

        } catch(Exception e){
            //something failed
        }
    }

    public static ArrayList<Vector3fImmutable> getOptimizedPath(ArrayList<Vector3fImmutable> startToGoal, ArrayList<Vector3fImmutable> goalToStart) {
        ArrayList<Vector3fImmutable> optimalPath = new ArrayList<>();
        optimalPath.add(startToGoal.get(0));
        for(Vector3fImmutable point : startToGoal)
        {
            if(!goalToStart.contains(point))
            {
                continue;
            }
            optimalPath.add(point);
        }
        return optimalPath;
    }

    private static ArrayList<Vector3fImmutable> getPath(Vector3fImmutable start, Vector3fImmutable goal) {
        ArrayList<Vector3fImmutable> path = new ArrayList<>();
        path.add(start);
        Vector3fImmutable current = start;
        boolean obstructed = false;
        int count = 0;
        while (current.distanceSquared(goal) > 9 && count < 250)
        {
            //gather the 8 cells around the player
            ArrayList<Vector3fImmutable> surroundingCells = new ArrayList<>();
            surroundingCells.add(current.add(new Vector3fImmutable(cellGap, 0, 0)));
            surroundingCells.add(current.add(new Vector3fImmutable(cellGap, 0, cellGap)));
            surroundingCells.add(current.add(new Vector3fImmutable(0, 0, cellGap)));
            surroundingCells.add(current.add(new Vector3fImmutable(-cellGap, 0, 0)));
            surroundingCells.add(current.add(new Vector3fImmutable(-cellGap, 0, -cellGap)));
            surroundingCells.add(current.add(new Vector3fImmutable(0, 0, -cellGap)));
            surroundingCells.add(current.add(new Vector3fImmutable(-cellGap, 0, cellGap)));
            surroundingCells.add(current.add(new Vector3fImmutable(cellGap, 0, -cellGap)));
            Vector3fImmutable cheapest = new Vector3fImmutable(Vector3fImmutable.ZERO);
            for (Vector3fImmutable point : surroundingCells)
            {
                count++;

                Building building = BuildingManager.getBuildingAtLocation(point);
                if(building != null){
                    float halfX = building.getBounds().getHalfExtents().x;
                    float halfY = building.getBounds().getHalfExtents().y;
                    ArrayList<Vector3fImmutable> corners = new ArrayList<>();
                    corners.add(building.loc.add(new Vector3fImmutable(halfX,0,-halfY)));
                    corners.add(building.loc.add(new Vector3fImmutable(halfX,0,halfY)));
                    corners.add(building.loc.subtract(new Vector3fImmutable(halfX,0,halfY)));
                    corners.add(building.loc.add(new Vector3fImmutable(-halfX,0,halfY)));
                    Vector3fImmutable cheapCorner = Vector3fImmutable.ZERO;
                    for(Vector3fImmutable corn : corners){
                        if(Bounds.collide(building.getBounds(),point,corn))
                            continue;
                        if(getCost(corn,start,goal) < getCost(cheapCorner,start,goal))
                            cheapCorner = corn;
                    }
                    if(!cheapCorner.equals(Vector3fImmutable.ZERO))
                        if (path.contains(cheapCorner)) {
                            continue;
                        }else {
                        path.add(cheapCorner);
                        current = cheapCorner;
                        continue;
                        }


                    if (path.contains(point))
                        continue;
                }

                if (path.contains(point))
                    continue;

                Regions region = Regions.getRegionAtLocation(point);
                if(region != null) {
                    path.add(new Vector3fImmutable(region.center.x,region.center.y,region.center.z)); //only use center points when travelling through regions
                    continue;

                }

                if (pointIsBlocked(point)) {
                    obstructed = true;
                    continue;
                }

                if (getCost(cheapest, current, goal) > getCost(point, current, goal))
                    cheapest = point;

            }

            current = cheapest;
            path.add(cheapest);
        }
        if(obstructed) {
            return path;
        }else {
            ArrayList<Vector3fImmutable> goalPath = new ArrayList<>();
            goalPath.add(goal);
            goalPath.add(start);
            return goalPath; //if the path isn't obstructed we can walk directly from start to the goal
        }
    }

    public static float getCost(Vector3fImmutable point, Vector3fImmutable start, Vector3fImmutable goal) {
        float gCost = start.distanceSquared(point);
        float hCost = goal.distanceSquared(point);
        return gCost + hCost;
    }

    public static boolean pointIsBlocked(Vector3fImmutable point) {

        Building building = BuildingManager.getBuildingAtLocation(point);
        if(building == null)
            return false;//no building at this location means nothing obstructing the walking path

        if(Regions.getRegionAtLocation(point) != null)
            return false;

        Zone currentZone = ZoneManager.findSmallestZone(point);
        if(currentZone == null)
            return false;
        else
            return currentZone.navMesh.contains(point.x,point.z);
        //return false;
    }

}
