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


package engine.objects;

import engine.Enum.DispatchChannel;
import engine.Enum.EffectSourceType;
import engine.Enum.GameObjectType;
import engine.Enum.GridObjectType;
import engine.InterestManagement.HeightMap;
import engine.InterestManagement.WorldGrid;
import engine.job.AbstractScheduleJob;
import engine.job.JobContainer;
import engine.job.JobScheduler;
import engine.jobs.NoTimeJob;
import engine.math.AtomicFloat;
import engine.math.Bounds;
import engine.math.Vector3f;
import engine.math.Vector3fImmutable;
import engine.net.Dispatch;
import engine.net.DispatchMessage;
import engine.net.client.ClientConnection;
import engine.net.client.msg.UpdateEffectsMsg;
import engine.powers.EffectsBase;
import engine.server.MBServerStatics;
import org.pmw.tinylog.Logger;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public abstract class AbstractWorldObject extends AbstractGameObject {

	private String name = "";

	protected final ReadWriteLock locationLock = new ReentrantReadWriteLock(true);
	protected final ReadWriteLock updateLock = new ReentrantReadWriteLock(true);
	
	protected Vector3fImmutable loc = new Vector3fImmutable(0.0f, 0.0f, 0.0f);
	private byte tier = 0;
	private Vector3f rot = new Vector3f(0.0f, 0.0f, 0.0f);
	protected AtomicFloat health = new AtomicFloat();
	public float healthMax;
	protected boolean load = true;
	protected ConcurrentHashMap<String, Effect> effects = new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD, MBServerStatics.CHM_THREAD_LOW);
	private int objectTypeMask = 0;
	private Bounds bounds;
	
	public int gridX = -1;
	public int gridZ = -1;
	
	protected GridObjectType gridObjectType;
	
	protected float altitude = 0;
	protected Regions region;
	protected boolean movingUp = false;
	public Regions landingRegion = null;
	public Vector3fImmutable lastLoc = Vector3fImmutable.ZERO;

	/**
	 * No Id Constructor
	 */
	public AbstractWorldObject() {
		super();
	}

	/**
	 * Normal Constructor
	 */
	public AbstractWorldObject(int objectUUID) {
		super(objectUUID);
	}

	
	/**
	 * ResultSet Constructor
	 */
	public AbstractWorldObject(ResultSet rs) throws SQLException {
		super(rs);
	}

	//this should be called to handle any after load functions.
	public abstract void runAfterLoad();

	/*
	 * Getters
	 */
	public float getHealth() {


		return this.health.get();

	}
	public float getCurrentHitpoints(){
		return this.health.get();
	}

	public float getHealthMax() {
		return this.healthMax;
	}

	public ConcurrentHashMap<String, Effect> getEffects() {
		return this.effects;
	}

	//Add new effect
	public void addEffect(String name, int duration, AbstractScheduleJob asj, EffectsBase eb, int trains) {

		if (!isAlive() && eb.getToken() != 1672601862) {
			return;
		}
		JobContainer jc = JobScheduler.getInstance().scheduleJob(asj, duration);
		Effect eff = new Effect(jc, eb, trains);
		this.effects.put(name, eff);
		applyAllBonuses();
	}

	public Effect addEffectNoTimer(String name, EffectsBase eb, int trains, boolean isStatic) {
		NoTimeJob ntj = new NoTimeJob(this, name, eb, trains); //infinite timer

		if (this.getObjectType() == GameObjectType.Item || this.getObjectType() == GameObjectType.City){
			ntj.setEffectSourceType(this.getObjectType().ordinal());
			ntj.setEffectSourceID(this.getObjectUUID());
		}

		JobContainer jc = new JobContainer(ntj);
		Effect eff = new Effect(jc, eb, trains);
		if (isStatic)
			eff.setIsStatic(isStatic);
		this.effects.put(name, eff);
		applyAllBonuses();
		return eff;
	}

	//called when an effect runs it's course
	public void endEffect(String name) {

		Effect eff = this.effects.get(name);
		if (eff == null) {
			return;
		}
		if (!isAlive() && eff.getEffectsBase().getToken() != 1672601862) {
			return;
		}

		if (eff.cancel()) {

			eff.endEffect();
			this.effects.remove(name);
			if (this.getObjectType().equals(GameObjectType.PlayerCharacter))
			if (name.equals("Flight")){
				((PlayerCharacter)this).update();
				PlayerCharacter.GroundPlayer((PlayerCharacter)this);
			}
		}
		applyAllBonuses();
	}

	public void endEffectNoPower(String name) {

		Effect eff = this.effects.get(name);
		if (eff == null) {
			return;
		}
		if (!isAlive() && eff.getEffectsBase().getToken() != 1672601862) {
			return;
		}

		if (eff.cancel()) {
			eff.cancelJob();
			eff.endEffectNoPower();
			this.effects.remove(name);
		}
		applyAllBonuses();
	}

	//Called to cancel an effect prematurely.
	public void cancelEffect(String name, boolean overwrite) {

		Effect eff = this.effects.get(name);
		if (eff == null) {
			return;
		}
		if (!isAlive() && eff.getEffectsBase().getToken() != 1672601862) {
			return;
		}

		if (eff.cancel()) {
			eff.cancelJob();
			this.effects.remove(name);
			if (AbstractWorldObject.IsAbstractCharacter(this)) {
				((AbstractCharacter) this).cancelLastChantIfSame(eff);
			}
		}
		if (!overwrite) {
			applyAllBonuses();
		}
	}

	//Called when an object dies/is destroyed
	public void clearEffects() {
		for (String name : this.effects.keySet()) {
			Effect eff = this.effects.get(name);
			if (eff == null) {
				return;
			}

			//Dont remove deathshroud here!
			if (eff.getEffectToken() == 1672601862)
				continue;

			if (eff.cancel()) {
				if (eff.getPower() == null) {
					if (!eff.isStatic())
						eff.endEffectNoPower();
				}
				if (!eff.isStatic())
					eff.cancelJob();
			}

			this.effects.remove(name);
		}
		if (AbstractWorldObject.IsAbstractCharacter(this)) {
			((AbstractCharacter) this).cancelLastChant();
		}
		applyAllBonuses();
	}

	public void removeEffectBySource(EffectSourceType source, int trains, boolean removeAll) {
		if (!isAlive() && source.equals(EffectSourceType.DeathShroud) == false) {
			return;
		}

		//hacky way to dispell trebs.
		if (this.getObjectType() == GameObjectType.Mob){
			Mob mob = (Mob)this;
			if (mob.isSiege()){
				if (mob.isPet()){
					PlayerCharacter petOwner = mob.getOwner();
					if (petOwner != null && source.equals(EffectSourceType.Effect)){
						petOwner.dismissPet();
						return;
					}
				}
			}
		}
		boolean changed = false;
		String toRemove = "";
		int toRemoveToken = Integer.MAX_VALUE;
		for (String name : this.effects.keySet()) {
			Effect eff = this.effects.get(name);
			if (eff == null) {
				continue;
			}
			if (eff.containsSource(source) && trains >= eff.getTrains()) {
				if (removeAll) {
					//remove all effects of source type
					if (eff.cancel()) {
						eff.cancelJob();
					}
					this.effects.remove(name);
					changed = true;
					
					if (source.equals("Flight")){
						//ground player
						if (this.getObjectType().equals(GameObjectType.PlayerCharacter)){
							((PlayerCharacter)this).update();
							PlayerCharacter.GroundPlayer((PlayerCharacter)this);
						}
					}
				} else {
					//find lowest token of source type to remove
					int tok = eff.getEffectToken();
					if (tok != 0 && tok < toRemoveToken) {
						toRemove = name;
						toRemoveToken = tok;
					}
				}
			}
		}
		
		//WTF IS THIS?
		if (toRemoveToken < Integer.MAX_VALUE && this.effects.containsKey(toRemove)) {
			//remove lowest found token of source type
			Effect eff = this.effects.get(toRemove);
			if (eff != null) {
				changed = true;
				if (eff.cancel()) {
					eff.cancelJob();
				}
				this.effects.remove(toRemove);
				
				if (source.equals("Flight")){
					//ground player
					if (this.getObjectType().equals(GameObjectType.PlayerCharacter)){
						((PlayerCharacter)this).update();
						PlayerCharacter.GroundPlayer((PlayerCharacter)this);
					}
				}
				
			}
		}
		if (changed) {
			applyAllBonuses();
		}
	}

	public void sendAllEffects(ClientConnection cc) {
		UpdateEffectsMsg msg = new UpdateEffectsMsg(this);
		Dispatch dispatch = Dispatch.borrow((PlayerCharacter)this, msg);
		DispatchMessage.dispatchMsgDispatch(dispatch, DispatchChannel.PRIMARY);
	}

	public void applyAllBonuses() {
		if (AbstractWorldObject.IsAbstractCharacter(this)) {
			((AbstractCharacter) this).applyBonuses();
		}
	}

	public JobContainer getEffectJobContainer(String name) {
		Effect ef = this.effects.get(name);
		if (ef != null) {
			return ef.getJobContainer();
		}
		return null;
	}

	public AbstractScheduleJob getEffectJob(String name) {
		Effect ef = this.effects.get(name);
		if (ef == null) {
			return null;
		}
		JobContainer jc = ef.getJobContainer();
		if (jc != null) {
			return (AbstractScheduleJob) jc.getJob();
		}
		return null;
	}

	public boolean containsEffect(int token) {
		for (Effect eff : this.effects.values()) {
			if (eff != null) {
				if (eff.getEffectsBase() != null) {
					if (eff.getEffectsBase().getToken() == token) {
						return true;
					}
				}
			}
		}
		return false;
	}

	public int getObjectTypeMask() {
		return objectTypeMask;
	}


	public Vector3fImmutable getLoc() {
		return this.loc;
	}

	public Vector3f getRot() {
		return rot;
	}

	public byte getTier() {
		return tier;
	}

	public boolean isAlive() {
		if (AbstractWorldObject.IsAbstractCharacter(this)) {
			return this.isAlive();
		} else if (this.getObjectType().equals(GameObjectType.Building)) {
			return (!(((Building) this).getRank() < 0));
		} else {
			return true;
		}
	}

	/*
	 * Setters
	 */
	public void setObjectTypeMask(int mask) {
		this.objectTypeMask = mask;
	}

	//TODO return false if something goes wrong? resync player?
	public void setLoc(Vector3fImmutable loc) {
		locationLock.writeLock().lock();
		try {
			if (Float.isNaN(loc.x) || Float.isNaN(loc.z))
				return;
			
			if (loc.equals(Vector3fImmutable.ZERO))
				return;
			
			if (loc.x > MBServerStatics.MAX_WORLD_WIDTH || loc.z < MBServerStatics.MAX_WORLD_HEIGHT)
				return;
			this.lastLoc = new Vector3fImmutable(this.loc);
			this.loc = loc;
			this.loc = this.loc.setY(HeightMap.getWorldHeight(this) + this.getAltitude());
			
			//lets not add mob to world grid if he is currently despawned.
			if (this.getObjectType().equals(GameObjectType.Mob) && ((Mob)this).despawned)
				return;
			
			//do not add objectUUID 0 to world grid. dunno da fuck this doing why its doing but its doing... da fuck.
			if (this.getObjectUUID() == 0)
				return;
			WorldGrid.addObject(this,loc.x,loc.z);
			
		}catch(Exception e){
			Logger.error("Failed to set location for World Object. Type = " + this.getObjectType().name() + " : Name = " + this.getName());
			e.printStackTrace();
		} finally {
			locationLock.writeLock().unlock();
		}
		
	}
	
	public void setY(float y){
		this.loc = this.loc.setY(y);
	}


	public void setRot(Vector3f rotation) {
		synchronized (this.rot) {
			this.rot = rotation;
		}
	}

	public void setTier(byte tier) {
		synchronized (this.rot) {
			this.tier = tier;
		}
	}

	public static int getType() {
		return 0;
	}

	public boolean load() {
		return this.load;
	}

	/*
	 * Utils
	 */
	public String getName() {
		if (this.name.length() == 0) {
			return "Unnamed " + '('
					+ this.getObjectUUID() + ')';
		} else {
			return this.name;
		}
	}

	public String getSimpleName() {
		return this.name;
	}


	/**
	 * @return the bounds
	 */
	public Bounds getBounds() {
		return bounds;
	}

	public void setBounds(Bounds bounds) {

		this.bounds = bounds;
	}

	/**
	 * @param health the health to set
	 */
	public void setHealth(float health) {

		this.health.set(health);
	}

	public static boolean IsAbstractCharacter(AbstractWorldObject awo){
		
		if (awo == null)
			return false;

		if (awo.getObjectType() == GameObjectType.PlayerCharacter || awo.getObjectType() == GameObjectType.Mob || awo.getObjectType() == GameObjectType.NPC)
			return true;
		return false;
	}

	public static void RemoveFromWorldGrid(AbstractWorldObject gridObjectToRemove){
		if (gridObjectToRemove.gridX < 0 || gridObjectToRemove.gridZ < 0)
			return;
		
		ConcurrentHashMap<Integer,AbstractWorldObject> gridMap;
		switch(gridObjectToRemove.gridObjectType){
		case STATIC:
			gridMap = WorldGrid.StaticGridMap[gridObjectToRemove.gridX][gridObjectToRemove.gridZ];
			break;
		case DYNAMIC:
				gridMap = WorldGrid.DynamicGridMap[gridObjectToRemove.gridX][gridObjectToRemove.gridZ];
			break;
		default:
			gridMap = WorldGrid.StaticGridMap[gridObjectToRemove.gridX][gridObjectToRemove.gridZ];
			break;
		
		}
		
		if (gridMap == null){
			Logger.info("Null gridmap for Object UUD: " + gridObjectToRemove);
			return;
		}
			
		
	
		
		gridMap.remove(gridObjectToRemove.getObjectUUID());
		gridObjectToRemove.gridX = -1;
		gridObjectToRemove.gridZ = -1;
		
	}
	
	public static boolean AddToWorldGrid(AbstractWorldObject gridObjectToAdd, int x, int z){
		try{
			
			ConcurrentHashMap<Integer,AbstractWorldObject> gridMap;
            if (gridObjectToAdd.gridObjectType.equals(GridObjectType.STATIC))
				gridMap = WorldGrid.StaticGridMap[x][z];
			else
				gridMap = WorldGrid.DynamicGridMap[x][z];
			
			gridMap.put(gridObjectToAdd.getObjectUUID(),gridObjectToAdd);
			gridObjectToAdd.gridX = x;
			gridObjectToAdd.gridZ = z;
			return true;
		}catch(Exception e){
			Logger.error(e);
			return false;
		}
		
	}
	
	public static Regions GetRegionByWorldObject(AbstractWorldObject worldObject){
		Regions region = null;
		
		if (worldObject.getObjectType().equals(GameObjectType.PlayerCharacter))
			if (((PlayerCharacter)worldObject).isFlying())
				return null;
		//Find building
		for (AbstractWorldObject awo:WorldGrid.getObjectsInRangePartial(worldObject.getLoc(), MBServerStatics.STRUCTURE_LOAD_RANGE, MBServerStatics.MASK_BUILDING)){
		Building building = (Building)awo;
		if (!Bounds.collide(worldObject.getLoc(), building.getBounds()))
			continue;
		
		//find regions that intersect x and z, check if object can enter.
		for (Regions toEnter: building.getBounds().getRegions()){
			if (toEnter.isPointInPolygon(worldObject.getLoc())){
				if (Regions.CanEnterRegion(worldObject, toEnter))
					if (region == null)
						region = toEnter;
					else // we're using a low level to high level tree structure, database not always in order low to high.
						//check for highest level index.
					if(region != null && toEnter.highLerp.y > region.highLerp.y)
					region = toEnter;
					
				
			}
		}
	}
	
		//set players new altitude to region lerp altitude.
		if (region != null)
			if (region.center.y == region.highLerp.y)
				worldObject.loc = worldObject.loc.setY(region.center.y + worldObject.getAltitude());
			else
			worldObject.loc = worldObject.loc.setY(region.lerpY(worldObject) + worldObject.getAltitude());
		
		return region;
	}
	
	public static Regions GetRegionFromBuilding(Vector3fImmutable worldLoc, Building building){
		Regions region = null;
		
		
		return region;
	}

	public float getAltitude() {
		return altitude;
	}


	public ReadWriteLock getUpdateLock() {
		return updateLock;
	}

	public GridObjectType getGridObjectType() {
		return gridObjectType;
	}

	public Regions getRegion() {
		return region;
	}


	public boolean isMovingUp() {
		return movingUp;
	}

	public void setMovingUp(boolean movingUp) {
		this.movingUp = movingUp;
	}

	public void setRegion(Regions region) {
		this.region = region;
	}
	
	//used for interestmanager loading and unloading objects to client.
	// if not in grid, unload from player.
	public boolean isInWorldGrid(){
		if (this.gridX == -1 && this.gridZ == -1)
			return false;
		
		return true;
	}
	

}