/*
 *   This file is a part of the SteamWar software.
 *
 *   Copyright (C) 2025 SteamWar.de-Serverteam
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU Affero General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *   GNU Affero General Public License for more details.
 *
 *   You should have received a copy of the GNU Affero General Public License
 *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package de.steamwar.entity;

import de.steamwar.Reflection;
import de.steamwar.core.*;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import lombok.Getter;
import org.bukkit.Location;
import org.bukkit.entity.EntityType;
import org.bukkit.inventory.ItemStack;

import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;

public class REntity {

    private static final Object entityStatusWatcher = BountifulWrapper.impl.getDataWatcherObject(0, Byte.class);
    private static final Object sneakingDataWatcher = BountifulWrapper.impl.getDataWatcherObject(Core.getVersion() > 12 ? 6 : 0, FlatteningWrapper.impl.getPose(FlatteningWrapper.EntityPose.NORMAL).getClass());
    private static final Object bowDrawnWatcher = BountifulWrapper.impl.getDataWatcherObject(Core.getVersion() > 12 ? 7 : 6, Byte.class);
    private static final Object nameWatcher = BountifulWrapper.impl.getDataWatcherObject(2, Core.getVersion() > 12 ? Optional.class : String.class); // Optional<IChatBaseComponent>
    private static final Object nameVisibleWatcher = BountifulWrapper.impl.getDataWatcherObject(3, Boolean.class);

    private static final Object noGravityDataWatcher = BountifulWrapper.impl.getDataWatcherObject(5,Boolean.class);

    private static int entityIdCounter = -1;
    private static final Random random = new Random();

    protected final REntityServer server;
    private final EntityType entityType;
    @Getter
    protected final int entityId;
    @Getter
    protected final UUID uuid;

    @Getter
    protected double x;
    @Getter
    protected double y;
    @Getter
    protected double z;
    private byte yaw;
    private byte pitch;
    private byte headYaw;

    @Getter
    private boolean hidden;
    @Getter
    private boolean invisible;
    @Getter
    private FlatteningWrapper.EntityPose pose = FlatteningWrapper.EntityPose.NORMAL;
    @Getter
    private boolean bowDrawn;
    @Getter
    private boolean noGravity;
    private boolean isGlowing;
    private int fireTick;

    private final int objectData;
    @Getter
    private String displayName;
    protected final Map<Object, ItemStack> itemSlots;

    public REntity(REntityServer server, EntityType entityType, Location location) {
        this(server,entityType,location,0);
        server.addEntity(this);
    }

    protected REntity(REntityServer server, EntityType entityType, Location location,int objectData) {
        this(server, entityType, new UUID(random.nextLong() & -61441L | 16384L, random.nextLong() & 4611686018427387903L | -9223372036854775808L), location,objectData);
    }

    protected REntity(REntityServer server, EntityType entityType, UUID uuid, Location location,int objectData) {
        this.server = server;
        this.entityType = entityType;
        this.entityId = entityIdCounter--;
        this.uuid = uuid;

        this.x = location.getX();
        this.y = location.getY();
        this.z = location.getZ();
        this.headYaw = this.yaw = rotToByte(location.getYaw());
        this.pitch = rotToByte(location.getPitch());

        this.itemSlots = entityType == EntityType.PLAYER ? new HashMap<>() : null;

        this.noGravity = false;
        this.isGlowing = false;

        this.objectData = objectData;
    }

    public void hide(boolean hide) {
        if(hidden == hide)
            return;

        if(hide) {
            despawn(packet -> server.updateEntity(this, packet));
            hidden = true;
        } else {
            hidden = false;
            spawn(packet -> server.updateEntity(this, packet));
        }
    }

    public void move(Location location) {
        move(location.getX(), location.getY(), location.getZ(), location.getPitch(), location.getYaw(), rotToByte(location.getYaw()));
    }

    private static final double MAX_REL_MOVE = Core.getVersion() > 8 ? 8.0 : 4.0;
    public void move(double locX, double locY, double locZ, float pitch, float yaw, byte headYaw) {
        server.preEntityMove(this, locX, locZ);

        double fromX = this.x;
        double fromZ = this.z;
        double diffX = locX - x;
        double diffY = locY - y;
        double diffZ = locZ - z;
        boolean rotEq = this.yaw == yaw && this.pitch == pitch;

        this.x = locX;
        this.y = locY;
        this.z = locZ;
        this.yaw = rotToByte(yaw);
        this.pitch = rotToByte(pitch);

        if(Math.abs(diffX) < MAX_REL_MOVE && Math.abs(diffY) < MAX_REL_MOVE && Math.abs(diffZ) < MAX_REL_MOVE) {
            Object packet = getMoveLookPacket(diffX, diffY, diffZ,rotEq);

            if(packet != null)
                server.updateEntity(this, packet);
        } else {
            server.updateEntity(this, getTeleportPacket());
        }

        if(this.headYaw != headYaw) {
            this.headYaw = headYaw;
            server.updateEntity(this, getHeadRotationPacket());
        }

        server.postEntityMove(this, fromX, fromZ);
    }

    private static final Class<?> animationPacket = Reflection.getClass("net.minecraft.network.protocol.game.ClientboundAnimatePacket");
    private static final Reflection.Field<Integer> animationEntity = Reflection.getField(animationPacket, int.class, Core.getVersion() > 15 ? (Core.getVersion() > 19 ? 5 : 6) : 0);
    private static final Reflection.Field<Integer> animationAnimation = Reflection.getField(animationPacket, int.class, Core.getVersion() > 15 ? (Core.getVersion() > 19 ? 6 : 7) : 1);
    public void showAnimation(byte animation) {
        Object packet = Reflection.newInstance(animationPacket);
        animationEntity.set(packet, entityId);
        animationAnimation.set(packet, (int) animation);
        server.updateEntity(this, packet);
    }

    private static final Class<?> velocityPacket = Reflection.getClass("net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket");
    private static final Reflection.Field<Integer> velocityEntity = Reflection.getField(velocityPacket, int.class, 0);
    private static final Reflection.Field<Integer> velocityX = Reflection.getField(velocityPacket, int.class, 1);
    private static final Reflection.Field<Integer> velocityY = Reflection.getField(velocityPacket, int.class, 2);
    private static final Reflection.Field<Integer> velocityZ = Reflection.getField(velocityPacket, int.class, 3);
    public void setVelocity(double dX, double dY, double dZ) {
        Object packet = Reflection.newInstance(velocityPacket);
        velocityEntity.set(packet, entityId);
        velocityX.set(packet, calcVelocity(dX));
        velocityY.set(packet, calcVelocity(dY));
        velocityZ.set(packet, calcVelocity(dZ));
        server.updateEntity(this, packet);
    }

    private static final Class<?> statusPacket = Reflection.getClass("net.minecraft.network.protocol.game.ClientboundEntityEventPacket");
    private static final Reflection.Field<Integer> statusEntity = Reflection.getField(statusPacket, int.class, 0);
    private static final Reflection.Field<Byte> statusStatus = Reflection.getField(statusPacket, byte.class, 0);
    public void showDamage() {
        Object packet = Reflection.newInstance(statusPacket);
        statusEntity.set(packet, entityId);
        statusStatus.set(packet, (byte) 2);
        server.updateEntity(this, packet);
    }

    public void setPose(FlatteningWrapper.EntityPose pose) {
        this.pose = pose;
        if(Core.getVersion() > 12) {
            server.updateEntity(this, getDataWatcherPacket(sneakingDataWatcher, FlatteningWrapper.impl.getPose(pose)));
        } else {
            server.updateEntity(this, getDataWatcherPacket(entityStatusWatcher, getEntityStatus()));
        }
    }

    public void setOnFire(boolean perma) {
        fireTick = perma ? -1 : 21;
        server.updateEntity(this, getDataWatcherPacket(entityStatusWatcher, getEntityStatus()));
    }

    public boolean isOnFire() {
        return fireTick == -1 || fireTick > 0;
    }

    public void setInvisible(boolean invisible) {
        this.invisible = invisible;
        server.updateEntity(this, getDataWatcherPacket(entityStatusWatcher, getEntityStatus()));
    }

    public void setBowDrawn(boolean drawn, boolean offHand) {
        bowDrawn = drawn;
        if(Core.getVersion() > 8){
            server.updateEntity(this, getDataWatcherPacket(bowDrawnWatcher, (byte) ((drawn ? 1 : 0) + (offHand ? 2 : 0))));
        }else{
            server.updateEntity(this, getDataWatcherPacket(entityStatusWatcher, getEntityStatus()));
        }
    }

    public void setDisplayName(String displayName) {
        this.displayName = displayName;
        server.updateEntity(this, getDataWatcherPacket(
                nameWatcher, FlatteningWrapper.impl.formatDisplayName(displayName),
                nameVisibleWatcher, displayName != null
        ));
    }

    public void setItem(Object slot, ItemStack stack) {
        itemSlots.put(slot, stack);
        server.updateEntity(this, getEquipmentPacket(slot, stack));
    }

    public void die() {
        server.removeEntity(this);
    }

    public void setNoGravity(boolean noGravity) {
        this.noGravity = noGravity;
        if(Core.getVersion() > 8)
            server.updateEntity(this,getDataWatcherPacket(noGravityDataWatcher,noGravity));
    }

    public void setGlowing(boolean glowing) {
        this.isGlowing = glowing;
        if(Core.getVersion() > 8) {
            server.updateEntity(this,getDataWatcherPacket(entityStatusWatcher,getEntityStatus()));
        }
    }

    public boolean isGlowing() {
        return isGlowing;
    }

    private static int spawnPacketOffset() {
        switch (Core.getVersion()) {
            case 8:
            case 18:
                return 1;
            case 9:
            case 10:
            case 12:
            case 14:
            case 15:
                return 0;
            case 19:
            default:
                return 2;
        }
    }
    private static final Function<REntity, Object> spawnPacketGenerator = entitySpawnPacketGenerator(ProtocolWrapper.spawnPacket, spawnPacketOffset());
    private static int objectDataOffset() {
        switch (Core.getVersion()) {
            case 8:
                return 9;
            case 9:
            case 14:
            case 12:
            case 10:
            case 15:
            case 18:
                return 6;
            case 19:
            default:
                return  4;
        }
    }
    private static final Reflection.Field<Integer> additionalData = Reflection.getField(ProtocolWrapper.spawnPacket, int.class, objectDataOffset());
    private Object spawnPacketGenerator() {
        Object packet = spawnPacketGenerator.apply(this);
        additionalData.set(packet, objectData);
        return packet;
    }

    void list(Consumer<Object> packetSink) {
        // empty for regular entity
    }

    private static final Function<REntity, Object> livingSpawnPacketGenerator = Core.getVersion() >= 19 ? REntity::spawnPacketGenerator : entitySpawnPacketGenerator(ProtocolWrapper.spawnLivingPacket, Core.getVersion() == 8 ? 2 : 0);
    void spawn(Consumer<Object> packetSink) {
        if(entityType.isAlive()) {
            packetSink.accept(livingSpawnPacketGenerator.apply(this));
        } else {
            packetSink.accept(spawnPacketGenerator());
        }

        postSpawn(packetSink);
    }

    protected void postSpawn(Consumer<Object> packetSink) {
        if(headYaw != 0) {
            packetSink.accept(getHeadRotationPacket());
        }

        if(Core.getVersion() > 12 && pose != FlatteningWrapper.EntityPose.NORMAL) {
            packetSink.accept(getDataWatcherPacket(sneakingDataWatcher, FlatteningWrapper.impl.getPose(pose)));
        }

        byte status = getEntityStatus();
        if(status != 0) {
            packetSink.accept(getDataWatcherPacket(entityStatusWatcher, getEntityStatus()));
        }

        if(displayName != null) {
            packetSink.accept(getDataWatcherPacket(nameWatcher, FlatteningWrapper.impl.formatDisplayName(displayName), nameVisibleWatcher, true));
        }

        if(Core.getVersion() > 8 && noGravity)
            packetSink.accept(getDataWatcherPacket(noGravityDataWatcher, true));
    }

    void tick() {
        if(fireTick > 0) {
            fireTick--;
            if(fireTick == 0) {
                server.updateEntity(this, getDataWatcherPacket(entityStatusWatcher, getEntityStatus()));
            }
        }
    }

    private static final Class<?> destroyPacket = Reflection.getClass("net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket");
    private static final Reflection.Field<?> destroyEntities;
    static {
        if(Core.getVersion() > 15)
            destroyEntities = Reflection.getField(destroyPacket, IntList.class, 0);
        else
            destroyEntities = Reflection.getField(destroyPacket, int[].class, 0);
    }
    void despawn(Consumer<Object> packetSink){
        Object packet = Reflection.newInstance(destroyPacket);
        destroyEntities.set(packet, Core.getVersion() > 15 ? new IntArrayList(new int[]{entityId}) : new int[]{entityId});
        packetSink.accept(packet);
    }

    void delist(Consumer<Object> packetSink) {
        // empty for regular entity
    }

    double x() {
        return x;
    }

    double z() {
        return z;
    }

    private byte getEntityStatus() {
        byte status = 0;

        if(fireTick != 0)
            status |= 1;
        if(pose == FlatteningWrapper.EntityPose.SNEAKING)
            status |= 2;
        if(Core.getVersion() == 8 && bowDrawn)
            status |= 0x10;
        if(invisible)
            status |= 0x20;
        if(Core.getVersion() > 8 && isGlowing)
            status |= 0x40;

        return status;
    }

    protected Object getDataWatcherPacket(Object... dataWatcherKeyValues) {
        return ChatWrapper.impl.getDataWatcherPacket(entityId, dataWatcherKeyValues);
    }

    public static final Class<?> teleportPacket = Reflection.getClass("net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket");
    public static final Reflection.Field<Integer> teleportEntity = Reflection.getField(teleportPacket, int.class, 0);
    public static final BountifulWrapper.PositionSetter teleportPosition = BountifulWrapper.impl.getPositionSetter(teleportPacket, Core.getVersion() == 8 ? 1 : 0);
    private Object getTeleportPacket(){
        if (Core.getVersion() >= 21) {
            return PacketConstructor.impl.teleportPacket(entityId, x, y, z, pitch, yaw);
        }

        Object packet = Reflection.newInstance(teleportPacket);
        teleportEntity.set(packet, entityId);
        teleportPosition.set(packet, x, y, z, pitch, yaw);
        return packet;
    }

    private static final Class<?> entityPacket = Reflection.getClass("net.minecraft.network.protocol.game.ClientboundMoveEntityPacket");
    private static final Reflection.Field<Integer> moveEntityId = Reflection.getField(entityPacket, int.class, 0);
    private static final BountifulWrapper.PositionSetter movePosition = BountifulWrapper.impl.getRelMoveSetter(entityPacket);
    private static final Class<?> lookPacket = Reflection.getClass("net.minecraft.network.protocol.game.ClientboundMoveEntityPacket$Rot");
    private static final Class<?> movePacket = Reflection.getClass("net.minecraft.network.protocol.game.ClientboundMoveEntityPacket$Pos");
    private static final Class<?> moveLookPacket = Reflection.getClass("net.minecraft.network.protocol.game.ClientboundMoveEntityPacket$PosRot");
    private Object getMoveLookPacket(double diffX, double diffY, double diffZ, boolean rotEq) {
        Class<?> clazz;
        if(diffX == 0 && diffY == 0 && diffZ == 0) {
            if(rotEq)
                return null;

            clazz = lookPacket;
        } else if (rotEq) {
            clazz = movePacket;
        } else {
            clazz = moveLookPacket;
        }

        Object packet = Reflection.newInstance(clazz);
        moveEntityId.set(packet, entityId);
        movePosition.set(packet, diffX, diffY, diffZ, pitch, yaw);
        return packet;
    }

    private static final Class<?> headRotationPacket = Reflection.getClass("net.minecraft.network.protocol.game.ClientboundRotateHeadPacket");
    private static final Reflection.Field<Integer> headRotationEntity = Reflection.getField(headRotationPacket, int.class, 0);
    private static final Reflection.Field<Byte> headRotationYaw = Reflection.getField(headRotationPacket, byte.class, 0);
    private Object getHeadRotationPacket(){
        Object packet = Reflection.newInstance(headRotationPacket);
        headRotationEntity.set(packet, entityId);
        headRotationYaw.set(packet, headYaw);
        return packet;
    }

    private static final Reflection.Field<Integer> equipmentEntity = Reflection.getField(ProtocolWrapper.equipmentPacket, int.class, 0);

    private static final Class<?> craftItemStack = Reflection.getClass("org.bukkit.craftbukkit.inventory.CraftItemStack");
    protected static final Reflection.Method asNMSCopy = Reflection.getTypedMethod(REntity.craftItemStack, "asNMSCopy", ProtocolWrapper.itemStack, ItemStack.class);
    protected Object getEquipmentPacket(Object slot, ItemStack stack){
        Object packet = Reflection.newInstance(ProtocolWrapper.equipmentPacket);
        equipmentEntity.set(packet, entityId);
        ProtocolWrapper.impl.setEquipmentPacketStack(packet, slot, asNMSCopy.invoke(null, stack));
        return packet;
    }

    private static Function<REntity, Object> entitySpawnPacketGenerator(Class<?> spawnPacket, int posOffset) {
        BountifulWrapper.UUIDSetter uuid = BountifulWrapper.impl.getUUIDSetter(spawnPacket);
        Function<REntity, Object> packetGenerator = spawnPacketGenerator(spawnPacket, posOffset);

        return entity -> {
            Object packet = packetGenerator.apply(entity);
            uuid.set(packet, entity.uuid);
            FlatteningWrapper.impl.setSpawnPacketType(packet, entity.entityType);
            return packet;
        };
    }

    protected static Function<REntity, Object> spawnPacketGenerator(Class<?> spawnPacket, int posOffset) {
        Reflection.Field<Integer> entityId = Reflection.getField(spawnPacket, int.class, 0);
        BountifulWrapper.PositionSetter position = BountifulWrapper.impl.getPositionSetter(spawnPacket, posOffset);

        return entity -> {
            Object packet = Reflection.newInstance(spawnPacket);
            entityId.set(packet, entity.entityId);
            position.set(packet, entity.x, entity.y, entity.z, entity.pitch, entity.yaw);
            return packet;
        };
    }

    private byte rotToByte(float rot) {
        return (byte)((int)(rot * 256.0F / 360.0F));
    }

    private int calcVelocity(double value) {
        return (int)(Math.max(-3.9, Math.min(value, 3.9)) * 8000);
    }
}
