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

package engine.gameManager;

import engine.Enum.DispatchChannel;
import engine.Enum.GameObjectType;
import engine.Enum.ModType;
import engine.Enum.SourceType;
import engine.InterestManagement.InterestManager;
import engine.InterestManagement.WorldGrid;
import engine.exception.MsgSendException;
import engine.math.Bounds;
import engine.math.Vector3f;
import engine.math.Vector3fImmutable;
import engine.net.DispatchMessage;
import engine.net.client.ClientConnection;
import engine.net.client.msg.MoveToPointMsg;
import engine.net.client.msg.TeleportToPointMsg;
import engine.net.client.msg.UpdateStateMsg;
import engine.objects.*;
import engine.server.MBServerStatics;
import org.pmw.tinylog.Logger;

import java.util.HashSet;
import java.util.Set;

import static engine.math.FastMath.sqr;

public enum MovementManager {

	MOVEMENTMANAGER;

	private static final String changeAltitudeTimerJobName = "ChangeHeight";
	private static final String flightTimerJobName = "Flight";

	public static void sendOOS(PlayerCharacter pc) {
		pc.setWalkMode(true);
		MovementManager.sendRWSSMsg(pc);
	}

	public static void sendRWSSMsg(AbstractCharacter ac) {

		if (!ac.isAlive())
			return;
		UpdateStateMsg rssm = new UpdateStateMsg();
		rssm.setPlayer(ac);
		if (ac.getObjectType() == GameObjectType.PlayerCharacter)
			DispatchMessage.dispatchMsgToInterestArea(ac, rssm, DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, true, false);
		else
			DispatchMessage.sendToAllInRange(ac, rssm);
	}

	/*
	 * Sets the first combat target for the AbstractCharacter. Used to clear the
	 * combat
	 * target upon each move, unless something has set the firstHitCombatTarget
	 * Also used to determine the size of a monster's hitbox
	 */
	public static void movement(MoveToPointMsg msg, AbstractCharacter toMove) throws MsgSendException {

		// check for stun/root
		if (!toMove.isAlive())
			return;

		if (toMove.getObjectType().equals(GameObjectType.PlayerCharacter)){
			if (((PlayerCharacter)toMove).isCasting())
				((PlayerCharacter)toMove).update();
		}
			
		
		
		toMove.setIsCasting(false);
		toMove.setItemCasting(false);

		if (toMove.getBonuses().getBool(ModType.Stunned, SourceType.None) || toMove.getBonuses().getBool(ModType.CannotMove, SourceType.None)) {
			return;
		}
		
		if (msg.getEndLat() > MBServerStatics.MAX_WORLD_WIDTH)
			msg.setEndLat((float) MBServerStatics.MAX_WORLD_WIDTH);
		
		if (msg.getEndLon() < MBServerStatics.MAX_WORLD_HEIGHT){
			msg.setEndLon((float) MBServerStatics.MAX_WORLD_HEIGHT);
		}
		
//		if (msg.getEndLat() < 0)
//			msg.setEndLat(0);
//		
//		if (msg.getEndLon() > 0)
//			msg.setEndLon(0);
		
		

		
		
		if (!toMove.isMoving())
			toMove.resetLastSetLocUpdate();
		 else
			toMove.update();

		// Update movement for the player
			
			
		//    	else if (toMove.getObjectType() == GameObjectType.Mob)
		//    		((Mob)toMove).updateLocation();
		// get start and end locations for the move
		Vector3fImmutable startLocation = new Vector3fImmutable(msg.getStartLat(), msg.getStartAlt(), msg.getStartLon());
		Vector3fImmutable endLocation = new Vector3fImmutable(msg.getEndLat(), msg.getEndAlt(), msg.getEndLon());

		//		if (toMove.getObjectType() == GameObjectType.PlayerCharacter)
		//			if (msg.getEndAlt() == 0 && msg.getTargetID() == 0){
		//				MovementManager.sendRWSSMsg(toMove);
		//			}

		//If in Building, let's see if we need to Fix

		// if inside a building, convert both locations from the building local reference frame to the world reference frame

		if (msg.getTargetID() > 0) {
			Building building = BuildingManager.getBuildingFromCache(msg.getTargetID());
			if (building != null) {
				
				Vector3fImmutable convertLocEnd = new Vector3fImmutable(ZoneManager.convertLocalToWorld(building, endLocation));
				//                if (!Bounds.collide(convertLocEnd, b) || !b.loadObjectsInside()) {
				//                    toMove.setInBuilding(-1);
				//                    toMove.setInFloorID(-1);
				//                    toMove.setInBuildingID(0);
				//                }
				//                else {
				toMove.setInBuilding(msg.getInBuilding());
				toMove.setInFloorID(msg.getUnknown01());
				toMove.setInBuildingID(msg.getTargetID());
				msg.setStartCoord(ZoneManager.convertWorldToLocal(building, toMove.getLoc()));
		
				if (toMove.getObjectType() == GameObjectType.PlayerCharacter) {
					if (convertLocEnd.distanceSquared2D(toMove.getLoc()) > 6000 * 6000) {

						Logger.info( "ENDLOC:" + convertLocEnd.x + ',' + convertLocEnd.y + ',' + convertLocEnd.z +
								',' + "GETLOC:" + toMove.getLoc().x + ',' + toMove.getLoc().y + ',' + toMove.getLoc().z + " Name " + ((PlayerCharacter) toMove).getCombinedName());
						toMove.teleport(toMove.getLoc());

						return;
					}
				}

				startLocation = toMove.getLoc();
				endLocation = convertLocEnd;

			} else {

				toMove.setInBuilding(-1);
				toMove.setInFloorID(-1);
				toMove.setInBuildingID(0);
				//SYNC PLAYER
				toMove.teleport(toMove.getLoc());
				return;
			}

		} else {
			toMove.setInBuildingID(0);
			toMove.setInFloorID(-1);
			toMove.setInBuilding(-1);
			msg.setStartCoord(toMove.getLoc());
		}
		
		//make sure we set the correct player.
		msg.setSourceType(toMove.getObjectType().ordinal());
		msg.setSourceID(toMove.getObjectUUID());
		
		//if player in region, modify location to local location of building. set target to building.
		if (toMove.getRegion() != null){
			Building regionBuilding = Regions.GetBuildingForRegion(toMove.getRegion());
			if (regionBuilding != null){
				msg.setStartCoord(ZoneManager.convertWorldToLocal(Regions.GetBuildingForRegion(toMove.getRegion()), toMove.getLoc()));
				msg.setEndCoord(ZoneManager.convertWorldToLocal(regionBuilding, endLocation));
				msg.setInBuilding(toMove.getRegion().level);
				msg.setUnknown01(toMove.getRegion().room);
				msg.setTargetType(GameObjectType.Building.ordinal());
				msg.setTargetID(regionBuilding.getObjectUUID());
			}
			
		}else{
			toMove.setInBuildingID(0);
			toMove.setInFloorID(-1);
			toMove.setInBuilding(-1);
			msg.setStartCoord(toMove.getLoc());
			msg.setEndCoord(endLocation);
			msg.setTargetType(0);
			msg.setTargetID(0);
		}

		//checks sync between character and server, if out of sync, teleport player to original position and return.
		if (toMove.getObjectType() == GameObjectType.PlayerCharacter) {
			boolean startLocInSync = checkSync(toMove, startLocation, toMove.getLoc());
			
			if (!startLocInSync){
				syncLoc(toMove, toMove.getLoc(), startLocInSync);
				return;
			}

		}
	
		// set direction, based on the current location which has just been sync'd
		// with the client and the calc'd destination
		toMove.setFaceDir(endLocation.subtract2D(toMove.getLoc()).normalize());
		
		boolean collide = false;
		if (toMove.getObjectType().equals(GameObjectType.PlayerCharacter)) {
			Vector3fImmutable collidePoint = Bounds.PlayerBuildingCollisionPoint((PlayerCharacter)toMove, toMove.getLoc(), endLocation);
			
			if (collidePoint != null) {
				msg.setEndCoord(collidePoint);
				endLocation = collidePoint;
				collide = true;
			}
						
		}
					
		if (toMove.getObjectType() == GameObjectType.PlayerCharacter && ((PlayerCharacter) toMove).isTeleportMode()) {
			toMove.teleport(endLocation);
			return;
		}

		// move to end location, this can interrupt the current move
		toMove.setEndLoc(endLocation);

		//	ChatManager.chatSystemInfo((PlayerCharacter)toMove, "Moving to " + Vector3fImmutable.toString(endLocation));

		// make sure server knows player is not sitting
		toMove.setSit(false);

		// cancel any effects that break upon movement
		toMove.cancelOnMove();

		//cancel any attacks for manual move.
		if ((toMove.getObjectType() == GameObjectType.PlayerCharacter) && msg.getUnknown02() == 0)
			toMove.setCombatTarget(null);


		// If it's not a player moving just send the message

		if ((toMove.getObjectType() == GameObjectType.PlayerCharacter) == false) {
			DispatchMessage.sendToAllInRange(toMove, msg);
			return;
		}

		// If it's a player who is moving then we need to handle characters
		// who should see the message via group follow

		PlayerCharacter player = (PlayerCharacter) toMove;

		player.setTimeStamp("lastMoveGate", System.currentTimeMillis());

		if (collide)
			DispatchMessage.dispatchMsgToInterestArea(player, msg, DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, true, false);
		else
			DispatchMessage.dispatchMsgToInterestArea(player, msg, DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, false, false);


		// Handle formation movement if needed

		if (player.getFollow() == false)
			return;


		City cityObject = null;
		Zone serverZone = null;

		serverZone = ZoneManager.findSmallestZone(player.getLoc());
		cityObject = (City) DbManager.getFromCache(GameObjectType.City, serverZone.getPlayerCityUUID());

		// Do not send group messages if player is on grid

		if (cityObject != null)
			return;

		// If player is not in a group we can exit here

		Group group = GroupManager.getGroup(player);

		if (group == null)
			return;

		// Echo group movement messages

		if (group.getGroupLead().getObjectUUID() == player.getObjectUUID())
			moveGroup(player, player.getClientConnection(), msg);
				
	}

	/**
	 * compare client and server location to verify that the two are in sync
	 *
	 * @param ac        the player character
	 * @param clientLoc location as reported by the client
	 * @param serverLoc location known to the server
	 * @return true if the two are in sync
	 */
	private static boolean checkSync(AbstractCharacter ac, Vector3fImmutable clientLoc, Vector3fImmutable serverLoc) {

		float desyncDist = clientLoc.distanceSquared2D(serverLoc);

		// desync logging
		if (MBServerStatics.MOVEMENT_SYNC_DEBUG)
			if (desyncDist > MBServerStatics.MOVEMENT_DESYNC_TOLERANCE * MBServerStatics.MOVEMENT_DESYNC_TOLERANCE)
				// our current location server side is a calc of last known loc + direction + speed and known time of last update
				Logger.debug("Movement out of sync for " + ac.getFirstName()
				+ ", Server Loc: " + serverLoc.getX() + ' ' + serverLoc.getZ()
				+ " , Client loc: " + clientLoc.getX() + ' ' + clientLoc.getZ()
				+ " desync distance " + desyncDist
				+ " moving=" + ac.isMoving());
			else
				Logger.debug( "Movement sync is good - desyncDist = " + desyncDist);

		if (ac.getDebug(1) && ac.getObjectType().equals(GameObjectType.PlayerCharacter))
			if (desyncDist > MBServerStatics.MOVEMENT_DESYNC_TOLERANCE * MBServerStatics.MOVEMENT_DESYNC_TOLERANCE) {
				PlayerCharacter pc = (PlayerCharacter) ac;
				ChatManager.chatSystemInfo(pc,
						"Movement out of sync for " + ac.getFirstName()
						+ ", Server Loc: " + serverLoc.getX() + ' ' + serverLoc.getZ()
						+ " , Client loc: " + clientLoc.getX() + ' ' + clientLoc.getZ()
						+ " desync distance " + desyncDist
						+ " moving=" + ac.isMoving());
			}

		// return indicator that the two are in sync or not
		return (desyncDist < 100f * 100f);

	}



	public static void finishChangeAltitude(AbstractCharacter ac, float targetAlt) {

		if (ac.getObjectType().equals(GameObjectType.PlayerCharacter) == false)
			return;

		//reset the getLoc timer before we clear other timers
		// otherwise the next call to getLoc will not be correct
		ac.resetLastSetLocUpdate();

		// call getLoc once as it processes loc to the ms
		Vector3fImmutable curLoc = ac.getLoc();

		if (MBServerStatics.MOVEMENT_SYNC_DEBUG)
			Logger.info("Finished Alt change, setting the end location to "
					+ ac.getEndLoc().getX() + ' ' + ac.getEndLoc().getZ()
					+ " moving=" + ac.isMoving()
					+ " and current location is " + curLoc.getX() + ' ' + curLoc.getZ());

		if (ac.getDebug(1) && ac.getObjectType().equals(GameObjectType.PlayerCharacter))
			ChatManager.chatSystemInfo((PlayerCharacter) ac, "Finished Alt change, setting the end location to " + ac.getEndLoc().getX() + ' ' + ac.getEndLoc().getZ() + " moving=" + ac.isMoving() + " and current location is " + curLoc.getX() + ' ' + curLoc.getZ());

		//Send run/walk/sit/stand to tell the client we are flying / landing etc
		ac.update();
		ac.stopMovement(ac.getLoc());
		if (ac.isAlive())
			MovementManager.sendRWSSMsg(ac);

		//Check collision again
	}


	// Handle formation movement in group

	public static void moveGroup(PlayerCharacter pc, ClientConnection origin, MoveToPointMsg msg) throws MsgSendException {
		// get forward vector
		Vector3f faceDir = new Vector3f(pc.getFaceDir().x, 0, pc.getFaceDir().z).normalize();
		// get perpendicular vector
		Vector3f crossDir = new Vector3f(faceDir.z, 0, -faceDir.x);

		//get source loc with altitude
		Vector3f sLoc = new Vector3f(pc.getLoc().x, pc.getAltitude(), pc.getLoc().z);

		Group group = GroupManager.getGroup(pc);
		Set<PlayerCharacter> members = group.getMembers();
		int pos = 0;
		for (PlayerCharacter member : members) {

			if (member == null)
				continue;
			if (member.getObjectUUID() == pc.getObjectUUID())
				continue;

			MoveToPointMsg groupMsg = new MoveToPointMsg(msg);

			// Verify group member should be moved

			pos++;
			if (member.getFollow() != true)
				continue;

			//get member loc with altitude, then range against source loc
			Vector3f mLoc = new Vector3f(member.getLoc().x, member.getAltitude(), member.getLoc().z);

			if (sLoc.distanceSquared2D(mLoc) > sqr(MBServerStatics.FORMATION_RANGE))
				continue;

			//don't move if player has taken damage from another player in last 60 seconds
			long lastAttacked = System.currentTimeMillis() - pc.getLastPlayerAttackTime();
			if (lastAttacked < 60000)
				continue;

			if (!member.isAlive())
				continue;

			//don't move if player is stunned or rooted
			PlayerBonuses bonus = member.getBonuses();
			if (bonus.getBool(ModType.Stunned, SourceType.None) || bonus.getBool(ModType.CannotMove, SourceType.None))
				continue;
			
			member.update();


			// All checks passed, let's move the player
			// First get the offset position
			Vector3f offset = Formation.getOffset(group.getFormation(), pos);
			Vector3fImmutable destination = pc.getEndLoc();
			// offset forwards or backwards
			destination = destination.add(faceDir.mult(offset.z));
			// offset left or right
			destination = destination.add(crossDir.mult(offset.x));
			//			ArrayList<AbstractWorldObject> awoList = WorldGrid.INSTANCE.getObjectsInRangePartial(member, member.getLoc().distance2D(destination) +1000, MBServerStatics.MASK_BUILDING);
			//
			//			boolean skip = false;
			//
			//			for (AbstractWorldObject awo: awoList){
			//				Building building = (Building)awo;
			//
			//				if (building.getBounds() != null){
			//					if (Bounds.collide(building, member.getLoc(), destination)){
			//						skip = true;
			//						break;
			//					}
			//
			//				}
			//
			//			}
			//
			//			if (skip)
			//				continue;
			//			if (member.isMoving())
			//				member.stopMovement();

			// Update player speed to match group lead speed and make standing
			if (member.isSit() || (member.isWalk() != pc.isWalk())) {
				member.setSit(false);
				member.setWalkMode(pc.isWalk());
				MovementManager.sendRWSSMsg(member);
			}

			//cancel any effects that break upon movement
			member.cancelOnMove();

			// send movement for other players to see
			groupMsg.setSourceID(member.getObjectUUID());
			groupMsg.setStartCoord(member.getLoc());
			groupMsg.setEndCoord(destination);
			groupMsg.clearTarget();
			DispatchMessage.sendToAllInRange(member, groupMsg);

			// update group member
			member.setFaceDir(destination.subtract2D(member.getLoc()).normalize());
			member.setEndLoc(destination);
		}
	}
	
	public static void translocate(AbstractCharacter teleporter, Vector3fImmutable targetLoc, Regions region) {


		if (targetLoc == null)
			return;

		Vector3fImmutable oldLoc = new Vector3fImmutable(teleporter.getLoc());

			teleporter.stopMovement(targetLoc);
			teleporter.setRegion(region);

			//mobs ignore region sets for now.
			if (teleporter.getObjectType().equals(GameObjectType.Mob)){
				teleporter.setInBuildingID(0);
				teleporter.setInBuilding(-1);
				teleporter.setInFloorID(-1);
				TeleportToPointMsg msg = new TeleportToPointMsg(teleporter, targetLoc.getX(), targetLoc.getY(), targetLoc.getZ(), 0, -1, -1);
				DispatchMessage.dispatchMsgToInterestArea(oldLoc, teleporter, msg, DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, false, false);
				return;
			}
		TeleportToPointMsg msg = new TeleportToPointMsg(teleporter, targetLoc.getX(), targetLoc.getY(), targetLoc.getZ(), 0, -1, -1);
		//we shouldnt need to send teleport message to new area, as loadjob should pick it up.
	//	DispatchMessage.dispatchMsgToInterestArea(teleporter, msg, DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, true, false);
		DispatchMessage.dispatchMsgToInterestArea(oldLoc, teleporter, msg, DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, true, false);
		
		if (teleporter.getObjectType().equals(GameObjectType.PlayerCharacter))
		InterestManager.INTERESTMANAGER.HandleLoadForTeleport((PlayerCharacter)teleporter);

	}
	
	public static void translocateToObject(AbstractCharacter teleporter, AbstractWorldObject worldObject) {

		Vector3fImmutable targetLoc = teleporter.getLoc();

		Vector3fImmutable oldLoc = new Vector3fImmutable(teleporter.getLoc());

		teleporter.stopMovement(teleporter.getLoc());

			//mobs ignore region sets for now.
			if (teleporter.getObjectType().equals(GameObjectType.Mob)){
				teleporter.setInBuildingID(0);
				teleporter.setInBuilding(-1);
				teleporter.setInFloorID(-1);
				TeleportToPointMsg msg = new TeleportToPointMsg(teleporter, targetLoc.getX(), targetLoc.getY(), targetLoc.getZ(), 0, -1, -1);
				DispatchMessage.dispatchMsgToInterestArea(oldLoc, teleporter, msg, DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, false, false);
				return;
			}

		boolean collide = false;
		int maxFloor = -1;
		int buildingID = 0;
		boolean isGroundLevel = false;
		HashSet<AbstractWorldObject> buildings = WorldGrid.getObjectsInRangePartial(teleporter, 200, MBServerStatics.MASK_BUILDING);
		for (AbstractWorldObject awo : buildings) {
			Building building = (Building) awo;
			if (collide)
				break;
		}

		if (!collide) {
			teleporter.setInBuildingID(0);
			teleporter.setInBuilding(-1);
			teleporter.setInFloorID(-1);
		} else {
			if (isGroundLevel) {
				teleporter.setInBuilding(0);
				teleporter.setInFloorID(-1);
			} else {
				teleporter.setInBuilding(maxFloor - 1);
				teleporter.setInFloorID(0);
			}
		}

		TeleportToPointMsg msg = new TeleportToPointMsg(teleporter, targetLoc.getX(), targetLoc.getY(), targetLoc.getZ(), 0, -1, -1);
		//we shouldnt need to send teleport message to new area, as loadjob should pick it up.
	//	DispatchMessage.dispatchMsgToInterestArea(teleporter, msg, DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, true, false);
		DispatchMessage.dispatchMsgToInterestArea(oldLoc, teleporter, msg, DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, true, false);
		
		if (teleporter.getObjectType().equals(GameObjectType.PlayerCharacter))
		InterestManager.INTERESTMANAGER.HandleLoadForTeleport((PlayerCharacter)teleporter);

	}

	private static void syncLoc(AbstractCharacter ac, Vector3fImmutable clientLoc, boolean useClientLoc) {
			ac.teleport(ac.getLoc());
	}
}