package de.srendi.advancedperipherals.common.util;

import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.shared.util.NBTUtil;
import de.srendi.advancedperipherals.AdvancedPeripherals;
import de.srendi.advancedperipherals.common.addons.APAddon;
import de.srendi.advancedperipherals.common.addons.computercraft.peripheral.InventoryManagerPeripheral;
import de.srendi.advancedperipherals.common.util.inventory.ChemicalUtil;
import de.srendi.advancedperipherals.common.util.inventory.FluidUtil;
import de.srendi.advancedperipherals.common.util.inventory.ItemUtil;
import mekanism.api.MekanismAPI;
import mekanism.api.chemical.Chemical;
import mekanism.api.chemical.ChemicalStack;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.resources.ResourceKey;
import net.minecraft.tags.TagKey;
import net.minecraft.util.StringRepresentable;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.animal.Animal;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.properties.Property;
import net.minecraft.world.level.material.Fluid;
import net.neoforged.neoforge.common.IShearable;
import net.neoforged.neoforge.fluids.FluidStack;
import net.neoforged.neoforge.fluids.FluidType;
import org.jetbrains.annotations.NotNull;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

public class LuaConverter {

    public static Map<String, Object> entityToLua(Entity entity) {
        Map<String, Object> data = new HashMap<>();
        data.put("id", entity.getId());
        data.put("uuid", entity.getStringUUID());
        data.put("name", entity.getName().getString());
        data.put("tags", entity.getTags());
        data.put("canFreeze", entity.canFreeze());
        data.put("isGlowing", entity.isCurrentlyGlowing());
        data.put("isInWall", entity.isInWall());
        return data;
    }

    public static Map<String, Object> livingEntityToLua(LivingEntity entity) {
        Map<String, Object> data = entityToLua(entity);
        data.put("health", entity.getHealth());
        data.put("maxHealth", entity.getMaxHealth());
        data.put("lastDamageSource", entity.getLastDamageSource() == null ? null : entity.getLastDamageSource().toString());
        return data;
    }

    public static Map<String, Object> animalToLua(Animal animal, ItemStack itemInHand) {
        Map<String, Object> data = livingEntityToLua(animal);
        data.put("baby", animal.isBaby());
        data.put("inLove", animal.isInLove());
        data.put("aggressive", animal.isAggressive());
        if (animal instanceof IShearable shareable && !itemInHand.isEmpty()) {
            data.put("shareable", shareable.isShearable(null, itemInHand, animal.level(), animal.blockPosition()));
        }
        return data;
    }

    public static Map<String, Object> completeEntityToLua(Entity entity, ItemStack itemInHand) {
        if (entity instanceof Animal animal) return animalToLua(animal, itemInHand);
        if (entity instanceof LivingEntity livingEntity) return livingEntityToLua(livingEntity);
        return entityToLua(entity);
    }

    public static Map<String, Object> completeEntityWithPositionToLua(Entity entity, ItemStack itemInHand, BlockPos pos) {
        Map<String, Object> data = completeEntityToLua(entity, itemInHand);
        data.put("x", entity.getX() - pos.getX());
        data.put("y", entity.getY() - pos.getY());
        data.put("z", entity.getZ() - pos.getZ());
        return data;
    }

    /**
     * Block states to a lua representable object
     *
     * @param blockStateValue block state see {@link net.minecraft.world.level.block.state.BlockState#getValue(Property)}
     * @return the state cast to a lua representable object
     */
    public static Object stateToObject(Comparable<?> blockStateValue) {
        if (blockStateValue == null) {
            return null;
        } else if (blockStateValue instanceof Boolean || blockStateValue instanceof Number || blockStateValue instanceof String) {
            // Just return the value since lua can represent them just fine
            return blockStateValue;
        } else if (blockStateValue instanceof StringRepresentable stringRepresentable) {
            return stringRepresentable.getSerializedName();
        } else {
            return null;
        }
    }

    public static Object posToObject(BlockPos pos) {
        if (pos == null) {
            return null;
        }

        Map<String, Object> properties = new HashMap<>(3);
        properties.put("x", pos.getX());
        properties.put("y", pos.getY());
        properties.put("z", pos.getZ());
        return properties;
    }

    public static Map<String, Object> itemStackToObject(@NotNull ItemStack stack) {
        if (stack.isEmpty()) {
            return null;
        }
        Map<String, Object> properties = itemToObject(stack.getItem());
        DataComponentPatch components = stack.getComponentsPatch();
        properties.put("count", stack.getCount());
        properties.put("displayName", stack.getDisplayName().getString());
        properties.put("maxStackSize", stack.getMaxStackSize());
        try {
            properties.put("components", NBTUtil.toLua(DataComponentUtil.toNbt(components)));
        } catch (IllegalStateException ex) {
            AdvancedPeripherals.debug("Couldn't create components for Item Stack " + stack, ex);
        }
        properties.put("fingerprint", ItemUtil.getFingerprint(stack));
        return properties;
    }

    public static Map<String, Object> itemStackToObject(@NotNull ItemStack stack, Level level) {
        if (stack.isEmpty()) {
            return null;
        }
        Map<String, Object> properties = itemToObject(stack.getItem());
        DataComponentPatch components = stack.getComponentsPatch();
        properties.put("count", stack.getCount());
        properties.put("displayName", stack.getDisplayName().getString());
        properties.put("maxStackSize", stack.getMaxStackSize());
        try {
            properties.put("components", NBTUtil.toLua(DataComponentUtil.toNbt(components, level)));
        } catch (IllegalStateException ex) {
            AdvancedPeripherals.debug("Couldn't create components for Item Stack " + stack, ex);
        }
        properties.put("fingerprint", ItemUtil.getFingerprint(stack));
        return properties;
    }


    public static Map<String, Object> fluidStackToObject(@NotNull FluidStack stack) {
        if (stack.isEmpty()) {
            return null;
        }
        Map<String, Object> properties = fluidToObject(stack.getFluid());
        DataComponentPatch components = stack.getComponentsPatch();
        properties.put("count", stack.getAmount());
        properties.put("displayName", stack.getHoverName().getString());
        properties.put("fluidType", fluidTypeToObject(stack.getFluidType()));
        properties.put("components", NBTUtil.toLua(DataComponentUtil.toNbt(components)));
        properties.put("fingerprint", FluidUtil.getFingerprint(stack));
        return properties;
    }

    public static Map<String, Object> chemicalStackToObject(@NotNull ChemicalStack stack) {
        // In theory should not be called if the addon is not installed, but just to be save
        if (!APAddon.MEKANISM.isLoaded()) {
            return null;
        }

        if (stack.isEmpty()) {
            return null;
        }
        Map<String, Object> properties = chemicalToObject(stack.getChemical());
        properties.put("count", stack.getAmount());
        properties.put("displayName", stack.getTextComponent().getString());
        properties.put("fingerprint", ChemicalUtil.getFingerprint(stack));
        return properties;
    }

    public static Map<String, Object> fluidTypeToObject(FluidType type) {
        Map<String, Object> properties = new HashMap<>();
        properties.put("viscosity", type.getViscosity());
        properties.put("density", type.getDensity());
        properties.put("canHydrate", type.canHydrate((Entity) null));
        properties.put("canExtinguish", type.canExtinguish(null));
        properties.put("canDrownIn", type.canDrownIn(null));
        properties.put("canSwim", type.canSwim(null));
        properties.put("canPushEntity", type.canPushEntity(null));
        properties.put("supportsBoating", type.supportsBoating(null));
        properties.put("canConvertToSource", type.canConvertToSource(null));
        properties.put("temperature", type.getTemperature(null));
        return properties;
    }

    public static Map<String, Object> itemStackToObject(@NotNull ItemStack itemStack, long count) {
        if (itemStack.isEmpty()) {
            return null;
        }
        Map<String, Object> properties = itemStackToObject(itemStack);
        properties.put("count", count);
        return properties;
    }

    public static Map<String, Object> fluidStackToObject(@NotNull FluidStack fluidStack, long count) {
        if (fluidStack.isEmpty()) {
            return null;
        }
        Map<String, Object> properties = fluidStackToObject(fluidStack);
        properties.put("count", count);
        return properties;
    }

    public static Map<String, Object> chemicalStackToObject(@NotNull ChemicalStack chemicalStack, long count) {
        // In theory should not be called if the addon is not installed, but just to be save
        if (!APAddon.MEKANISM.isLoaded()) {
            return null;
        }

        if (chemicalStack.isEmpty()) {
            return null;
        }
        Map<String, Object> properties = chemicalStackToObject(chemicalStack);
        properties.put("count", count);
        return properties;
    }

    /**
     * Returns the stack but with a slot entry. Used to prevent zero indexed tables
     *
     * @param stack the item stack
     * @param slot  the slot of the item
     * @return a Map containing proper item stack details
     * @see InventoryManagerPeripheral#getItems()
     */
    public static Map<String, Object> stackToObjectWithSlot(@NotNull ItemStack stack, int slot) {
        if (stack.isEmpty()) {
            return null;
        }
        Map<String, Object> properties = itemStackToObject(stack);
        properties.put("slot", slot);
        return properties;
    }

    public static Map<String, Object> itemToObject(@NotNull Item item) {
        Map<String, Object> properties = new HashMap<>();
        properties.put("tags", getHolderTags(item.builtInRegistryHolder()));
        properties.put("name", ItemUtil.getRegistryKey(item).toString());
        return properties;
    }

    public static Map<String, Object> fluidToObject(@NotNull Fluid fluid) {
        Map<String, Object> properties = new HashMap<>();
        properties.put("tags", getHolderTags(fluid.builtInRegistryHolder()));
        properties.put("name", FluidUtil.getRegistryKey(fluid).toString());
        return properties;
    }

    public static Map<String, Object> chemicalToObject(@NotNull Chemical chemical) {
        // In theory should not be called if the addon is not installed, but just to be save
        if (!APAddon.MEKANISM.isLoaded()) {
            return null;
        }

        Map<String, Object> properties = new HashMap<>();
        properties.put("tags", getHolderTags(MekanismAPI.CHEMICAL_REGISTRY.wrapAsHolder(chemical)));
        properties.put("isGaseous", chemical.isGaseous());
        properties.put("radioactivity", chemical.isRadioactive());
        properties.put("name", ChemicalUtil.getRegistryKey(chemical).toString());
        return properties;
    }

    private static final LRUCache<ResourceKey<?>, List<String>> HOLDER2TAGS_CACHE = new LRUCache<>(512);

    public static <T> List<String> getHolderTags(final Holder<T> holder) {
        final ResourceKey<T> holderKey = holder.unwrapKey().orElse(null);
        if (holderKey == null) {
            return tagsToList(holder.tags());
        }
        synchronized (HOLDER2TAGS_CACHE) {
            return HOLDER2TAGS_CACHE.computeIfAbsent(holderKey, (k) -> tagsToList(holder.tags()));
        }
    }

    public static <T> List<String> tagsToList(Stream<TagKey<T>> tags) {
        return tags.map(LuaConverter::tagToString).toList();
    }

    private static final LRUCache<TagKey<?>, String> TAG2STRING_CACHE = new LRUCache<>(1024);

    public static String tagToString(@NotNull TagKey<?> tag) {
        synchronized (TAG2STRING_CACHE) {
            return TAG2STRING_CACHE.computeIfAbsent(tag, (t) -> registryToSlashString(t.registry()) + t.location().toString());
        }
    }

    private static final LRUCache<ResourceKey<?>, String> REGISTRY2SLASH_STRING_CACHE = new LRUCache<>(256);

    private static String registryToSlashString(final ResourceKey<? extends Registry<?>> key) {
        synchronized (REGISTRY2SLASH_STRING_CACHE) {
            return REGISTRY2SLASH_STRING_CACHE.computeIfAbsent(key, (k) -> k.location().toString() + "/");
        }
    }

    // BlockPos tricks
    public static BlockPos convertToBlockPos(Map<?, ?> table) throws LuaException {
        if (!table.containsKey("x") || !table.containsKey("y") || !table.containsKey("z"))
            throw new LuaException("Table should be block position table");
        if (!(table.get("x") instanceof Number x) || !(table.get("y") instanceof Number y) || !(table.get("z") instanceof Number z))
            throw new LuaException("Table should be block position table");
        return new BlockPos(x.intValue(), y.intValue(), z.intValue());
    }

    public static BlockPos convertToBlockPos(BlockPos center, Map<?, ?> table) throws LuaException {
        BlockPos relative = convertToBlockPos(table);
        return new BlockPos(center.getX() + relative.getX(), center.getY() + relative.getY(), center.getZ() + relative.getZ());
    }
}
