package net.knarcraft.blacksmith.config;

import net.citizensnpcs.api.util.DataKey;
import net.citizensnpcs.api.util.YamlStorage;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.enchantments.Enchantment;

import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;

/**
 * A class which keeps track of all default NPC settings and all global settings
 */
public class GlobalSettings {

    private final Map<Material, Double> materialBasePrices = new HashMap<>();
    private final Map<Material, Double> materialPricePerDurabilityPoints = new HashMap<>();
    private final Map<Enchantment, Double> enchantmentCosts = new HashMap<>();

    private final Map<NPCSetting, Object> defaultNPCSettings = new HashMap<>();
    private final Map<GlobalSetting, Object> globalSettings = new HashMap<>();

    private final YamlStorage defaultConfig;

    /**
     * Instantiates a new "Settings"
     *
     * @param plugin <p>A reference to the blacksmith plugin</p>
     */
    public GlobalSettings(BlacksmithPlugin plugin) {
        defaultConfig = new YamlStorage(new File(plugin.getDataFolder() + File.separator + "config.yml"),
                "Blacksmith Configuration\nWarning: The values under defaults are the values set for a blacksmith" +
                        "upon creation. To change any values for existing NPCs, edit the citizens NPC file.");
    }

    /**
     * Loads all configuration values from the config file
     */
    public void load() {
        //Load the config from disk
        defaultConfig.load();
        DataKey root = defaultConfig.getKey("");

        //Just in case, clear existing values
        defaultNPCSettings.clear();
        globalSettings.clear();
        materialBasePrices.clear();
        materialPricePerDurabilityPoints.clear();
        enchantmentCosts.clear();

        //Load/Save NPC default settings
        loadDefaultNPCSettings(root);

        //Load/Save global settings
        loadGlobalSettings(root);

        //Save any modified values to disk
        defaultConfig.save();
    }

    /**
     * Gets the current value of the default NPC settings
     *
     * @return <p>The current value of the default NPC settings</p>
     */
    public Map<NPCSetting, Object> getDefaultNPCSettings() {
        return new HashMap<>(this.defaultNPCSettings);
    }

    /**
     * Gets whether to use natural cost for cost calculation
     *
     * <p>Natural cost makes it more costly the more damage is dealt to an item. The alternative is the legacy behavior
     * where the amount of durability points remaining increases the cost.</p>
     *
     * @return <p>Whether to use natural cost</p>
     */
    public boolean getUseNaturalCost() {
        return asBoolean(GlobalSetting.NATURAL_COST);
    }

    /**
     * Gets the base price for the given material
     *
     * @param material <p>The material to get the base price for</p>
     * @return <p>The base price for the material</p>
     */
    public double getBasePrice(Material material) {
        if (materialBasePrices.containsKey(material) && materialBasePrices.get(material) != null) {
            return materialBasePrices.get(material);
        } else {
            return asDouble(GlobalSetting.BASE_PRICE);
        }
    }

    /**
     * Gets the price per durability point for the given material
     *
     * @param material <p>The material to get the durability point price for</p>
     * @return <p>The durability point price for the material</p>
     */
    public double getPricePerDurabilityPoint(Material material) {
        if (materialPricePerDurabilityPoints.containsKey(material) &&
                materialPricePerDurabilityPoints.get(material) != null) {
            return materialPricePerDurabilityPoints.get(material);
        } else {
            return asDouble(GlobalSetting.PRICE_PER_DURABILITY_POINT);
        }
    }

    /**
     * Gets the cost to be added for each level of the given enchantment
     *
     * @param enchantment <p>The enchantment to get the cost for</p>
     * @return <p>The cost of each enchantment level</p>
     */
    public double getEnchantmentCost(Enchantment enchantment) {
        if (enchantmentCosts.containsKey(enchantment) && enchantmentCosts.get(enchantment) != null) {
            return enchantmentCosts.get(enchantment);
        } else {
            return asDouble(GlobalSetting.ENCHANTMENT_COST);
        }
    }

    /**
     * Gets the given value as a boolean
     *
     * <p>This will throw an exception if used for a non-boolean value</p>
     *
     * @param setting <p>The setting to get the value of</p>
     * @return <p>The value of the given setting as a boolean</p>
     */
    public boolean asBoolean(GlobalSetting setting) {
        Object value = getValue(setting);
        if (value instanceof String) {
            return Boolean.parseBoolean((String) value);
        } else {
            return (Boolean) value;
        }
    }

    /**
     * Gets the given value as a double
     *
     * <p>This will throw an exception if used for a non-double setting</p>
     *
     * @param setting <p>The setting to get the value of</p>
     * @return <p>The value of the given setting as a double</p>
     */
    public double asDouble(GlobalSetting setting) {
        Object value = getValue(setting);
        if (value instanceof String) {
            return Double.parseDouble((String) value);
        } else if (value instanceof Integer) {
            return (Integer) value;
        } else {
            return (Double) value;
        }
    }

    /**
     * Gets the value of a setting, using the default if not set
     *
     * @param setting <p>The setting to get the value of</p>
     * @return <p>The current value</p>
     */
    private Object getValue(GlobalSetting setting) {
        Object value = globalSettings.get(setting);
        //If not set in config.yml, use the default value from the enum
        if (value == null) {
            value = setting.getDefaultValue();
        }
        return value;
    }

    /**
     * Loads all global settings
     *
     * @param root <p>The root node of all global settings</p>
     */
    private void loadGlobalSettings(DataKey root) {
        for (GlobalSetting globalSetting : GlobalSetting.values()) {
            if (!root.keyExists(globalSetting.getPath())) {
                //If the setting does not exist in the config file, add it
                root.setRaw(globalSetting.getPath(), globalSetting.getDefaultValue());
            } else {
                //Set the setting to the value found in the path
                globalSettings.put(globalSetting, root.getRaw(globalSetting.getPath()));
            }
        }

        //Load all base prices
        DataKey basePriceNode = root.getRelative(GlobalSetting.BASE_PRICE.getParent());
        Map<String, String> relevantKeys = getRelevantKeys(basePriceNode);
        for (String key : relevantKeys.keySet()) {
            String materialName = relevantKeys.get(key);
            Material material = Material.matchMaterial(materialName);
            if (material != null) {
                materialBasePrices.put(material, basePriceNode.getDouble(key));
            } else {
                BlacksmithPlugin.getInstance().getLogger().log(Level.WARNING,
                        "Unable to find a material matching " + materialName);
            }
        }

        //Load all per-durability-point prices
        DataKey basePerDurabilityPriceNode = root.getRelative(GlobalSetting.PRICE_PER_DURABILITY_POINT.getParent());
        relevantKeys = getRelevantKeys(basePerDurabilityPriceNode);
        for (String key : relevantKeys.keySet()) {
            String materialName = relevantKeys.get(key);
            Material material = Material.matchMaterial(materialName);
            if (material != null) {
                materialPricePerDurabilityPoints.put(material, basePerDurabilityPriceNode.getDouble(key));
            } else {
                BlacksmithPlugin.getInstance().getLogger().log(Level.WARNING,
                        "Unable to find a material matching " + materialName);
            }
        }

        //Load all enchantment prices
        DataKey enchantmentCostNode = root.getRelative(GlobalSetting.ENCHANTMENT_COST.getParent());
        relevantKeys = getRelevantKeys(basePerDurabilityPriceNode);
        for (String key : relevantKeys.keySet()) {
            String enchantmentName = relevantKeys.get(key);
            Enchantment enchantment = Enchantment.getByKey(NamespacedKey.minecraft(enchantmentName));
            if (enchantment != null) {
                enchantmentCosts.put(enchantment, enchantmentCostNode.getDouble(key));
            } else {
                BlacksmithPlugin.getInstance().getLogger().log(Level.WARNING,
                        "Unable to find an enchantment matching " + enchantmentName);
            }
        }
    }

    /**
     * Gets a map between relevant keys and their normalized name
     *
     * @param rootKey <p>The root data key containing sub-keys</p>
     * @return <p>Any sub-keys found that aren't the default</p>
     */
    private Map<String, String> getRelevantKeys(DataKey rootKey) {
        Map<String, String> relevant = new HashMap<>();
        for (DataKey dataKey : rootKey.getSubKeys()) {
            String keyName = dataKey.name();
            //Skip the default value
            if (keyName.equals("default")) {
                continue;
            }
            String normalizedName = keyName.toUpperCase().replace("-", "_");
            relevant.put(keyName, normalizedName);
        }
        return relevant;
    }

    /**
     * Loads all default NPC settings
     *
     * @param root <p>The root node of all default NPC settings</p>
     */
    private void loadDefaultNPCSettings(DataKey root) {
        for (NPCSetting setting : NPCSetting.values()) {
            if (!root.keyExists(setting.getPath())) {
                //If the setting does not exist in the config file, add it
                root.setRaw(setting.getPath(), setting.getDefaultValue());
            } else {
                //Set the setting to the value found in the path
                defaultNPCSettings.put(setting, root.getRaw(setting.getPath()));
            }
        }
    }

}