/*
 * Copyright 2019 Aleksander Jagiełło <[email protected]>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package pl.craftserve.radiation;

import org.bukkit.ChatColor;
import org.bukkit.Color;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.MemoryConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.BrewEvent;
import org.bukkit.event.player.PlayerItemConsumeEvent;
import org.bukkit.inventory.BrewerInventory;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.PotionMeta;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.plugin.Plugin;
import org.bukkit.potion.PotionType;
import pl.craftserve.radiation.nms.RadiationNmsBridge;

import java.text.MessageFormat;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.Objects;
import java.util.Optional;
import java.util.StringJoiner;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;

public class LugolsIodinePotion implements Listener, Predicate<ItemStack> {
    private static final byte TRUE = 1;

    private final Plugin plugin;
    private final LugolsIodineEffect effect;
    private final Config config;

    private NamespacedKey potionKey;
    private NamespacedKey durationKey;
    private NamespacedKey durationSecondsKey;

    public LugolsIodinePotion(Plugin plugin, LugolsIodineEffect effect, Config config) {
        this.plugin = Objects.requireNonNull(plugin, "plugin");
        this.effect = Objects.requireNonNull(effect, "effect");
        this.config = Objects.requireNonNull(config, "config");
    }

    public void enable(RadiationNmsBridge nmsBridge) {
        Objects.requireNonNull(nmsBridge, "nmsBridge");

        this.potionKey = new NamespacedKey(this.plugin, "lugols_iodine");
        this.durationKey = new NamespacedKey(this.plugin, "duration");
        this.durationSecondsKey = new NamespacedKey(this.plugin, "duration_seconds");

        if (this.config.getRecipe().enabled) {
            nmsBridge.registerLugolsIodinePotion(this.potionKey, this.config);
        }
        this.plugin.getServer().getPluginManager().registerEvents(this, this.plugin);
    }

    public void disable(RadiationNmsBridge nmsBridge) {
        Objects.requireNonNull(nmsBridge, "nmsBridge");

        HandlerList.unregisterAll(this);
        if (this.config.getRecipe().enabled) {
            nmsBridge.unregisterLugolsIodinePotion(this.potionKey);
        }
    }

    public Duration getDuration() {
        return this.config.duration();
    }

    @Override
    public boolean test(ItemStack itemStack) {
        Objects.requireNonNull(itemStack, "itemStack");

        if (!itemStack.hasItemMeta()) {
            return false;
        }

        PersistentDataContainer container = itemStack.getItemMeta().getPersistentDataContainer();
        if (container.has(this.potionKey, PersistentDataType.BYTE)) {
            Byte value = container.get(this.potionKey, PersistentDataType.BYTE);
            return value != null && value == TRUE;
        }

        return false;
    }

    @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
    public void onPlayerItemConsume(PlayerItemConsumeEvent event) {
        ItemStack item = event.getItem();
        if (!this.test(item)) {
            return;
        }

        PersistentDataContainer container = item.getItemMeta().getPersistentDataContainer();

        int durationSeconds = 0;
        if (container.has(this.durationSecondsKey, PersistentDataType.INTEGER)) {
            durationSeconds = container.getOrDefault(this.durationSecondsKey, PersistentDataType.INTEGER, 0);
        } else if (container.has(this.durationKey, PersistentDataType.INTEGER)) {
            durationSeconds = (int) TimeUnit.MINUTES.toSeconds(container.getOrDefault(this.durationKey, PersistentDataType.INTEGER, 0)); // legacy
        }

        if (durationSeconds <= 0) {
            return;
        }

        Player player = event.getPlayer();
        this.effect.setEffect(player, durationSeconds);
        this.broadcastConsumption(player, durationSeconds);
    }

    private void broadcastConsumption(Player player, int durationSeconds) {
        Objects.requireNonNull(player, "player");
        this.plugin.getLogger().info(player.getName() + " has consumed " + this.config.name() + " with a duration of " + durationSeconds + " seconds");

        this.config.drinkMessage().ifPresent(rawMessage -> {
            String message = ChatColor.RED + MessageFormat.format(rawMessage, player.getDisplayName() + ChatColor.RESET, this.config.name());
            for (Player online : this.plugin.getServer().getOnlinePlayers()) {
                if (online.canSee(player)) {
                    online.sendMessage(message);
                }
            }
        });
    }

    @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
    public void onBrew(BrewEvent event) {
        if (!config.recipe.enabled) {
            return;
        }

        BrewerInventory inventory = event.getContents();
        BrewingStandWindow window = BrewingStandWindow.fromArray(inventory.getContents());

        if (!window.ingredient.getType().equals(config.recipe.ingredient)) {
            return;
        }

        boolean[] modified = new boolean[BrewingStandWindow.SLOTS];

        for (int i = 0; i < BrewingStandWindow.SLOTS; i++) {
            ItemStack result = window.results[i];
            if (result == null) {
                continue; // nothing in this slot
            }

            ItemMeta itemMeta = result.getItemMeta();
            if (!(itemMeta instanceof PotionMeta)) {
                continue;
            }

            PotionMeta potionMeta = (PotionMeta) itemMeta;
            if (potionMeta.getBasePotionData().getType().equals(config.recipe.basePotion)) {
                result.setItemMeta(this.convert(potionMeta));

                modified[i] = true;
            }
        }

        // delay this, because nms changes item stacks after BrewEvent is called
        this.plugin.getServer().getScheduler().runTask(this.plugin, () -> {
            for (int i = 0; i < BrewingStandWindow.SLOTS; i++) {
                if (modified[i]) {
                    ItemStack[] contents = inventory.getContents();
                    contents[i] = window.getResult(i);
                    inventory.setContents(contents);
                }
            }
        });
    }

    public PotionMeta convert(PotionMeta potionMeta) {
        Objects.requireNonNull(potionMeta, "potionMeta");

        Duration duration = this.config.duration();
        String formattedDuration = this.formatDuration(this.config.duration());

        if (this.config.color != null) {
            potionMeta.setColor(this.config.color);
        }
        potionMeta.addItemFlags(ItemFlag.HIDE_POTION_EFFECTS);
        potionMeta.setDisplayName(ChatColor.AQUA + this.config.name());
        potionMeta.setLore(Collections.singletonList(ChatColor.BLUE + MessageFormat.format(this.config.description(), formattedDuration)));

        PersistentDataContainer container = potionMeta.getPersistentDataContainer();
        container.set(this.potionKey, PersistentDataType.BYTE, TRUE);
        container.set(this.durationSecondsKey, PersistentDataType.INTEGER, (int) duration.getSeconds());
        return potionMeta;
    }

    private String formatDuration(Duration duration) {
        Objects.requireNonNull(duration, "duration");

        long seconds = duration.getSeconds();
        long minutes = TimeUnit.SECONDS.toMinutes(seconds);
        long secondsLeft = seconds - (TimeUnit.MINUTES.toSeconds(minutes));

        return (minutes < 10 ? "0" : "") +  minutes + ":" + (secondsLeft < 10 ? "0" : "") + secondsLeft;
    }

    /**
     * Class to simplify brewing stand window fields.
     * {@link BrewerInventory}
     */
    static class BrewingStandWindow {
        static final int SLOTS = 3;

        final ItemStack ingredient;
        final ItemStack fuel;
        final ItemStack[] results;

        BrewingStandWindow(ItemStack ingredient, ItemStack fuel, ItemStack[] results) {
            this.ingredient = ingredient;
            this.fuel = fuel;
            this.results = Objects.requireNonNull(results, "results");

            if (results.length != 3) {
                throw new IllegalArgumentException(results.length + " array length, expected 3");
            }
        }

        @Override
        public String toString() {
            return new StringJoiner(", ", BrewingStandWindow.class.getSimpleName() + "[", "]")
                    .add("ingredient=" + ingredient)
                    .add("fuel=" + fuel)
                    .add("results=" + Arrays.toString(results))
                    .toString();
        }

        static BrewingStandWindow fromArray(ItemStack[] contents) {
            if (contents.length != 5) {
                throw new IllegalArgumentException("length is " + contents.length + ", expected 5!");
            }

            ItemStack ingredient = Objects.requireNonNull(contents[3], "ingredient shouldn't be null, right?");;
            ItemStack fuel = contents[4];

            return new BrewingStandWindow(ingredient, fuel, Arrays.copyOfRange(contents, 0, 3));
        }

        public ItemStack getResult(int index) {
            return this.results[index];
        }
    }

    public Config getConfig() {
        return config;
    }

    //
    // Config
    //

    public static class Config {
        private final Recipe recipe;
        private final String name;
        private final Color color;
        private final String description;
        private final Duration duration;
        private final String drinkMessage;

        public Config(Recipe recipe, String name, Color color, String description, Duration duration, String drinkMessage) {
            this.recipe = Objects.requireNonNull(recipe, "recipe");
            this.name = Objects.requireNonNull(name, "name");
            this.color = color;
            this.description = Objects.requireNonNull(description, "description");
            this.duration = Objects.requireNonNull(duration, "duration");
            this.drinkMessage = Objects.requireNonNull(drinkMessage, "drinkMessage");
        }

        public Config(ConfigurationSection section) throws InvalidConfigurationException {
            if (section == null) {
                section = new MemoryConfiguration();
            }

            this.recipe = new Recipe(section.getConfigurationSection("recipe"));
            this.name = section.getString("name", "Lugol's Iodine");
            String colorHex = section.getString("color", null);
            try {
                this.color = colorHex == null ? null : Color.fromRGB(
                        Integer.parseInt(colorHex.substring(1, 3), 16),
                        Integer.parseInt(colorHex.substring(3, 5), 16),
                        Integer.parseInt(colorHex.substring(5, 7), 16)
                );
            } catch (NumberFormatException | StringIndexOutOfBoundsException exception) {
                throw new InvalidConfigurationException("Invalid potion color.", exception);
            }
            this.description = section.getString("description", "Radiation resistance ({0})");
            this.duration = Duration.ofSeconds(section.getInt("duration", 600));

            String drinkMessage = RadiationPlugin.colorize(section.getString("drink-message"));
            this.drinkMessage = drinkMessage != null && !drinkMessage.isEmpty() ? drinkMessage : null;

            if (this.duration.isZero() || this.duration.isNegative()) {
                throw new InvalidConfigurationException("Given potion duration must be positive.");
            }
        }

        public String name() {
            return this.name;
        }

        public String description() {
            return this.description;
        }

        public Duration duration() {
            return this.duration;
        }

        public Optional<String> drinkMessage() {
            return Optional.ofNullable(this.drinkMessage);
        }

        public Recipe getRecipe() {
            return recipe;
        }

        public static class Recipe {
            public static Material DEFAULT_INGREDIENT = Material.GHAST_TEAR;
            public static PotionType DEFAULT_BASE_POTION = PotionType.THICK;

            private final boolean enabled;
            private final PotionType basePotion;
            private final Material ingredient;

            public Recipe(boolean enabled, Material ingredient, PotionType basePotion) {
                this.enabled = enabled;
                this.ingredient = Objects.requireNonNull(ingredient, "ingredient");
                this.basePotion = Objects.requireNonNull(basePotion, "basePotion");
            }

            public Recipe(ConfigurationSection section) throws InvalidConfigurationException {
                if (section == null) {
                    section = new MemoryConfiguration();
                }

                this.enabled = section.getBoolean("enabled", true);
                if (this.enabled) {
                    this.ingredient = Material.matchMaterial(Objects.requireNonNull(section.getString("ingredient", DEFAULT_INGREDIENT.getKey().getKey())));
                    if (ingredient == null) {
                        throw new InvalidConfigurationException("Invalid recipe ingredient name");
                    }

                    try {
                        this.basePotion = PotionType.valueOf(Objects.requireNonNull(section.getString("base-potion", DEFAULT_BASE_POTION.name())).toUpperCase());
                    } catch (IllegalArgumentException exception) {
                        throw new InvalidConfigurationException("Invalid recipe base potion name", exception);
                    }
                } else {
                    this.ingredient = null;
                    this.basePotion = null;
                }
            }

            public boolean isEnabled() {
                return enabled;
            }

            public PotionType getBasePotion() {
                return basePotion;
            }

            public Material getIngredient() {
                return ingredient;
            }
        }
    }
}