package de.slikey.effectlib;

import de.slikey.effectlib.math.Transforms;
import de.slikey.effectlib.util.ConfigUtils;
import de.slikey.effectlib.util.Disposable;
import de.slikey.effectlib.util.DynamicLocation;
import de.slikey.effectlib.util.ImageLoadCallback;
import de.slikey.effectlib.util.ImageLoadTask;
import de.slikey.effectlib.util.ParticleDisplay;

import org.bukkit.Bukkit;
import org.bukkit.Color;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Particle;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.MemoryConfiguration;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitScheduler;
import org.bukkit.scheduler.BukkitTask;
import org.bukkit.util.Vector;

import java.awt.Font;
import java.awt.image.BufferedImage;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;

import com.google.common.base.CaseFormat;

/**
 * Dispose the EffectManager if you don't need him anymore.
 *
 * @author Kevin
 *
 */
public class EffectManager implements Disposable {

    private static List<EffectManager> effectManagers;
    private static Map<String, Class<? extends Effect>> effectClasses = new HashMap<String, Class<? extends Effect>>();
    private final Plugin owningPlugin;
    private final Map<Effect, BukkitTask> effects;
    private ParticleDisplay display;
    private boolean disposed;
    private boolean disposeOnTermination;
    private boolean debug = false;
    private int visibleRange = 32;
    private File imageCacheFolder;
    private Map<String, BufferedImage[]> imageCache = new HashMap<String, BufferedImage[]>();

    public EffectManager(Plugin owningPlugin) {
        imageCacheFolder = owningPlugin == null ? null : new File(owningPlugin.getDataFolder(), "imagecache");
        this.owningPlugin = owningPlugin;
        Transforms.setEffectManager(this);
        effects = new HashMap<Effect, BukkitTask>();
        disposed = false;
        disposeOnTermination = false;
    }

    private ParticleDisplay getDisplay() {
        if (display == null) {
            display = ParticleDisplay.newInstance();
        }
        display.setManager(this);

        return display;
    }

    public void display(Particle particle, Location center, float offsetX, float offsetY, float offsetZ, float speed, int amount, float size, Color color, Material material, byte materialData, double range, List<Player> targetPlayers) {
        getDisplay().display(particle, center, offsetX, offsetY, offsetZ, speed, amount, size, color, material, materialData, range, targetPlayers);
    }

    public void start(Effect effect) {
        if (disposed) {
            throw new IllegalStateException("EffectManager is disposed and not able to accept any effects.");
        }
        if (disposeOnTermination) {
            throw new IllegalStateException("EffectManager is awaiting termination to dispose and not able to accept any effects.");
        }

        if (effects.containsKey(effect)) {
            effect.cancel(false);
        }

        if (!owningPlugin.isEnabled()) return;

        BukkitScheduler s = Bukkit.getScheduler();
        BukkitTask task = null;
        switch (effect.getType()) {
            case INSTANT:
                if(effect.isAsynchronous()) {
                    task = s.runTaskAsynchronously(owningPlugin, effect);
                } else {
                    task = s.runTask(owningPlugin, effect);
                }
                break;
            case DELAYED:
                if (effect.isAsynchronous()) {
                    task = s.runTaskLaterAsynchronously(owningPlugin, effect, effect.getDelay());
                } else {
                    task = s.runTaskLater(owningPlugin, effect, effect.getDelay());
                }
                break;
            case REPEATING:
                if (effect.isAsynchronous()) {
                    task = s.runTaskTimerAsynchronously(owningPlugin, effect, effect.getDelay(), effect.getPeriod());
                } else {
                    task = s.runTaskTimer(owningPlugin, effect, effect.getDelay(), effect.getPeriod());
                }
                break;
        }
        synchronized (this) {
            effects.put(effect, task);
        }
    }

    public Effect start(String effectClass, ConfigurationSection parameters, Location origin, Entity originEntity) {
        return start(effectClass, parameters, origin, null, originEntity, null, null);
    }

    public Effect start(String effectClass, ConfigurationSection parameters, Entity originEntity) {
        return start(effectClass, parameters, originEntity == null ? null : originEntity.getLocation(), null, originEntity, null, null);
    }

    public Effect start(String effectClass, ConfigurationSection parameters, Location origin) {
        return start(effectClass, parameters, origin, null, null, null, null);
    }

    public Effect start(String effectClass, ConfigurationSection parameters, Location origin, Player targetPlayer){
        return start(effectClass, parameters, new DynamicLocation(origin, null), new DynamicLocation(null, null), (ConfigurationSection)null, targetPlayer);
    }

    /**
     * Start an Effect from a Configuration map of parameters.
     *
     * @param effectClass The name of the Effect class to instantiate. If unqualified, defaults to the de.slikey.effectlib.effect namespace.
     * @param parameters A Configuration-driven map of key/value parameters. Each of these will be applied directly to the corresponding field in the Effect instance.
     * @param origin The origin Location
     * @param target The target Location, only used in some Effects (like LineEffect)
     * @param originEntity The origin Entity, the effect will attach to the Entity's Location
     * @param targetEntity The target Entity, only used in some Effects
     * @param parameterMap A map of parameter values to replace. These must start with the "$" character, values in the parameters map that contain a $key will be replaced with the value in this parameterMap.
     * @return
     */
    @Deprecated
    public Effect start(String effectClass, ConfigurationSection parameters, Location origin, Location target, Entity originEntity, Entity targetEntity, Map<String, String> parameterMap) {
        return start(effectClass, parameters, new DynamicLocation(origin, originEntity), new DynamicLocation(target, targetEntity), parameterMap);
    }

    /**
     * Start an Effect from a Configuration map of parameters.
     *
     * @param effectClass The name of the Effect class to instantiate. If unqualified, defaults to the de.slikey.effectlib.effect namespace.
     * @param parameters A Configuration-driven map of key/value parameters. Each of these will be applied directly to the corresponding field in the Effect instance.
     * @param origin The origin Location
     * @param target The target Location, only used in some Effects (like LineEffect)
     * @param parameterMap A map of parameter values to replace. These must start with the "$" character, values in the parameters map that contain a $key will be replaced with the value in this parameterMap.
     * @return
     */
    @Deprecated
    public Effect start(String effectClass, ConfigurationSection parameters, DynamicLocation origin, DynamicLocation target, Map<String, String> parameterMap) {
        return start(effectClass, parameters, origin, target, parameterMap, null);
    }

    public Effect getEffectByClassName(String effectClass) {
        Class<? extends Effect> effectLibClass;
        try {
            // First check the name as given
            effectLibClass = effectClasses.get(effectClass);

            // A shaded manager may provide a fully-qualified path.
            if (effectLibClass == null && !effectClass.contains(".")) {
                effectClass = "de.slikey.effectlib.effect." + effectClass;
                if (!effectClass.endsWith("Effect")) {
                    effectClass = effectClass + "Effect";
                }
                effectLibClass = effectClasses.get(effectClass);
            }
            if (effectLibClass == null) {
                effectLibClass = (Class<? extends Effect>) Class.forName(effectClass);
                effectClasses.put(effectClass, effectLibClass);
            }
        } catch (Throwable ex) {
            onError("Error loading EffectLib class: " + effectClass, ex);
            return null;
        }

        Effect effect = null;
        try {
            Constructor constructor = effectLibClass.getConstructor(EffectManager.class);
            effect = (Effect) constructor.newInstance(this);
        } catch (Exception ex) {
            onError("Error loading EffectLib class: " + effectClass, ex);
        }

        return effect;
    }

    public Effect getEffect(String effectClass, ConfigurationSection parameters, DynamicLocation origin, DynamicLocation target, ConfigurationSection parameterMap, Player targetPlayer) {
        Effect effect = getEffectByClassName(effectClass);
        if (effect == null) {
            return null;
        }

        Collection<String> keys = parameters.getKeys(false);
        for (String key : keys) {
            if (key.equals("class")) {
                continue;
            }

            if (!setField(effect, key, parameters, parameterMap) && debug) {
                owningPlugin.getLogger().warning("Unable to assign EffectLib property " + key + " of class " + effect.getClass().getName());
            }
        }

        effect.setDynamicOrigin(origin);
        effect.setDynamicTarget(target);

        if (targetPlayer != null)
            effect.setTargetPlayer(targetPlayer);

        return effect;
    }

    @Deprecated
    public Effect start(String effectClass, ConfigurationSection parameters, DynamicLocation origin, DynamicLocation target, Map<String, String> parameterMap, Player targetPlayer) {
        ConfigurationSection configMap = null;
        if (parameterMap != null) {
            configMap = ConfigUtils.toStringConfiguration(parameterMap);
        }

        return start(effectClass, parameters, origin, target, configMap, targetPlayer);
    }

    /**
     * Start an effect, possibly using parameter replacement.
     *
     * @param effectClass the effect class to start
     * @param parameters any parameters to pass to the effect
     * @param origin the origin location
     * @param target the target location
     * @param parameterMap a configuration of variables from the parameter config to replace
     * @param targetPlayer The player who should see this effect.
     * @return
     */
    public Effect start(String effectClass, ConfigurationSection parameters, DynamicLocation origin, DynamicLocation target, ConfigurationSection parameterMap, Player targetPlayer) {
        Effect effect = getEffect(effectClass, parameters, origin, target, parameterMap, targetPlayer);
        if (effect == null) {
            return null;
        }
        effect.start();
        return effect;
    }
    
    public void cancel(boolean callback) {
        List<Effect> allEffects = new ArrayList<Effect>(effects.keySet());
        for (Effect effect : allEffects) {
            effect.cancel(callback);
        }
    }
    
    public void done(Effect effect) {
        synchronized (this) {
            BukkitTask existingTask = effects.get(effect);
            if (existingTask != null) {
                existingTask.cancel();
            }
            effects.remove(effect);
        }
        if (effect.callback != null && owningPlugin.isEnabled()) {
            Bukkit.getScheduler().runTask(owningPlugin, effect.callback);
        }
        if (disposeOnTermination && effects.isEmpty()) {
            dispose();
        }
    }
    
    @Override
    public void dispose() {
        if (disposed) {
            return;
        }
        disposed = true;
        cancel(false);
        if (effectManagers != null) {
            effectManagers.remove(this);
        }
    }
    
    public void disposeOnTermination() {
        disposeOnTermination = true;
        if (effects.isEmpty()) {
            dispose();
        }
    }

    public void enableDebug(boolean enable) {
        debug = enable;
    }

    public boolean isDebugEnabled() {
        return debug;
    }
    
    public void onError(Throwable ex) {
        if (debug) {
            owningPlugin.getLogger().log(Level.WARNING, "Particle Effect error", ex);
        }
    }

    public void onError(String message) {
        if (debug) {
            owningPlugin.getLogger().log(Level.WARNING, message);
        }
    }

    public void onError(String message, Throwable ex) {
        if (debug) {
            owningPlugin.getLogger().log(Level.WARNING, message, ex);
        }
    }

    public int getParticleRange() {
        return visibleRange;
    }
    
    public void setParticleRange(int range) {
        visibleRange = range;
    }

    public Plugin getOwningPlugin() {
        return owningPlugin;
    }

    protected boolean setField(Object effect, String key, ConfigurationSection section, ConfigurationSection parameterMap) {
        try {
            String stringValue = section.getString(key);
            String fieldKey = key;

            // Allow underscore_style and dash_style parameters
            if (key.contains("-")) {
                key = key.replace("-", "_");
            }
            if (key.contains("_")) {
                key = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, key);
            }

            ConfigurationSection fieldSection = section;
            if (parameterMap != null && stringValue.startsWith("$") && parameterMap.contains(stringValue)) {
                fieldKey = stringValue;
                fieldSection = parameterMap;
            }
            Field field = effect.getClass().getField(key);
            if (field.getType().equals(Integer.TYPE) || field.getType().equals(Integer.class)) {
                int intValue = Integer.MAX_VALUE;
                if (!ConfigUtils.isMaxValue(stringValue)) {
                    intValue = fieldSection.getInt(fieldKey);
                }
                field.set(effect, intValue);
            } else if (field.getType().equals(Float.TYPE) || field.getType().equals(Float.class)) {
                float floatValue = Float.MAX_VALUE;
                if (!ConfigUtils.isMaxValue(stringValue)) {
                    floatValue = (float)fieldSection.getDouble(fieldKey);
                }
                field.set(effect,floatValue);
            } else if (field.getType().equals(Double.TYPE) || field.getType().equals(Double.class)) {
                double doubleValue = Double.MAX_VALUE;
                if (!ConfigUtils.isMaxValue(stringValue)) {
                    doubleValue = fieldSection.getDouble(fieldKey);
                }
                field.set(effect, doubleValue);
            } else if (field.getType().equals(Boolean.TYPE) || field.getType().equals(Boolean.class)) {
                field.set(effect, fieldSection.getBoolean(fieldKey));
            } else if (field.getType().equals(Long.TYPE) || field.getType().equals(Long.class)) {
                long longValue = Long.MAX_VALUE;
                if (!ConfigUtils.isMaxValue(stringValue)) {
                    longValue = fieldSection.getLong(fieldKey);
                }
                field.set(effect, longValue);
            } else if (field.getType().equals(Short.TYPE) || field.getType().equals(Short.class)) {
                short shortValue = Short.MAX_VALUE;
                if (!ConfigUtils.isMaxValue(stringValue)) {
                    shortValue = (short)fieldSection.getInt(fieldKey);
                }
                field.set(effect, shortValue);
            } else if (field.getType().equals(Byte.TYPE) || field.getType().equals(Byte.class)) {
                byte byteValue = Byte.MAX_VALUE;
                if (!ConfigUtils.isMaxValue(stringValue)) {
                    byteValue = (byte)fieldSection.getInt(fieldKey);
                }
                field.set(effect, byteValue);
            } else if (field.getType().equals(String.class)) {
                String value = fieldSection.getString(fieldKey);
                field.set(effect, value);
            } else if (field.getType().equals(Color.class)) {
                try {
                    String value = fieldSection.getString(fieldKey);
                    Integer rgb = null;
                    if (value.equalsIgnoreCase("random")) {
                        byte red =  (byte)(Math.random() * 255);
                        byte green =  (byte)(Math.random() * 255);
                        byte blue  =  (byte)(Math.random() * 255);
                        rgb = (red << 16) | (green << 8) | blue;
                    } else {
                        rgb = Integer.parseInt(value, 16);
                    }
                    Color color = Color.fromRGB(rgb);
                    field.set(effect, color);
                } catch (Exception ex) {
                    onError(ex);
                }
            } else if (Map.class.isAssignableFrom(field.getType()) && section.isConfigurationSection(key)) {
                Map<String, Object> map = (Map<String, Object>)field.get(effect);
                ConfigurationSection subSection = section.getConfigurationSection(key);
                Set<String> keys = subSection.getKeys(false);
                for (String mapKey : keys) {
                    map.put(mapKey, subSection.get(mapKey));
                }
            } else if (Map.class.isAssignableFrom(field.getType()) && Map.class.isAssignableFrom(section.get(key).getClass())) {
                field.set(effect, section.get(key));
            } else if (ConfigurationSection.class.isAssignableFrom(field.getType())) {
                ConfigurationSection configSection = ConfigUtils.getConfigurationSection(section, key);
                if (parameterMap != null) {
                    ConfigurationSection baseConfiguration = configSection;
                    configSection = new MemoryConfiguration();
                    Set<String> keys = baseConfiguration.getKeys(false);
                    // Note this doesn't handle sections within sections.
                    for (String baseKey : keys) {
                        Object baseValue = baseConfiguration.get(baseKey);
                        if (baseValue instanceof String && ((String)baseValue).startsWith("$")) {
                            // If this is an equation it will get parsed when needed
                            String parameterValue = parameterMap.getString((String)baseValue);
                            baseValue = parameterValue == null ? baseValue : parameterValue;
                        }
                        configSection.set(baseKey, baseValue);
                    }
                }
                field.set(effect, configSection);
            } else if (field.getType().equals(Vector.class)) {
                double x = 0;
                double y = 0;
                double z = 0;
                try {
                    String value = fieldSection.getString(fieldKey);
                    String[] pieces = value.split(",");
                    x = pieces.length > 0 ? Double.parseDouble(pieces[0]) : 0;
                    y = pieces.length > 1 ? Double.parseDouble(pieces[1]) : 0;
                    z = pieces.length > 2 ? Double.parseDouble(pieces[2]) : 0;
                } catch (Exception ex) {
                    onError(ex);
                }
                field.set(effect, new Vector(x, y, z));
            } else if (field.getType().isEnum()) {
                Class<Enum> enumType = (Class<Enum>)field.getType();
                try {
                    String value = fieldSection.getString(fieldKey);
                    Enum enumValue = Enum.valueOf(enumType, value.toUpperCase());
                    field.set(effect, enumValue);
                } catch (Exception ex) {
                    onError(ex);
                }
            } else if (field.getType().equals(Font.class)) {
                try {
                    // Should caching the fonts be considered?
                    // Or is the performance gain negligible?
                    String value = fieldSection.getString(fieldKey);
                    Font font = Font.decode(value);
                    field.set(effect, font);
                } catch (Exception ex) {
                    onError(ex);
                }
            } else {
                return false;
            }

            return true;
        } catch (Exception ex) {
            this.onError(ex);
        }

        return false;
    }
    
    public static void initialize() {
        effectManagers = new ArrayList<EffectManager>();
    }
    
    public static List<EffectManager> getManagers() {
        if (effectManagers == null) {
            initialize();
        }
        return effectManagers;
    }
    
    public static void disposeAll() {
        if (effectManagers != null) {
            for (Iterator<EffectManager> i = effectManagers.iterator(); i.hasNext();) {
                EffectManager em = i.next();
                i.remove();
                em.dispose();
            }
        }
    }

    public void setImageCacheFolder(File folder) {
        imageCacheFolder = folder;
    }

    public File getImageCacheFolder() {
        return imageCacheFolder;
    }

    public void loadImage(final String fileName, final ImageLoadCallback callback) {
        BufferedImage[] images = imageCache.get(fileName);
        if (images != null) {
            callback.loaded(images);
            return;
        }

        owningPlugin.getServer().getScheduler().runTaskAsynchronously(owningPlugin, new ImageLoadTask(this, fileName, new ImageLoadCallback() {
            @Override
            public void loaded(final BufferedImage[] images) {
                 owningPlugin.getServer().getScheduler().runTask(owningPlugin, new Runnable() {
                     @Override
                     public void run() {
                         imageCache.put(fileName, images);
                         callback.loaded(images);
                     }
                 });
            }
        }));
    }

    public void registerEffectClass(String key, Class<? extends Effect> effectClass) {
        effectClasses.put(key, effectClass);
    }
}