package org.bukkit.craftbukkit.entity;

import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import java.util.Map;
import java.util.UUID;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import net.minecraft.core.BlockPosition;
import net.minecraft.core.Holder;
import net.minecraft.core.registries.Registries;
import net.minecraft.world.entity.ai.gossip.ReputationType;
import net.minecraft.world.entity.monster.EntityZombie;
import net.minecraft.world.entity.monster.EntityZombieVillager;
import net.minecraft.world.entity.npc.EntityVillager;
import net.minecraft.world.entity.npc.VillagerProfession;
import net.minecraft.world.entity.npc.VillagerType;
import net.minecraft.world.level.block.BlockBed;
import net.minecraft.world.level.block.state.IBlockData;
import org.bukkit.Location;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.craftbukkit.CraftRegistry;
import org.bukkit.craftbukkit.CraftServer;
import org.bukkit.craftbukkit.registry.CraftOldEnumRegistryItem;
import org.bukkit.craftbukkit.util.CraftLocation;
import org.bukkit.craftbukkit.util.Handleable;
import org.bukkit.entity.Villager;
import org.bukkit.entity.ZombieVillager;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.event.entity.EntityTransformEvent;

public class CraftVillager extends CraftAbstractVillager implements Villager {

    public CraftVillager(CraftServer server, EntityVillager entity) {
        super(server, entity);
    }

    @Override
    public EntityVillager getHandle() {
        return (EntityVillager) entity;
    }

    @Override
    public String toString() {
        return "CraftVillager";
    }

    @Override
    public void remove() {
        getHandle().releaseAllPois();

        super.remove();
    }

    @Override
    public Profession getProfession() {
        return CraftProfession.minecraftToBukkit(getHandle().getVillagerData().getProfession());
    }

    @Override
    public void setProfession(Profession profession) {
        Preconditions.checkArgument(profession != null, "Profession cannot be null");
        getHandle().setVillagerData(getHandle().getVillagerData().setProfession(CraftProfession.bukkitToMinecraft(profession)));
    }

    @Override
    public Type getVillagerType() {
        return CraftType.minecraftToBukkit(getHandle().getVillagerData().getType());
    }

    @Override
    public void setVillagerType(Type type) {
        Preconditions.checkArgument(type != null, "Type cannot be null");
        getHandle().setVillagerData(getHandle().getVillagerData().setType(CraftType.bukkitToMinecraft(type)));
    }

    @Override
    public int getVillagerLevel() {
        return getHandle().getVillagerData().getLevel();
    }

    @Override
    public void setVillagerLevel(int level) {
        Preconditions.checkArgument(1 <= level && level <= 5, "level (%s) must be between [1, 5]", level);

        getHandle().setVillagerData(getHandle().getVillagerData().setLevel(level));
    }

    @Override
    public int getVillagerExperience() {
        return getHandle().getVillagerXp();
    }

    @Override
    public void setVillagerExperience(int experience) {
        Preconditions.checkArgument(experience >= 0, "Experience (%s) must be positive", experience);

        getHandle().setVillagerXp(experience);
    }

    @Override
    public boolean sleep(Location location) {
        Preconditions.checkArgument(location != null, "Location cannot be null");
        Preconditions.checkArgument(location.getWorld() != null, "Location needs to be in a world");
        Preconditions.checkArgument(location.getWorld().equals(getWorld()), "Cannot sleep across worlds");
        Preconditions.checkState(!getHandle().generation, "Cannot sleep during world generation");

        BlockPosition position = CraftLocation.toBlockPosition(location);
        IBlockData iblockdata = getHandle().level().getBlockState(position);
        if (!(iblockdata.getBlock() instanceof BlockBed)) {
            return false;
        }

        getHandle().startSleeping(position);
        return true;
    }

    @Override
    public void wakeup() {
        Preconditions.checkState(isSleeping(), "Cannot wakeup if not sleeping");
        Preconditions.checkState(!getHandle().generation, "Cannot wakeup during world generation");

        getHandle().stopSleeping();
    }

    @Override
    public void shakeHead() {
        getHandle().setUnhappy();
    }

    @Override
    public ZombieVillager zombify() {
        EntityZombieVillager entityzombievillager = EntityZombie.convertVillagerToZombieVillager(getHandle().level().getMinecraftWorld(), getHandle(), getHandle().blockPosition(), isSilent(), EntityTransformEvent.TransformReason.INFECTION, CreatureSpawnEvent.SpawnReason.CUSTOM);
        return (entityzombievillager != null) ? (ZombieVillager) entityzombievillager.getBukkitEntity() : null;
    }

    @Override
    public int getReputation(UUID uuid, ReputationType reputationType) {
        Preconditions.checkArgument(uuid != null, "UUID cannot be null");
        Preconditions.checkArgument(reputationType != null, "Reputation type cannot be null");
        return getHandle().getGossips().getReputation(uuid,
                Predicate.isEqual(CraftReputationType.bukkitToMinecraft(reputationType)),
                false);
    }

    @Override
    public int getWeightedReputation(UUID uuid, ReputationType reputationType) {
        Preconditions.checkArgument(uuid != null, "UUID cannot be null");
        Preconditions.checkArgument(reputationType != null, "Reputation type cannot be null");
        return getHandle().getGossips().getReputation(uuid,
                Predicate.isEqual(CraftReputationType.bukkitToMinecraft(reputationType)),
                true);
    }

    @Override
    public int getReputation(UUID uuid) {
        Preconditions.checkArgument(uuid != null, "UUID cannot be null");
        return getHandle().getGossips().getReputation(uuid, reputationType -> true);
    }

    @Override
    public void addReputation(UUID uuid, ReputationType reputationType, int amount) {
        addReputation(uuid, reputationType, amount, ReputationEvent.UNSPECIFIED);
    }

    @Override
    public void addReputation(UUID uuid, ReputationType reputationType, int amount, ReputationEvent changeReason) {
        Preconditions.checkArgument(uuid != null, "UUID cannot be null");
        Preconditions.checkArgument(reputationType != null, "Reputation type cannot be null");
        Preconditions.checkArgument(changeReason != null, "Change reason cannot be null");
        getHandle().getGossips().add(uuid, CraftReputationType.bukkitToMinecraft(reputationType), amount, changeReason);
    }

    @Override
    public void removeReputation(UUID uuid, ReputationType reputationType, int amount) {
        removeReputation(uuid, reputationType, amount, ReputationEvent.UNSPECIFIED);
    }

    @Override
    public void removeReputation(UUID uuid, ReputationType reputationType, int amount, ReputationEvent changeReason) {
        Preconditions.checkArgument(uuid != null, "UUID cannot be null");
        Preconditions.checkArgument(reputationType != null, "Reputation type cannot be null");
        Preconditions.checkArgument(changeReason != null, "Change reason cannot be null");
        getHandle().getGossips().remove(uuid, CraftReputationType.bukkitToMinecraft(reputationType), amount, changeReason);
    }

    @Override
    public void setReputation(UUID uuid, ReputationType reputationType, int amount) {
        setReputation(uuid, reputationType, amount, ReputationEvent.UNSPECIFIED);
    }

    @Override
    public void setReputation(UUID uuid, ReputationType reputationType, int amount, ReputationEvent changeReason) {
        Preconditions.checkArgument(uuid != null, "UUID cannot be null");
        Preconditions.checkArgument(reputationType != null, "Reputation type cannot be null");
        Preconditions.checkArgument(changeReason != null, "Change reason cannot be null");
        getHandle().getGossips().set(uuid, CraftReputationType.bukkitToMinecraft(reputationType), amount, changeReason);
    }

    @Override
    public void setGossipDecayTime(long ticks) {
        getHandle().gossipDecayInterval = ticks;
    }

    @Override
    public long getGossipDecayTime() {
        return getHandle().gossipDecayInterval;
    }

    public static class CraftType extends CraftOldEnumRegistryItem<Type, VillagerType> implements Type {
        private static int count = 0;

        public static Type minecraftToBukkit(VillagerType minecraft) {
            return CraftRegistry.minecraftToBukkit(minecraft, Registries.VILLAGER_TYPE, Registry.VILLAGER_TYPE);
        }

        public static VillagerType bukkitToMinecraft(Type bukkit) {
            return CraftRegistry.bukkitToMinecraft(bukkit);
        }

        public CraftType(NamespacedKey key, Holder<VillagerType> handle) {
            super(key, handle, count++);
        }

        @Override
        public NamespacedKey getKey() {
            return getKeyOrThrow();
        }
    }

    public static class CraftProfession extends CraftOldEnumRegistryItem<Profession, VillagerProfession> implements Profession {
        private static int count = 0;

        public static Profession minecraftToBukkit(VillagerProfession minecraft) {
            return CraftRegistry.minecraftToBukkit(minecraft, Registries.VILLAGER_PROFESSION, Registry.VILLAGER_PROFESSION);
        }

        public static VillagerProfession bukkitToMinecraft(Profession bukkit) {
            return CraftRegistry.bukkitToMinecraft(bukkit);
        }

        public CraftProfession(NamespacedKey key, Holder<VillagerProfession> handle) {
            super(key, handle, count++);
        }

        @Override
        public NamespacedKey getKey() {
            return getKeyOrThrow();
        }
    }

    public static class CraftReputationType implements ReputationType, Handleable<net.minecraft.world.entity.ai.gossip.ReputationType> {

        public static final Map<String, CraftReputationType> BY_ID = Stream
                .of(net.minecraft.world.entity.ai.gossip.ReputationType.values())
                .collect(Collectors.toMap(reputationType -> reputationType.id, CraftReputationType::new));
        private final net.minecraft.world.entity.ai.gossip.ReputationType handle;

        public CraftReputationType(net.minecraft.world.entity.ai.gossip.ReputationType handle) {
            this.handle = handle;
        }

        @Override
        public net.minecraft.world.entity.ai.gossip.ReputationType getHandle() {
            return handle;
        }

        @Override
        public int getMaxValue() {
            return handle.max;
        }

        @Override
        public int getWeight() {
            return handle.weight;
        }

        public static net.minecraft.world.entity.ai.gossip.ReputationType bukkitToMinecraft(ReputationType bukkit) {
            Preconditions.checkArgument(bukkit != null);

            return ((CraftReputationType) bukkit).getHandle();
        }

        public static ReputationType minecraftToBukkit(net.minecraft.world.entity.ai.gossip.ReputationType minecraft) {
            Preconditions.checkArgument(minecraft != null);

            return switch (minecraft) {
                case MAJOR_NEGATIVE -> ReputationType.MAJOR_NEGATIVE;
                case MINOR_NEGATIVE -> ReputationType.MINOR_NEGATIVE;
                case MINOR_POSITIVE -> ReputationType.MINOR_POSITIVE;
                case MAJOR_POSITIVE -> ReputationType.MAJOR_POSITIVE;
                case TRADING -> ReputationType.TRADING;
            };
        }
    }

    public static class CraftReputationEvent implements ReputationEvent, Handleable<net.minecraft.world.entity.ai.village.ReputationEvent> {

        private static final Map<String, ReputationEvent> ALL = Maps.newHashMap();
        private final net.minecraft.world.entity.ai.village.ReputationEvent handle;

        public CraftReputationEvent(net.minecraft.world.entity.ai.village.ReputationEvent handle) {
            this.handle = handle;
            ALL.put(handle.toString(), this);
        }

        @Override
        public net.minecraft.world.entity.ai.village.ReputationEvent getHandle() {
            return handle;
        }

        public static net.minecraft.world.entity.ai.village.ReputationEvent bukkitToMinecraft(ReputationEvent bukkit) {
            Preconditions.checkArgument(bukkit != null);

            return ((CraftReputationEvent) bukkit).getHandle();
        }

        public static ReputationEvent minecraftToBukkit(net.minecraft.world.entity.ai.village.ReputationEvent minecraft) {
            Preconditions.checkArgument(minecraft != null);

            ReputationEvent bukkit = ALL.get(minecraft.toString());
            return bukkit == null ? new CraftReputationEvent(minecraft) : bukkit;
        }
    }
}
