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

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

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

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


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

    public static int getType() {
        return 0;
    }

    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.loc) + worldObject.getAltitude());

        return region;
    }

    public static Regions GetRegionFromBuilding(Vector3fImmutable worldLoc, Building building) {
        Regions region = null;


        return region;
    }

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

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


        return this.health.get();

    }

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

        this.health.set(health);
    }

    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 = (PlayerCharacter) mob.guardCaptain;
                    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;
    }

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

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

    //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;

            if(this instanceof AbstractCharacter && this.region != null){
                this.loc = this.loc.setY(this.region.lerpY(this.loc) + this.getAltitude());
            } else{
                this.loc = this.loc.setY(Terrain.getWorldHeight(this.getLoc()) + 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 Vector3f getRot() {
        return rot;
    }

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

    public byte getTier() {
        return tier;
    }

    public void setTier(byte tier) {
        synchronized (this.rot) {
            this.tier = 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;
        }
    }

    public void setY(float y) {
        this.loc = this.loc.setY(y);
    }

    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;
    }

    public float getAltitude() {
        return altitude;
    }


    public ReadWriteLock getUpdateLock() {
        return updateLock;
    }

    public GridObjectType getGridObjectType() {
        return gridObjectType;
    }


    public boolean isMovingUp() {
        return movingUp;
    }

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


    //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;
    }


}