package net.knarcraft.blacksmith.trait;

import net.citizensnpcs.api.npc.NPC;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.config.NPCSettings;
import net.knarcraft.blacksmith.manager.EconomyManager;
import net.knarcraft.blacksmith.util.InputParsingHelper;
import net.knarcraft.blacksmith.util.ItemHelper;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.Damageable;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.scheduler.BukkitScheduler;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.logging.Level;

import static net.knarcraft.blacksmith.formatting.StringFormatter.sendNPCMessage;

/**
 * A representation of the session between a player and a blacksmith
 */
public class ReforgeSession implements Runnable {

    private final BlacksmithTrait blacksmithTrait;
    private final Player player;
    private final NPC npc;
    private final ItemStack itemToReforge;
    private int taskId;
    private long finishTime = 0;
    private final NPCSettings config;
    private static final String[] enchantments = new String[Enchantment.values().length];
    private static final Random random = new Random();

    /**
     * Instantiates a new session
     *
     * @param blacksmithTrait <p>A reference to the blacksmith trait</p>
     * @param player          <p>The player initiating the session</p>
     * @param npc             <p>The Blacksmith NPC involved in the session</p>
     * @param config          <p>The config to use for the session</p>
     */
    ReforgeSession(BlacksmithTrait blacksmithTrait, Player player, NPC npc, NPCSettings config) {
        this.blacksmithTrait = blacksmithTrait;
        this.player = player;
        this.npc = npc;
        this.itemToReforge = player.getInventory().getItemInMainHand();
        this.config = config;

        //Populate enchantments the first time this is run
        if (enchantments[0] == null) {
            int i = 0;
            for (Enchantment enchantment : Enchantment.values()) {
                enchantments[i++] = enchantment.getKey().getKey();
            }
        }
    }

    /**
     * Gets the time in milliseconds when this reforging session will finish
     *
     * @return <p>The time the reforging will finish</p>
     */
    public long getFinishTime() {
        return this.finishTime;
    }

    /**
     * Runs the actual reforge which fixes the item and gives it back to the player
     */
    @Override
    public void run() {
        sendNPCMessage(this.npc, player, reforgeItem() ? config.getSuccessMessage() : config.getFailMessage());

        //Stop the reforged item from displaying in the blacksmith's hand
        if (npc.getEntity() instanceof Player) {
            ((Player) npc.getEntity()).getInventory().setItemInMainHand(null);
        } else {
            Objects.requireNonNull(((LivingEntity) npc.getEntity()).getEquipment()).setItemInMainHand(null);
        }

        //Give the item back to the player
        if (!config.getDisableDelay()) {
            //If the player isn't online, or the player cannot fit the item, drop the item to prevent it from disappearing
            if (config.getDropItem() || !player.isOnline() || player.getInventory().firstEmpty() == -1) {
                player.getWorld().dropItemNaturally(npc.getEntity().getLocation(), itemToReforge);
            } else {
                player.getInventory().addItem(itemToReforge);
            }
        } else {
            //It can be assumed as this happens instantly, that the player still has the item's previous slot selected
            player.getInventory().setItemInMainHand(itemToReforge);
        }

        //Mark this blacksmith as available
        blacksmithTrait.unsetSession();

        // Start cool-down
        Calendar wait = Calendar.getInstance();
        wait.add(Calendar.SECOND, config.getReforgeCoolDown());
        blacksmithTrait.addCoolDown(player.getUniqueId(), wait);
    }

    /**
     * Performs the actual reforge where the item's damage is reduced
     *
     * @return <p>Whether the reforge was successful. False if the blacksmith failed to fully repair the item.</p>
     */
    private boolean reforgeItem() {
        if (random.nextInt(100) < config.getFailChance()) {
            failReforge();
            return false;
        } else {
            succeedReforge();
            return true;
        }
    }

    /**
     * The method to run when a blacksmith successfully reforges an item
     */
    private void succeedReforge() {
        // Remove any damage done to the item
        updateDamage(itemToReforge, 0);

        // Add random enchantments
        int roll = random.nextInt(100);
        if (!(roll < config.getExtraEnchantmentChance() &&
                itemToReforge.getEnchantments().keySet().size() < config.getMaxEnchantments())) {
            // Abort if randomness isn't on our side, or if max enchantments has been reached
            return;
        }

        //Find usable enchantments first
        List<Enchantment> usableEnchantments = new ArrayList<>();
        for (String enchantmentName : enchantments) {
            Enchantment enchantment = InputParsingHelper.matchEnchantment(enchantmentName);
            if (enchantment != null && enchantment.canEnchantItem(itemToReforge)) {
                usableEnchantments.add(enchantment);
            }
        }

        //Choose a random enchantment
        Enchantment randomEnchantment = usableEnchantments.get(random.nextInt(usableEnchantments.size()));
        if (randomEnchantment != null) {
            int randomBound = randomEnchantment.getMaxLevel() + 1;
            //A workaround for the random method's bound sometimes being negative
            if (randomBound >= 0) {
                int existingLevel = itemToReforge.getEnchantmentLevel(randomEnchantment);
                /* Add a random enchantment whose level is no lower than the start level, and no lower than the
                existing level (to prevent making the item worse) */
                itemToReforge.addEnchantment(randomEnchantment, Math.max(Math.max(random.nextInt(randomBound),
                        randomEnchantment.getStartLevel()), existingLevel));
            }
        }
    }

    /**
     * The method to run when a blacksmith fails re-forging an item
     */
    private void failReforge() {
        // Remove or downgrade existing enchantments
        for (Enchantment enchantment : itemToReforge.getEnchantments().keySet()) {
            if (random.nextBoolean()) {
                itemToReforge.removeEnchantment(enchantment);
            } else {
                if (itemToReforge.getEnchantmentLevel(enchantment) > 1) {
                    itemToReforge.removeEnchantment(enchantment);
                    itemToReforge.addEnchantment(enchantment, 1);
                }
            }
        }

        // Damage the item
        short currentItemDurability = ItemHelper.getDurability(itemToReforge);
        short newDurability = (short) (currentItemDurability + (currentItemDurability * random.nextInt(8)));
        short maxDurability = itemToReforge.getType().getMaxDurability();
        if (newDurability <= 0) {
            newDurability = (short) (maxDurability / 3);
        } else if (currentItemDurability + newDurability > maxDurability) {
            newDurability = (short) (maxDurability - random.nextInt(maxDurability - 25));
        }
        updateDamage(itemToReforge, maxDurability - newDurability);
    }

    /**
     * Updates the damage done to an item
     *
     * @param item      <p>The item to update damage for</p>
     * @param newDamage <p>The new damage done</p>
     */
    private void updateDamage(ItemStack item, int newDamage) {
        ItemMeta meta = item.getItemMeta();
        Damageable damageable = (Damageable) meta;
        if (damageable != null) {
            damageable.setDamage(newDamage);
        } else {
            BlacksmithPlugin.getInstance().getLogger().log(Level.WARNING, "Unable to change damage of " + item);
        }
        item.setItemMeta(meta);
    }

    /**
     * Gets whether to end the current session
     *
     * <p>If the player has switched their item, or the player cannot pay, this returns true.</p>
     *
     * @return <p>True if the current session should end</p>
     */
    public boolean endSession() {
        // Prevent player from switching items during session
        ItemStack itemInHand = player.getInventory().getItemInMainHand();
        if (!itemToReforge.equals(itemInHand)) {
            sendNPCMessage(this.npc, player, config.getItemChangedMessage());
            return true;
        }
        // The player is unable to pay
        if (!EconomyManager.canPay(player)) {
            sendNPCMessage(this.npc, player, config.getInsufficientFundsMessage());
            return true;
        }
        return false;
    }

    /**
     * Gets whether the current session is still running
     *
     * @return <p>True if the current session is still running</p>
     */
    public boolean isRunning() {
        return BlacksmithPlugin.getInstance().getServer().getScheduler().isQueued(taskId);
    }

    /**
     * Gets whether the given player is currently in a reforging session
     *
     * @param other <p>The player to check if is in session</p>
     * @return <p>True if the given player is in a reforge session</p>
     */
    public boolean isInSession(Player other) {
        return player.getName().equals(other.getName());
    }

    /**
     * Begins the actual item reforging
     */
    public void beginReforge() {
        BukkitScheduler scheduler = BlacksmithPlugin.getInstance().getServer().getScheduler();
        int reforgeDelay;
        if (!config.getDisableCoolDown()) {
            //Finish the reforging after a random delay between the max and min
            reforgeDelay = new Random().nextInt(config.getMaxReforgeDelay()) + config.getMinReforgeDelay();
        } else {
            //Finish the reforging as soon as possible
            reforgeDelay = 0;
        }
        this.finishTime = System.currentTimeMillis() + (reforgeDelay * 1000L);
        taskId = scheduler.scheduleSyncDelayedTask(BlacksmithPlugin.getInstance(), this, reforgeDelay * 20L);
    }

    /**
     * Gets the player currently in this reforge session
     *
     * @return <p>The player currently in this reforge session</p>
     */
    public Player getPlayer() {
        return player;
    }

}