package de.slikey.effectlib;

import de.slikey.effectlib.util.DynamicLocation;

import org.bukkit.Particle;
import org.bukkit.Color;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.util.Vector;

import java.util.ArrayList;
import java.util.List;

public abstract class Effect implements Runnable {

    /**
     * Handles the type, the effect is played.
     *
     * @see {@link de.slikey.effectlib.EffectType}
     */
    public EffectType type = EffectType.INSTANT;

    /**
     * Can be used to colorize certain particles. As of 1.8, those
     * include SPELL_MOB_AMBIENT, SPELL_MOB and REDSTONE.
     */
    public Color color = null;

    /**
     * This can be used to give particles a set speed when spawned.
     * This will not work with colored particles.
     */
    @Deprecated
    public float speed = 0;

    /**
     * This can be used to give particles a set speed when spawned.
     * This will not work with colored particles.
     *
     * This is a replacement for "speed"
     */
    public float particleData = 0;

    /**
     * Delay to wait for delayed effects.
     *
     * @see {@link de.slikey.effectlib.EffectType}
     */
    public int delay = 0;

    /**
     * Interval to wait for repeating effects.
     *
     * @see {@link de.slikey.effectlib.EffectType}
     */
    public int period = 1;

    /**
     * Amount of repetitions to do.
     * Set this to -1 for an infinite effect
     *
     * @see {@link de.slikey.effectlib.EffectType}
     */
    public int iterations = 0;

    /**
     * Total duration of this effect in milliseconds.
     *
     * If set, this will adjust iterations to match
     * the defined delay such that the effect lasts
     * a specific duration.
     */
    public Integer duration = null;

    /**
     * Callback to run, after effect is done.
     *
     * @see {@link java.lang.Runnable}
     */
    public Runnable callback = null;

    /**
     * Display particles to players within this radius.
     */
    public float visibleRange = 32;

    /**
     * If true, and a "target" Location or Entity is set, the two Locations
     * will orient to face one another.
     */
    public boolean autoOrient = false;

    /**
     * If set, will offset the origin location
     */
    public Vector offset = null;

    /**
     * If set, will offset the origin location, relative to the origin direction
     */
    public Vector relativeOffset = null;

    /**
     * If set, will offset the target location
     */
    public Vector targetOffset = null;

    /**
     * These are used to modify the direction of the origin Location.
     */
    public float yawOffset = 0;
    public float pitchOffset = 0;
    public Float yaw = null;
    public Float pitch = null;

    /**
     * If set to false, Entity-bound locations will not update during the Effect
     */
    public boolean updateLocations = true;

    /**
     * If set to false, Entity-bound directions will not update during the Effect
     */
    public boolean updateDirections = true;

    /**
     * A specific player who should see this effect.
     */
    public Player targetPlayer;

    /**
     * A group of players who should see this effect.
     */
    public List<Player> targetPlayers;

    /**
     * The Material and data to use for block and item break particles
     */
    public Material material;
    public byte materialData;

    /**
     * These can be used to spawn multiple particles per packet.
     * It will not work with colored particles, however.
     */
    public int particleCount = 1;

    /**
     * These can be used to apply an offset to spawned particles, particularly
     * useful when spawning multiples.
     */
    public float particleOffsetX = 0;
    public float particleOffsetY = 0;
    public float particleOffsetZ = 0;

    /**
     * This can be used to scale up or down a particle's size.
     *
     * This currently only works with the redstone particle in 1.13 and up.
     */
    public float particleSize = 1;

    /**
     * If set, will run asynchronously.
     * Some effects don't support this (TurnEffect, JumpEffect)
     *
     * Generally this shouldn't be changed, unless you want to
     * make an async effect synchronous.
     */
    public boolean asynchronous = true;
    protected final EffectManager effectManager;

    protected DynamicLocation origin = null;
    protected DynamicLocation target = null;

    /**
     * This will store the base number of iterations
     */
    protected int maxIterations;

    /**
     * Should this effect stop playing if the origin entity becomes invalid?
     */
    public boolean disappearWithOriginEntity = false;
    
    /**
     * Should this effect stop playing if the target entity becomes invalid?
     */
    public boolean disappearWithTargetEntity = false;

    private boolean done = false;

    public Effect(EffectManager effectManager) {
        if (effectManager == null) {
            throw new IllegalArgumentException("EffectManager cannot be null!");
        }
        this.effectManager = effectManager;
        this.visibleRange = effectManager.getParticleRange();
    }

    public final void cancel() {
        cancel(true);
    }

    public final void cancel(boolean callback) {
        if (callback) {
            done();
        } else {
            done = true;
        }
    }

    public final boolean isDone() {
        return done;
    }

    public abstract void onRun();

    /**
     * Called when this effect is done playing (when {@link #done()} is called).
     */
    public void onDone() {
    }

    @Override
    public final void run() {
        if (!validate()) {
            cancel();
            return;
        }
        if (done) {
            return;
        }
        try {
            onRun();
        } catch (Exception ex) {
            done();
            effectManager.onError(ex);
        }
        if (type == EffectType.REPEATING) {
            if (iterations == -1) {
                return;
            }
            iterations--;
            if (iterations < 1) {
                done();
            }
        } else {
            done();
        }
    }

    /**
     * Effects should override this if they want to be reusable, this is called prior to starting so
     * state can be reset.
     */
    protected void reset() {
        this.done = false;
    }

    public void prepare() {
        reset();
        updateDuration();
    }

    public final void start() {
        prepare();
        effectManager.start(this);
    }

    public final void infinite() {
        type = EffectType.REPEATING;
        iterations = -1;
    }

    /**
     * Extending Effect classes can use this to determine the Entity this
     * Effect is centered upon.
     *
     * This may return null, even for an Effect that was set with an Entity,
     * if the Entity gets GC'd.
     */
    public Entity getEntity() {
        return origin == null ? null : origin.getEntity();
    }

    /**
     * Extending Effect classes can use this to determine the Entity this
     * Effect is targeted upon. This is probably a very rare case, such as
     * an Effect that "links" two Entities together somehow. (Idea!)
     *
     * This may return null, even for an Effect that was set with a target Entity,
     * if the Entity gets GC'd.
     */
    public Entity getTargetEntity() {
        return target == null ? null : target.getEntity();
    }

    /**
     * Extending Effect classes should use this method to obtain the
     * current "root" Location of the effect.
     *
     * This method will not return null when called from onRun. Effects
     * with invalid locations will be cancelled.
     */
    public final Location getLocation() {
        return origin == null ? null : origin.getLocation();
    }

    /**
     * Extending Effect classes should use this method to obtain the
     * current "target" Location of the effect.
     *
     * Unlike getLocation, this may return null.
     */
    public final Location getTarget() {
        return target == null ? null : target.getLocation();
    }

    /**
     * Set the Location this Effect is centered on.
     */
    public void setDynamicOrigin(DynamicLocation location) {
        if (location == null) {
            throw new IllegalArgumentException("Origin Location cannot be null!");
        }
        origin = location;

        if (offset != null) {
            origin.addOffset(offset);
        }
        if (relativeOffset != null) {
            origin.addRelativeOffset(relativeOffset);
        }
        origin.setDirectionOffset(yawOffset, pitchOffset);
        origin.setYaw(yaw);
        origin.setPitch(pitch);
        origin.setUpdateLocation(updateLocations);
        origin.setUpdateDirection(updateDirections);
        origin.updateDirection();
    }

    /**
     * Set the Location this Effect is targeting.
     */
    public void setDynamicTarget(DynamicLocation location) {
        target = location;
        if (target != null && targetOffset != null) {
            target.addOffset(targetOffset);
        }
        if (target != null) {
            target.setUpdateLocation(updateLocations);
            target.setUpdateDirection(updateDirections);
        }
    }

    protected final boolean validate() {
        // Check if the origin and target entities are present
        if (disappearWithOriginEntity && (origin != null && !origin.hasValidEntity())) {
            return false;
        }
        
        if (disappearWithTargetEntity && (target != null && !target.hasValidEntity())) {
            return false;
        }
        
        // Check for a valid Location
        updateLocation();
        updateTarget();
        Location location = getLocation();
        if (location == null) {
            return false;
        }
        if (autoOrient) {
            Location targetLocation = target == null ? null : target.getLocation();
            if (targetLocation != null) {
                Vector direction = targetLocation.toVector().subtract(location.toVector());
                location.setDirection(direction);
                targetLocation.setDirection(direction.multiply(-1));
            }
        }

        return true;
    }

    protected void updateDuration() {
        if (duration != null) {
            if (period < 1) {
                period = 1;
            }
            iterations = duration / period / 50;
        }
        maxIterations = iterations;
    }

    protected void updateLocation() {
        if (origin != null) {
            origin.update();
        }
    }

    protected void updateTarget() {
        if (target != null) {
            target.update();
        }
    }

    protected void display(Particle effect, Location location) {
        display(effect, location, this.color);
    }

    protected void display(Particle particle, Location location, Color color) {
        display(particle, location, color, particleData != 0 ? particleData : speed, particleCount);
    }

    protected void display(Particle particle, Location location, float speed, int amount) {
        display(particle, location, this.color, speed, amount);
    }

    protected void display(Particle particle, Location location, Color color, float speed, int amount) {
        if (targetPlayers == null && targetPlayer != null) {
            targetPlayers = new ArrayList<Player>();
            targetPlayers.add(targetPlayer);
        }
        effectManager.display(particle, location, particleOffsetX, particleOffsetY, particleOffsetZ, speed, amount,
                particleSize, color, material, materialData, visibleRange, targetPlayers);
    }

    private void done() {
        done = true;
        effectManager.done(this);
        onDone();
    }

    public EffectType getType() {
        return type;
    }

    public boolean isAsynchronous() {
        return asynchronous;
    }

    public int getDelay() {
        return delay;
    }

    public int getPeriod() {
        return period;
    }
    
    public void setEntity(Entity entity) {
        setDynamicOrigin(new DynamicLocation(entity));
    }

    public void setLocation(Location location) {
        setDynamicOrigin(new DynamicLocation(location));
    }

    public void setTargetEntity(Entity entity) {
        target = new DynamicLocation(entity);
    }

    public void setTargetLocation(Location location) {
        target = new DynamicLocation(location);
    }

    public Player getTargetPlayer() {return this.targetPlayer; }
    public void setTargetPlayer(Player p){ this.targetPlayer = p; }
}