package com.gmail.nossr50.util;

import com.gmail.nossr50.mcMMO;
import org.bukkit.NamespacedKey;
import org.bukkit.inventory.meta.PotionMeta;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.potion.PotionType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class PotionUtil {
    // Some of the old potion types got renamed, our configs can still contain these old names
    private static final Map<String, String> legacyPotionTypes = new HashMap<>();
    private static final Method methodPotionTypeGetKey;
    private static final Method methodPotionTypeGetEffectType;
    private static final Method methodPotionTypeGetPotionEffects;
    private static final Method methodPotionDataIsUpgraded;
    private static final Method methodPotionDataIsExtended;
    private static final Method methodPotionDataGetType;
    private static final Method methodPotionMetaGetBasePotionData;
    private static final Method methodPotionMetaSetBasePotionData;
    private static final Method methodPotionMetaGetBasePotionType;
    private static final Method methodPotionMetaSetBasePotionType;
    private static final Class<?> potionDataClass;

    public static final String STRONG = "STRONG";
    public static final String LONG = "LONG";
    public static final String WATER_POTION_TYPE_STR = "WATER";

    private static final PotionCompatibilityType COMPATIBILITY_MODE;

    static {
        potionDataClass = getPotionDataClass();
        // Uncraftable doesn't exist in modern versions
        // It served as a potion that didn't craft into anything else so it didn't conflict with vanilla systems
        // Instead we will use Mundane, which doesn't make anything in vanilla systems
        legacyPotionTypes.put("UNCRAFTABLE", "MUNDANE");
        legacyPotionTypes.put("JUMP", "LEAPING");
        legacyPotionTypes.put("SPEED", "SWIFTNESS");
        legacyPotionTypes.put("INSTANT_HEAL", "HEALING");
        legacyPotionTypes.put("INSTANT_DAMAGE", "HARMING");
        legacyPotionTypes.put("REGEN", "REGENERATION");
        methodPotionTypeGetKey = getKeyMethod();
        methodPotionDataIsUpgraded = getPotionDataIsUpgraded();
        methodPotionDataIsExtended = getPotionDataIsExtended();
        methodPotionMetaGetBasePotionData = getGetBasePotionDataMethod();
        methodPotionMetaGetBasePotionType = getGetBasePotionTypeMethod();
        methodPotionMetaSetBasePotionType = getMethodPotionMetaSetBasePotionType();
        methodPotionDataGetType = getPotionDataGetTypeMethod();
        methodPotionTypeGetEffectType = getPotionTypeEffectTypeMethod();
        methodPotionTypeGetPotionEffects = getPotionTypeGetPotionEffectsMethod();
        methodPotionMetaSetBasePotionData = setBasePotionData();

        if (potionDataClass != null
                && !mcMMO.getCompatibilityManager().getMinecraftGameVersion().isAtLeast(1, 20, 5)) {
            COMPATIBILITY_MODE = PotionCompatibilityType.PRE_1_20_5;
        } else {
            COMPATIBILITY_MODE = PotionCompatibilityType.MODERN;
        }
    }

    /**
     * Derive a potion from a partial name, and whether it should be upgraded or extended.
     * @param partialName potion type as a string, can be a substring of the potion type but must match exactly
     * @return The potion type
     */
    public static PotionType matchPotionType(String partialName, boolean isUpgraded, boolean isExtended) {
        // updatedName = convertUpgradedOrExtended(updatedName, isUpgraded, isExtended);
        if (COMPATIBILITY_MODE == PotionCompatibilityType.PRE_1_20_5) {
            return matchLegacyPotionType(partialName);
        } else {
            String updatedName = convertLegacyNames(partialName).toUpperCase();
            return Arrays.stream(PotionType.values())
                    .filter(potionType -> getKeyGetKey(potionType).toUpperCase().contains(updatedName))
                    .filter(potionType -> !isUpgraded || potionType.name().toUpperCase().contains(STRONG))
                    .filter(potionType -> !isExtended || potionType.name().toUpperCase().contains(LONG))
                    .findAny().orElse(null);
        }
    }

    /**
     * Legacy matching for {@link PotionType}
     *
     * @param name The partial name of the potion
     * @return The potion type
     */
    private static PotionType matchLegacyPotionType(String name) {
        return Arrays.stream(PotionType.values())
                .filter(potionType -> getKeyGetKey(potionType).equalsIgnoreCase(name)
                        || getKeyGetKey(potionType).equalsIgnoreCase(convertLegacyNames(name))
                        || potionType.name().equalsIgnoreCase(name)
                        || potionType.name().equalsIgnoreCase(convertLegacyNames(name)))
                .findAny().orElse(null);
    }

    private static String convertUpgradedOrExtended(String potionType, boolean isUpgraded, boolean isExtended) {
        if (isUpgraded) {
            potionType = STRONG + "_" + potionType;
        }
        if (isExtended) {
            potionType = LONG + "_" + potionType;
        }
        return potionType;
    }

    public static String getKeyGetKey(PotionType potionType) {
        try {
            if (getKeyMethod() != null) {
                NamespacedKey key = (NamespacedKey) methodPotionTypeGetKey.invoke(potionType);
                return key.getKey();
            } else {
                return potionType.name();
            }
        } catch (InvocationTargetException | IllegalAccessException e) {
            mcMMO.p.getLogger().warning("Failed to get potion key for " + potionType.name());
            return potionType.name();
        }
    }

    private static Class<?> getPotionDataClass() {
        try {
            return Class.forName("org.bukkit.potion.PotionData");
        } catch (ClassNotFoundException e) {
            return null;
        }
    }

    /**
     * Older versions of Spigot do not have getKey() in PotionType
     * We need to check for the existence of this method before calling it
     * @return The getKey method
     */
    private static @Nullable Method getKeyMethod() {
        try {
            return PotionType.class.getMethod("getKey");
        } catch (NoSuchMethodException e) {
            return null;
        }
    }

    private static @Nullable Method setBasePotionData() {
        try {
            return PotionMeta.class.getMethod("setBasePotionData", potionDataClass);
        } catch (NoSuchMethodException e) {
            return null;
        }
    }

    private static Method getMethodPotionMetaSetBasePotionType() {
        try {
            return PotionMeta.class.getMethod("setBasePotionType", PotionType.class);
        } catch (NoSuchMethodException e) {
            return null;
        }
    }

    private static Method getSetBasePotionData() {
        try {
            return PotionMeta.class.getMethod("setBasePotionData", potionDataClass);
        } catch (NoSuchMethodException e) {
            return null;
        }
    }

    private static @Nullable Method getPotionDataIsUpgraded() {
        try {
            // TODO: <?> Needed?
            final Class<?> clazz = Class.forName("org.bukkit.potion.PotionData");
            return clazz.getMethod("isUpgraded");
        } catch (NoSuchMethodException | ClassNotFoundException e) {
            return null;
        }
    }

    private static @Nullable Method getPotionDataIsExtended() {
        try {
            // TODO: <?> Needed?
            final Class<?> clazz = Class.forName("org.bukkit.potion.PotionData");
            return clazz.getMethod("isExtended");
        } catch (NoSuchMethodException | ClassNotFoundException e) {
            return null;
        }
    }

    /**
     * Newer versions of Spigot do not have getBasePotionData() in PotionMeta
     *
     * @return the getBasePotionData method, or null if it does not exist
     */
    private static @Nullable Method getGetBasePotionDataMethod() {
        try {
            return PotionMeta.class.getMethod("getBasePotionData");
        } catch (NoSuchMethodException e) {
            return null;
        }
    }

    private static Method getGetBasePotionTypeMethod() {
        try {
            return PotionMeta.class.getMethod("getBasePotionType");
        } catch (NoSuchMethodException e) {
            return null;
        }
    }

    private static Method getPotionDataGetTypeMethod() {
        try {
            final Class<?> clazz = Class.forName("org.bukkit.potion.PotionData");
            return clazz.getMethod("getType");
        } catch (NoSuchMethodException | ClassNotFoundException e) {
            return null;
        }
    }

    private static Method getPotionTypeEffectTypeMethod() {
        try {
            return PotionType.class.getMethod("getEffectType");
        } catch (NoSuchMethodException e) {
            return null;
        }
    }

    private static Method getPotionTypeGetPotionEffectsMethod() {
        try {
            return PotionType.class.getMethod("getPotionEffects");
        } catch (NoSuchMethodException e) {
            return null;
        }
    }

    public static String convertPotionConfigName(String legacyName) {
        String replacementName = legacyName;

        // Remove generated potions.yml config naming convention
        if (replacementName.contains("POTION_OF_")) {
            replacementName = replacementName.replace("POTION_OF_", "");
        }

        if (replacementName.contains("_II")) {
            replacementName = replacementName.replace("_II", "");
            replacementName = "STRONG_" + replacementName;
        } else if (replacementName.contains("_EXTENDED")) {
            replacementName = replacementName.replace("_EXTENDED", "");
            replacementName = "LONG_" + replacementName;
        }
        return replacementName;
    }

    public static String convertLegacyNames(String legacyPotionType) {
        String modernized = legacyPotionType;
        // check for legacy names
        for (var key : legacyPotionTypes.keySet()) {
            if (modernized.contains(key)) {
                // Replace the legacy name with the new name
                modernized = modernized.replace(key, legacyPotionTypes.get(key));
                break;
            }
        }
        return modernized;
    }

    public static boolean hasLegacyName(String potionType) {
        for (var key : legacyPotionTypes.keySet()) {
            if (potionType.contains(key)) {
                return true;
            }
        }
        return false;
    }

    public static boolean isStrong(PotionMeta potionMeta) {
        if (methodPotionMetaGetBasePotionData == null) {
            return isStrongModern(potionMeta);
        } else {
            return isStrongLegacy(potionMeta);
        }

    }

    public static boolean isLong(PotionMeta potionMeta) {
        if (methodPotionMetaGetBasePotionData == null) {
            return isLongModern(potionMeta);
        } else {
            return isLongLegacy(potionMeta);
        }
    }

    private static boolean isLongLegacy(PotionMeta potionMeta) {
        try {
            Object potionData = methodPotionMetaGetBasePotionData.invoke(potionMeta);
            return (boolean) methodPotionDataIsExtended.invoke(potionData);
        } catch (IllegalAccessException | InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }

    private static boolean isLongModern(PotionMeta potionMeta) {
        try {
            return getModernPotionTypeKey(potionMeta).getKey().startsWith(LONG);
        } catch (IllegalAccessException | InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }

    private static boolean isStrongLegacy(PotionMeta potionMeta) {
        try {
            Object potionData = methodPotionMetaGetBasePotionData.invoke(potionMeta);
            return (boolean) methodPotionDataIsUpgraded.invoke(potionData);
        } catch (IllegalAccessException | InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }

    private static boolean isStrongModern(PotionMeta potionMeta) {
        try {
            return getModernPotionTypeKey(potionMeta).getKey().startsWith(STRONG);
        } catch (IllegalAccessException | InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }

    private static NamespacedKey getModernPotionTypeKey(PotionMeta potionMeta) throws IllegalAccessException, InvocationTargetException {
        PotionType potionType = (PotionType) methodPotionMetaGetBasePotionType.invoke(potionMeta);
        return (NamespacedKey) methodPotionTypeGetKey.invoke(potionType);
    }

    public static boolean isPotionJustWater(PotionMeta potionMeta) {
        return isPotionTypeWater(potionMeta)
                && !hasBasePotionEffects(potionMeta)
                && potionMeta.getCustomEffects().isEmpty();
    }

    public static boolean isPotionTypeWater(@NotNull PotionMeta potionMeta) {
        if (COMPATIBILITY_MODE == PotionCompatibilityType.PRE_1_20_5) {
            return isPotionTypeWaterLegacy(potionMeta);
        } else {
            return isPotionTypeWaterModern(potionMeta);
        }
    }

    public static boolean isPotionType(@NotNull PotionMeta potionMeta, String potionType) {
        if (COMPATIBILITY_MODE == PotionCompatibilityType.PRE_1_20_5) {
            return isPotionTypeLegacy(potionMeta, potionType);
        } else {
            return isPotionTypeModern(potionMeta, potionType);
        }
    }

    public static boolean isPotionTypeWithoutEffects(@NotNull PotionMeta potionMeta, String potionType) {
        return isPotionType(potionMeta, potionType)
                && !hasBasePotionEffects(potionMeta)
                && potionMeta.getCustomEffects().isEmpty();
    }

    private static boolean isPotionTypeModern(@NotNull PotionMeta potionMeta, String potionType) {
        try {
            return getModernPotionTypeKey(potionMeta).getKey().equalsIgnoreCase(potionType);
        } catch (IllegalAccessException | InvocationTargetException ex) {
            throw new RuntimeException(ex);
        }
    }

    private static boolean isPotionTypeLegacy(@NotNull PotionMeta potionMeta, String potionType) {
        try {
            Object potionData = methodPotionMetaGetBasePotionData.invoke(potionMeta);
            PotionType potionTypeObj = (PotionType) methodPotionDataGetType.invoke(potionData);
            return potionTypeObj.name().equalsIgnoreCase(potionType);
        } catch (IllegalAccessException | InvocationTargetException ex) {
            throw new RuntimeException(ex);
        }
    }

    private static boolean isPotionTypeWaterLegacy(@NotNull PotionMeta potionMeta) {
        try {
            Object potionData = methodPotionMetaGetBasePotionData.invoke(potionMeta);
            PotionType potionType = (PotionType) methodPotionDataGetType.invoke(potionData);
            return potionType.name().equalsIgnoreCase(WATER_POTION_TYPE_STR)
                    || PotionType.valueOf(WATER_POTION_TYPE_STR) == potionType;
        } catch (InvocationTargetException | IllegalAccessException ex) {
            throw new RuntimeException(ex);
        }
    }

    private static boolean isPotionTypeWaterModern(@NotNull PotionMeta potionMeta) {
        try {
            return getModernPotionTypeKey(potionMeta).getKey().equalsIgnoreCase(WATER_POTION_TYPE_STR);
        } catch (IllegalAccessException | InvocationTargetException ex) {
            throw new RuntimeException(ex);
        }
    }

    public static boolean samePotionType(PotionMeta potionMeta, PotionMeta otherPotionMeta) {
        if (COMPATIBILITY_MODE == PotionCompatibilityType.PRE_1_20_5) {
            return samePotionTypeLegacy(potionMeta, otherPotionMeta);
        } else {
            return samePotionTypeModern(potionMeta, otherPotionMeta);
        }
    }

    private static boolean samePotionTypeLegacy(PotionMeta potionMeta, PotionMeta otherPotionMeta) {
        try {
            Object potionData = methodPotionMetaGetBasePotionData.invoke(potionMeta);
            Object otherPotionData = methodPotionMetaGetBasePotionData.invoke(otherPotionMeta);
            PotionType potionType = (PotionType) methodPotionDataGetType.invoke(potionData);
            PotionType otherPotionType = (PotionType) methodPotionDataGetType.invoke(otherPotionData);
            return potionType == otherPotionType;
        } catch (IllegalAccessException | InvocationTargetException ex) {
            throw new RuntimeException(ex);
        }
    }

    private static boolean samePotionTypeModern(PotionMeta potionMeta, PotionMeta otherPotionMeta) {
        try {
            PotionType potionType = (PotionType) methodPotionMetaGetBasePotionType.invoke(potionMeta);
            PotionType otherPotionType = (PotionType) methodPotionMetaGetBasePotionType.invoke(otherPotionMeta);
            return potionType == otherPotionType;
        } catch (IllegalAccessException | InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }

    public static boolean samePotionEffects(PotionMeta potionMeta, PotionMeta otherPotionMeta) {
        if (COMPATIBILITY_MODE == PotionCompatibilityType.PRE_1_20_5) {
            return true;
        } else {
            return samePotionEffectsModern(potionMeta, otherPotionMeta);
        }
    }

    private static boolean samePotionEffectsModern(PotionMeta potionMeta, PotionMeta otherPotionMeta) {
        return potionMeta.getCustomEffects().equals(otherPotionMeta.getCustomEffects());
    }

    public static boolean hasBasePotionEffects(PotionMeta potionMeta) {
        if (COMPATIBILITY_MODE == PotionCompatibilityType.PRE_1_20_5) {
            return hasBasePotionEffectsLegacy(potionMeta);
        } else {
            return hasBasePotionEffectsModern(potionMeta);
        }
    }

    private static boolean hasBasePotionEffectsLegacy(PotionMeta potionMeta) {
        try {
            Object potionData = methodPotionMetaGetBasePotionData.invoke(potionMeta);
            PotionType potionType = (PotionType) methodPotionDataGetType.invoke(potionData);
            return methodPotionTypeGetEffectType.invoke(potionType) != null;
        } catch (IllegalAccessException | InvocationTargetException ex) {
            throw new RuntimeException(ex);
        }
    }

    private static boolean hasBasePotionEffectsModern(PotionMeta potionMeta) {
        try {
            PotionType potionType = (PotionType) methodPotionMetaGetBasePotionType.invoke(potionMeta);
            List<PotionEffectType> potionEffectTypeList = (List<PotionEffectType>) methodPotionTypeGetPotionEffects.invoke(potionType);
            return potionEffectTypeList != null && !potionEffectTypeList.isEmpty();
        } catch (IllegalAccessException | InvocationTargetException ex) {
            throw new RuntimeException(ex);
        }
    }

    /**
     * Set the base potion type of a potion meta.
     * Note that extended/upgraded are ignored in 1.20.5 and later.
     *
     * @param potionMeta the potion meta
     * @param extended true if the potion is extended
     * @param upgraded true if the potion is upgraded
     */
    public static void setBasePotionType(PotionMeta potionMeta, PotionType potionType, boolean extended, boolean upgraded) {
        if (methodPotionMetaSetBasePotionType == null) {
            setBasePotionTypeLegacy(potionMeta, potionType, extended, upgraded);
        } else {
            setBasePotionTypeModern(potionMeta, potionType);
        }
    }

    public static void setUpgradedAndExtendedProperties(PotionType potionType, PotionMeta potionMeta,
                                                        boolean isUpgraded, boolean isExtended) {
        if (potionDataClass == null || mcMMO.getCompatibilityManager().getMinecraftGameVersion().isAtLeast(1, 20, 5)) {
            return;
        }

        try {
            final Object potionData = potionDataClass.getConstructor(PotionType.class, boolean.class, boolean.class)
                    .newInstance(potionType, isExtended, isUpgraded);
            methodPotionMetaSetBasePotionData.invoke(potionMeta, potionData);
        } catch (IllegalAccessException | InvocationTargetException | InstantiationException
                 | NoSuchMethodException ex) {
            throw new RuntimeException(ex);
        }
    }

    private static void setBasePotionTypeLegacy(PotionMeta potionMeta, PotionType potionType, boolean extended,
                                                boolean upgraded) {
        try {
            Object potionData = potionDataClass.getConstructor(PotionType.class, boolean.class, boolean.class)
                    .newInstance(potionType, extended, upgraded);
            methodPotionMetaSetBasePotionData.invoke(potionMeta, potionData);
        } catch (IllegalAccessException | InvocationTargetException | InstantiationException | NoSuchMethodException ex) {
            throw new RuntimeException(ex);
        }
    }

    private static void setBasePotionTypeModern(PotionMeta potionMeta, PotionType potionType) {
        try {
            methodPotionMetaSetBasePotionType.invoke(potionMeta, potionType);
        } catch (IllegalAccessException | InvocationTargetException ex) {
            throw new RuntimeException(ex);
        }
    }

    public static boolean isPotionDataEqual(PotionMeta potionMeta, PotionMeta otherPotionMeta) {
        if (COMPATIBILITY_MODE == PotionCompatibilityType.MODERN) {
            return true; // we don't compare data on newer versions
        } else {
            try {
                final Object potionData = methodPotionMetaGetBasePotionData.invoke(potionMeta);
                final Object otherPotionData = methodPotionMetaGetBasePotionData.invoke(otherPotionMeta);
                final PotionType potionType = (PotionType) methodPotionDataGetType.invoke(potionData);
                final PotionType otherPotionType = (PotionType) methodPotionDataGetType.invoke(otherPotionData);
                if (potionType != otherPotionType) {
                    return false;
                }
                if (methodPotionDataIsExtended.invoke(potionData) != methodPotionDataIsExtended.invoke(otherPotionData)) {
                    return false;
                }
                if (methodPotionDataIsUpgraded.invoke(potionData) != methodPotionDataIsUpgraded.invoke(otherPotionData)) {
                    return false;
                }
                return true;
            } catch (IllegalAccessException | InvocationTargetException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
