/*
 *   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 com.mojang.datafixers.util.Pair;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import lombok.Getter;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.network.chat.contents.PlainTextContents;
import net.minecraft.network.protocol.game.*;
import net.minecraft.network.syncher.EntityDataAccessor;
import net.minecraft.network.syncher.EntityDataSerializers;
import net.minecraft.network.syncher.SynchedEntityData;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.entity.Pose;
import net.minecraft.world.entity.PositionMoveRotation;
import net.minecraft.world.phys.Vec3;
import org.bukkit.Location;
import org.bukkit.craftbukkit.inventory.CraftItemStack;
import org.bukkit.craftbukkit.util.CraftNamespacedKey;
import org.bukkit.entity.EntityType;
import org.bukkit.inventory.ItemStack;

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

public class REntity {

    private static final EntityDataAccessor<Byte> entityStatusWatcher = new EntityDataAccessor<>(0, EntityDataSerializers.BYTE);
    private static final EntityDataAccessor<Pose> poseDataWatcher = new EntityDataAccessor<>(6, EntityDataSerializers.POSE);
    private static final EntityDataAccessor<Optional<Component>> nameWatcher = new EntityDataAccessor<>(2, EntityDataSerializers.OPTIONAL_COMPONENT);
    private static final EntityDataAccessor<Boolean> nameVisibleWatcher = new EntityDataAccessor<>(3, EntityDataSerializers.BOOLEAN);
    private static final EntityDataAccessor<Boolean> noGravityDataWatcher = new EntityDataAccessor<>(5, EntityDataSerializers.BOOLEAN);

    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;
    protected byte yaw;
    protected byte pitch;
    protected byte headYaw;

    @Getter
    private boolean hidden;
    @Getter
    private boolean invisible;
    @Getter
    private Pose pose = Pose.STANDING;
    @Getter
    private boolean bowDrawn;
    @Getter
    private boolean noGravity;
    @Getter
    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 = 8.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);
    }

    public void showAnimation(byte animation) {
        ClientboundAnimatePacket packet = new ClientboundAnimatePacket();
        packet.id = entityId;
        packet.action = animation;
        server.updateEntity(this, packet);
    }

    public void setVelocity(double dX, double dY, double dZ) {
        server.updateEntity(this, new ClientboundSetEntityMotionPacket(entityId, new Vec3(calcVelocity(dX), calcVelocity(dY), calcVelocity(dZ))));
    }

    public void showDamage() {
        ClientboundEntityEventPacket packet = new ClientboundEntityEventPacket();
        packet.entityId = entityId;
        packet.eventId = (byte) 2;
        server.updateEntity(this, packet);
    }

    public void setPose(Pose pose) {
        this.pose = pose;
        server.updateEntity(this, entityDataPacket().add(poseDataWatcher, pose).build());
    }

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

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

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

    public void setBowDrawn(boolean drawn, boolean offHand) {
        bowDrawn = drawn;
        server.updateEntity(this, entityDataPacket().add(poseDataWatcher, Pose.SHOOTING).build());
    }

    public void setDisplayName(String displayName) {
        this.displayName = displayName;
        server.updateEntity(this, entityDataPacket().add(nameWatcher, formatDisplayName(displayName))
                .add(nameVisibleWatcher, displayName != null).build());
    }

    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;
        server.updateEntity(this, entityDataPacket().add(noGravityDataWatcher, noGravity).build());
    }

    public void setGlowing(boolean glowing) {
        this.isGlowing = glowing;
        server.updateEntity(this, entityDataPacket().add(entityStatusWatcher, getEntityStatus()).build());
    }

    private ClientboundAddEntityPacket spawnPacketGenerator() {
        ResourceLocation key = CraftNamespacedKey.toMinecraft(entityType.getKey());
        net.minecraft.world.entity.EntityType<?> entityType = BuiltInRegistries.ENTITY_TYPE.get(key).get().value();
        return new ClientboundAddEntityPacket(entityId, uuid, x, y, z, pitch, yaw, entityType, objectData, Vec3.ZERO, 0);
    }

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

    void spawn(Consumer<Object> packetSink) {
        packetSink.accept(spawnPacketGenerator());
        postSpawn(packetSink);
    }

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

        if (pose != Pose.STANDING) {
            entityDataPacket().add(poseDataWatcher, pose).send(packetSink);
        }

        byte status = getEntityStatus();
        if (status != 0) {
            entityDataPacket().add(entityStatusWatcher, getEntityStatus()).send(packetSink);
        }

        if (displayName != null) {
            entityDataPacket().add(nameWatcher, formatDisplayName(displayName))
                    .add(nameVisibleWatcher, true)
                    .send(packetSink);
        }

        if (noGravity) {
            entityDataPacket().add(noGravityDataWatcher, true).send(packetSink);
        }
    }

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

    void despawn(Consumer<Object> packetSink) {
        packetSink.accept(new ClientboundRemoveEntitiesPacket(new IntArrayList(new int[]{entityId})));
    }

    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 == Pose.CROUCHING) status |= 2;
        if (invisible) status |= 0x20;
        if (isGlowing) status |= 0x40;
        return status;
    }

    protected EntityDataPacketBuilder entityDataPacket() {
        return new EntityDataPacketBuilder();
    }

    public class EntityDataPacketBuilder {
        private List<SynchedEntityData.DataValue<?>> values = new ArrayList<>();

        public <T> EntityDataPacketBuilder add(EntityDataAccessor<T> accessor, T value) {
            values.add(new SynchedEntityData.DataItem<>(accessor, value).value());
            return this;
        }

        public boolean isEmpty() {
            return values.isEmpty();
        }

        public ClientboundSetEntityDataPacket build() {
            return new ClientboundSetEntityDataPacket(entityId, values);
        }

        public void send(Consumer<Object> packetSink) {
            packetSink.accept(build());
        }
    }

    private Object getTeleportPacket() {
        PositionMoveRotation rot = new PositionMoveRotation(new Vec3(x, y, z), Vec3.ZERO, pitch, yaw);
        return new ClientboundTeleportEntityPacket(entityId, rot, Collections.emptySet(), false);
    }

    private Object getMoveLookPacket(double diffX, double diffY, double diffZ, boolean rotEq) {
        short x = (short) (diffX * 4096);
        short y = (short) (diffY * 4096);
        short z = (short) (diffZ * 4096);
        byte yaw = (byte) (this.yaw * 256 / 360);
        byte pitch = (byte) (this.pitch * 256 / 360);

        if (diffX == 0 && diffY == 0 && diffZ == 0) {
            if (rotEq) return null;
            return new ClientboundMoveEntityPacket.Rot(entityId, pitch, yaw, false);
        } else if (rotEq) {
            return new ClientboundMoveEntityPacket.Pos(entityId, x, y, z, false);
        } else {
            return new ClientboundMoveEntityPacket.PosRot(entityId, x, y, z, pitch, yaw, false);
        }
    }

    private Object getHeadRotationPacket() {
        ClientboundRotateHeadPacket packet = new ClientboundRotateHeadPacket();
        packet.entityId = entityId;
        packet.yHeadRot = headYaw;
        return packet;
    }

    protected Object getEquipmentPacket(Object slot, ItemStack stack) {
        return new ClientboundSetEquipmentPacket(entityId, Collections.singletonList(Pair.of((EquipmentSlot) slot, CraftItemStack.asNMSCopy(stack))));
    }

    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);
    }

    private static Optional<Component> formatDisplayName(String displayName) {
        return displayName != null ? Optional.of(MutableComponent.create(PlainTextContents.create(displayName))) : Optional.empty();
    }
}
