/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2020 Crypto Morin
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
 * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
 * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package com.cryptomorin.xseries.particles;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.bukkit.Location;
import org.bukkit.Particle;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Entity;
import org.bukkit.util.Vector;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.awt.*;
import java.util.Objects;

/**
 * By default the particle xyz offsets and speed aren't 0, but
 * everything will be 0 by default in this class.
 * Particles are spawned to a location. So all the nearby players can see it.
 * <p>
 * The fields of this class are publicly accessible for ease of use.
 * All the fields can be null except the particle type.
 * <p>
 * For cross-version compatibility, instead of Bukkit's {@link org.bukkit.Color}
 * the java awt {@link Color} class is used.
 * <p>
 * the data field is used to store special particle data, such as colored particles.
 * For colored particles a float list is used since the particle size is a float.
 * The format of float list data for a colored particle is:
 * <code>[r, g, b, size]</code>
 *
 * @author Crypto Morin
 * @version 3.0.0
 * @see XParticle
 */
public class ParticleDisplay {
    private static final boolean ISFLAT = XParticle.getParticle("FOOTSTEP") == null;
    @Nonnull
    public Particle particle;
    public Location location;
    public int count;
    public double offsetx, offsety, offsetz;
    public double extra;
    public Vector rotation;
    public Object data;

    /**
     * Make a new instance of particle display.
     * The position of each particle will be randomized positively and negatively by the offset parameters on each axis.
     *
     * @param particle the particle to spawn.
     * @param location the location to spawn the particle at.
     * @param count    the count of particles to spawn.
     * @param offsetx  the x offset.
     * @param offsety  the y offset.
     * @param offsetz  the z offset.
     * @param extra    in most cases extra is the speed of the particles.
     */
    public ParticleDisplay(@Nonnull Particle particle, @Nullable Location location, int count, double offsetx, double offsety, double offsetz, double extra) {
        this.particle = particle;
        this.location = location;
        this.count = count;
        this.offsetx = offsetx;
        this.offsety = offsety;
        this.offsetz = offsetz;
        this.extra = extra;
    }

    public ParticleDisplay(@Nonnull Particle particle, @Nullable Location location, int count, double offsetx, double offsety, double offsetz) {
        this(particle, location, count, offsetx, offsety, offsetz, 0);
    }

    public ParticleDisplay(@Nonnull Particle particle, @Nullable Location location, int count) {
        this(particle, location, count, 0, 0, 0);
    }

    public ParticleDisplay(@Nonnull Particle particle, @Nullable Location location) {
        this(particle, location, 0);
    }

    /**
     * Builds a simple ParticleDisplay object with cross-version
     * compatible {@link org.bukkit.Particle.DustOptions} properties.
     *
     * @param location the location of the display.
     * @param size     the size of the dust.
     * @return a redstone colored dust.
     * @see #simple(Location, Particle)
     * @since 1.0.0
     */
    @Nonnull
    public static ParticleDisplay colored(@Nullable Location location, int r, int g, int b, float size) {
        ParticleDisplay dust = new ParticleDisplay(Particle.REDSTONE, location, 1, 0, 0, 0, 0);
        dust.data = new float[]{r, g, b, size};
        return dust;
    }

    /**
     * Builds a simple ParticleDisplay object with cross-version
     * compatible {@link org.bukkit.Particle.DustOptions} properties.
     *
     * @param location the location of the display.
     * @param color    the color of the particle.
     * @param size     the size of the dust.
     * @return a redstone colored dust.
     * @see #colored(Location, int, int, int, float)
     * @since 3.0.0
     */
    @Nonnull
    public static ParticleDisplay colored(@Nullable Location location, @Nonnull Color color, float size) {
        return colored(location, color.getRed(), color.getGreen(), color.getBlue(), size);
    }

    /**
     * Builds a simple ParticleDisplay object.
     * An invocation of this method yields exactly the same result as the expression:
     * <p>
     * <blockquote>
     * new ParticleDisplay(particle, location, 1, 0, 0, 0, 0);
     * </blockquote>
     *
     * @param location the location of the display.
     * @param particle the particle of the display.
     * @return a simple ParticleDisplay.
     * @since 1.0.0
     */
    @Nonnull
    public static ParticleDisplay simple(@Nullable Location location, @Nonnull Particle particle) {
        Objects.requireNonNull(particle, "Cannot build ParticleDisplay with null particle");
        return new ParticleDisplay(particle, location, 1, 0, 0, 0, 0);
    }

    /**
     * A quick access method to display a simple particle.
     * An invocation of this method yields the same result as the expression:
     * <p>
     * <blockquote>
     * ParticleDisplay.simple(location, particle).spawn();
     * </blockquote>
     *
     * @param location the location of the particle.
     * @param particle the particle to show.
     * @return a simple ParticleDisplay.
     * @since 1.0.0
     */
    public static ParticleDisplay display(@Nonnull Location location, @Nonnull Particle particle) {
        Objects.requireNonNull(location, "Cannot display particle in null location");
        ParticleDisplay display = simple(location, particle);
        display.spawn();
        return display;
    }

    /**
     * Builds particle settings from a configuration section.
     *
     * @param location the location for this particle settings.
     * @param config   the config section for the settings.
     * @return a parsed ParticleDisplay.
     * @since 1.0.0
     */
    @Nonnull
    public static ParticleDisplay fromConfig(@Nullable Location location, @Nonnull ConfigurationSection config) {
        Objects.requireNonNull(config, "Cannot parse ParticleDisplay from a null config section");
        Particle particle = XParticle.getParticle(config.getString("particle"));
        if (particle == null) particle = Particle.FLAME;
        int count = config.getInt("count");
        double extra = config.getDouble("extra");
        double offsetx = 0, offsety = 0, offsetz = 0;

        String offset = config.getString("offset");
        if (offset != null) {
            String[] offsets = StringUtils.split(offset, ',');
            if (offsets.length > 0) {
                offsetx = NumberUtils.toDouble(offsets[0]);
                if (offsets.length > 1) {
                    offsety = NumberUtils.toDouble(offsets[1]);
                    if (offsets.length > 2) {
                        offsetz = NumberUtils.toDouble(offsets[2]);
                    }
                }
            }
        }

        double x = 0, y = 0, z = 0;
        String rotation = config.getString("rotation");
        if (rotation != null) {
            String[] rotations = StringUtils.split(rotation, ',');
            if (rotations.length > 0) {
                x = NumberUtils.toDouble(rotations[0]);
                if (rotations.length > 1) {
                    y = NumberUtils.toDouble(rotations[1]);
                    if (rotations.length > 2) {
                        z = NumberUtils.toDouble(rotations[2]);
                    }
                }
            }
        }

        float[] rgbs = null;
        String color = config.getString("color");
        if (color != null) {
            String[] colors = StringUtils.split(rotation, ',');
            if (colors.length >= 3) rgbs = new float[]
                    {NumberUtils.toInt(colors[0]), NumberUtils.toInt(colors[1]), NumberUtils.toInt(colors[2]),
                            (colors.length > 3 ? NumberUtils.toFloat(colors[0]) : 1.0f)};
        }

        Vector rotate = new Vector(x, y, z);
        ParticleDisplay display = new ParticleDisplay(particle, location, count, offsetx, offsety, offsetz, extra);
        display.rotation = rotate;
        display.data = rgbs;
        return display;
    }

    /**
     * Rotates the given xyz with the given rotation radians and
     * adds the to the specified location.
     *
     * @param location the location to add the rotated axis.
     * @param rotation the xyz rotation radians.
     * @return a cloned rotated location.
     * @since 3.0.0
     */
    @Nonnull
    public static Location rotate(@Nonnull Location location, double x, double y, double z, @Nonnull Vector rotation) {
        Location loc;
        if (rotation != null) {
            Vector rotate = new Vector(x, y, z);
            if (rotation.getX() != 0) XParticle.rotateAroundX(rotate, rotation.getX());
            if (rotation.getY() != 0) XParticle.rotateAroundY(rotate, rotation.getY());
            if (rotation.getZ() != 0) XParticle.rotateAroundZ(rotate, rotation.getZ());
            loc = location.clone().add(rotate);
        } else loc = location.clone().add(x, y, z);
        return loc;
    }

    /**
     * Changes the particle count of the particle settings.
     *
     * @param count the particle count.
     * @return the same particle display.
     * @since 3.0.0
     */
    @Nonnull
    public ParticleDisplay withCount(int count) {
        this.count = count;
        return this;
    }

    /**
     * Adds color properties to the particle settings.
     *
     * @param color the RGB color of the particle.
     * @param size  the size of the particle.
     * @return a colored particle.
     * @see #colored(Location, Color, float)
     * @since 3.0.0
     */
    @Nonnull
    public ParticleDisplay withColor(@Nonnull Color color, float size) {
        data = new float[]{color.getRed(), color.getGreen(), color.getBlue(), size};
        return this;
    }

    /**
     * Adjusts the rotation settings to face the entitys direction.
     * Only some of the shapes support this method.
     *
     * @param entity the entity to face.
     * @return the same particle display.
     * @see #rotate(Vector)
     * @since 3.0.0
     */
    @Nonnull
    public ParticleDisplay faceEntity(@Nonnull Entity entity) {
        Objects.requireNonNull(entity, "Cannot face null entity");
        Location loc = entity.getLocation();
        this.rotation = new Vector(Math.toRadians(loc.getPitch() + 90), Math.toRadians(-loc.getYaw()), 0);
        return this;
    }

    /**
     * Clones the location of this particle display and adds xyz.
     *
     * @param x the x to add to the location.
     * @param y the y to add to the location.
     * @param z the z to add to the location.
     * @return the cloned location.
     * @see #clone()
     * @since 1.0.0
     */
    @Nullable
    public Location cloneLocation(double x, double y, double z) {
        if (location == null) return null;
        return location.clone().add(x, y, z);
    }

    /**
     * Clones this particle settings and adds xyz to its location.
     *
     * @param x the x to add.
     * @param y the y to add.
     * @param z the z to add.
     * @return the cloned ParticleDisplay.
     * @see #clone()
     * @since 1.0.0
     */
    @Nonnull
    public ParticleDisplay cloneWithLocation(double x, double y, double z) {
        ParticleDisplay display = clone();
        if (location == null) return display;
        display.location.add(x, y, z);
        return display;
    }

    /**
     * Clones this particle settings.
     *
     * @return the cloned ParticleDisplay.
     * @see #cloneWithLocation(double, double, double)
     * @see #cloneLocation(double, double, double)
     */
    @SuppressWarnings("MethodDoesntCallSuperMethod")
    @Override
    @Nonnull
    public ParticleDisplay clone() {
        ParticleDisplay display = new ParticleDisplay(particle, (location == null ? null : location.clone()), count, offsetx, offsety, offsetz, extra);
        if (rotation != null) display.rotation = rotation.clone();
        display.data = data;
        return display;
    }

    /**
     * Rotates the particle position based on this vector.
     *
     * @param vector the vector to rotate from. The xyz values of this vectors must be radians.
     * @see #rotate(double, double, double)
     * @since 1.0.0
     */
    @Nonnull
    public ParticleDisplay rotate(@Nonnull Vector vector) {
        Objects.requireNonNull(vector, "Cannot rotate ParticleDisplay with null vector");
        if (rotation == null) rotation = vector;
        else rotation.add(vector);
        return this;
    }

    /**
     * Rotates the particle position based on the xyz radians.
     * Rotations are only supported for some shapes in {@link XParticle}.
     * Rotating some of them can result in weird shapes.
     *
     * @see #rotate(Vector)
     * @since 3.0.0
     */
    @Nonnull
    public ParticleDisplay rotate(double x, double y, double z) {
        rotate(new Vector(x, y, z));
        return this;
    }

    /**
     * Set the xyz offset of the particle settings.
     *
     * @since 1.1.0
     */
    @Nonnull
    public ParticleDisplay offset(double x, double y, double z) {
        offsetx = x;
        offsety = y;
        offsetz = z;
        return this;
    }

    /**
     * When a particle is set to be directional it'll only
     * spawn one particle and the xyz offset values are used for
     * the direction of the particle.
     * <p>
     * Colored particles in 1.12 and below don't support this.
     *
     * @see #isDirectional()
     * @since 1.1.0
     */
    @Nonnull
    public ParticleDisplay directional() {
        count = 0;
        return this;
    }

    /**
     * Check if this particle setting is a directional particle.
     *
     * @return true if the particle is directional, otherwise false.
     * @see #directional()
     * @since 2.1.0
     */
    public boolean isDirectional() {
        return count == 0;
    }

    /**
     * Spawns the particle at the current location.
     *
     * @see #spawn(boolean)
     * @since 2.0.1
     */
    public void spawn() {
        spawn(this.location, false);
    }

    /**
     * Spawns the particle at the current location.
     *
     * @see #spawn(Location, boolean)
     * @since 3.0.0
     */
    public void spawn(boolean rotate) {
        spawn(this.location, rotate);
    }

    /**
     * Spawns the particle at the given location.
     *
     * @see #spawn(Location, boolean)
     * @since 1.0.0
     */
    public void spawn(@Nonnull Location location) {
        spawn(location, false);
    }

    /**
     * Adds xyz of the given vector to the cloned location before
     * spawning particles.
     *
     * @param location the xyz to add.
     * @since 1.0.0
     */
    public void spawn(@Nonnull Vector location) {
        Objects.requireNonNull(location, "Cannot add xyz of null vector to ParticleDisplay");
        spawn(location.getX(), location.getY(), location.getZ());
    }

    /**
     * Adds xyz to the cloned loaction before spawning particle.
     *
     * @since 1.0.0
     */
    @Nonnull
    public Location spawn(double x, double y, double z) {
        Location loc = rotate(location, x, y, z, rotation);
        spawn(loc, false);
        return loc;
    }

    /**
     * Displays the particle in the specified location.
     * This method does not support rotations if used directly.
     *
     * @param loc the location to display the particle at.
     * @see #spawn(double, double, double)
     * @since 2.1.0
     */
    public void spawn(@Nonnull Location loc, boolean rotate) {
        if (rotate) loc = rotate(location, loc.getX(), loc.getY(), loc.getZ(), rotation);
        if (data != null) {
            if (data instanceof float[]) {
                float[] datas = (float[]) data;
                if (ISFLAT) {
                    Particle.DustOptions dust = new Particle.DustOptions(org.bukkit.Color
                            .fromRGB((int) datas[0], (int) datas[1], (int) datas[2]), datas[3]);
                    loc.getWorld().spawnParticle(particle, loc, count, offsetx, offsety, offsetz, extra, dust);
                } else {
                    loc.getWorld().spawnParticle(particle, loc, count, (int) datas[0], (int) datas[1], (int) datas[2], datas[3]);
                }
            }
        } else {
            loc.getWorld().spawnParticle(particle, loc, count, offsetx, offsety, offsetz, extra);
        }
    }
}