/*
 * Copyright (c) 2025 GeyserMC. http://geysermc.org
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @author GeyserMC
 * @link https://github.com/GeyserMC/Geyser
 */

package org.geysermc.geyser.entity.spawn;

import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.protocol.common.util.TriFunction;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.entity.definition.GeyserEntityDefinition;
import org.geysermc.geyser.api.entity.definition.JavaEntityType;
import org.geysermc.geyser.api.entity.type.GeyserEntity;
import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity;
import org.geysermc.geyser.api.event.java.ServerAttachParrotsEvent;
import org.geysermc.geyser.api.event.java.ServerSpawnEntityEvent;
import org.geysermc.geyser.entity.BedrockEntityDefinition;
import org.geysermc.geyser.entity.EntityTypeDefinition;
import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.entity.ClientboundAddEntityPacket;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Objects;
import java.util.UUID;
import java.util.function.Consumer;

@Getter
@Setter
@Accessors(fluent = true)
public class EntitySpawnContext {
    private final GeyserSession session;
    private final EntityTypeDefinition<?> entityTypeDefinition;
    private int javaId;
    private final UUID uuid;
    private BedrockEntityDefinition bedrockEntityDefinition;
    private Vector3f position;
    private Vector3f motion;
    private float yaw;
    private float pitch;
    private float headYaw;
    private @Nullable Long geyserId;
    private @Nullable Collection<Consumer<GeyserEntity>> consumers;

    public static final TriFunction<GeyserSession, UUID, EntityTypeDefinition<?>, EntitySpawnContext> DUMMY_CONTEXT = ((session, uuid, definition) -> {
        return new EntitySpawnContext(session, definition, -1, uuid);
    });

    public EntitySpawnContext(GeyserSession session, EntityTypeDefinition<?> type, int javaId, UUID uuid) {
        this(session, type, javaId, uuid, type.defaultBedrockDefinition(), Vector3f.ZERO, Vector3f.ZERO, 0, 0, 0, null);
    }

    public EntitySpawnContext(GeyserSession session, EntityTypeDefinition<?> type, int entityId, long geyserId) {
        this(session, type, entityId, null, type.defaultBedrockDefinition(), Vector3f.ZERO, Vector3f.ZERO, 0, 0, 0, geyserId);
    }

    public static EntitySpawnContext fromPacket(GeyserSession session, EntityTypeDefinition<?> definition, ClientboundAddEntityPacket packet) {
        Vector3f position = Vector3f.from(packet.getX(), packet.getY(), packet.getZ());
        Vector3f motion = packet.getMovement().toFloat();
        return new EntitySpawnContext(session, definition, packet.getEntityId(), packet.getUuid(), definition.defaultBedrockDefinition(),
            position, motion, packet.getYaw(), packet.getPitch(), packet.getHeadYaw(), null);
    }

    public static EntitySpawnContext inherited(GeyserSession session, EntityTypeDefinition<?> definition, Entity base, Vector3f position) {
        return new EntitySpawnContext(session, definition, -1, null, definition.defaultBedrockDefinition(), position, base.getMotion(), base.getYaw(),
            base.getPitch(), base.getHeadYaw(), null);
    }

    public EntitySpawnContext(GeyserSession session, EntityTypeDefinition<?> definition, int javaId, UUID uuid, BedrockEntityDefinition bedrockEntityDefinition, Vector3f position,
                              Vector3f motion, float yaw, float pitch, float headYaw, @Nullable Long geyserId) {
        this.session = session;
        this.entityTypeDefinition = definition;
        this.javaId = javaId;
        this.uuid = uuid;
        this.position = position;
        this.bedrockEntityDefinition = bedrockEntityDefinition;
        this.motion = motion;
        this.yaw = yaw;
        this.pitch = pitch;
        this.headYaw = headYaw;
        this.geyserId = geyserId;
        this.consumers = null;
    }

    /**
     * @return true if an entity should be spawned
     */
    public boolean callServerSpawnEvent() {
        ServerSpawnEntityEvent event = new ServerSpawnEntityEvent(session) {
            private boolean cancelled = false;

            @Override
            public boolean isCancelled() {
                return cancelled || bedrockEntityDefinition == null;
            }

            @Override
            public void setCancelled(boolean cancelled) {
                this.cancelled = cancelled;
            }

            @Override
            public int entityId() {
                return javaId;
            }

            @Override
            public @NonNull UUID uuid() {
                return uuid;
            }

            @Override
            public @NonNull JavaEntityType entityType() {
                return entityTypeDefinition.type();
            }

            @Override
            public @Nullable GeyserEntityDefinition definition() {
                return bedrockEntityDefinition;
            }

            @Override
            public void definition(@Nullable GeyserEntityDefinition entityDefinition) {
                if (entityDefinition == null) {
                    bedrockEntityDefinition = null;
                    cancelled = true;
                    return;
                }

                if (!(entityDefinition instanceof BedrockEntityDefinition definition)) {
                    throw new IllegalStateException("Unknown implementation of GeyserEntityDefinition! Got: " + entityDefinition.getClass().getSimpleName());
                }

                if (!entityDefinition.registered()) {
                    throw new IllegalStateException("%s is not registered!".formatted(entityDefinition.identifier()));
                }

                bedrockEntityDefinition = definition;
            }

            @Override
            public void preSpawnConsumer(@NonNull Consumer<@NonNull GeyserEntity> consumer) {
                Objects.requireNonNull(consumer, "consumer cannot be null!");
                if (consumers == null) {
                    consumers = new ArrayList<>();
                }
                consumers.add(consumer);
            }
        };

        GeyserImpl.getInstance().eventBus().fire(event);

        return bedrockEntityDefinition != null && !event.isCancelled();
    }

    /**
     * @return true if the parrot should be spawned
     */
    public boolean callParrotEvent(PlayerEntity player, int variant, boolean right) {
        ServerAttachParrotsEvent event = new ServerAttachParrotsEvent(session) {
            private boolean cancelled = false;

            @Override
            public @NonNull GeyserPlayerEntity player() {
                return player;
            }

            @Override
            public int variant() {
                return variant;
            }

            @Override
            public boolean right() {
                return right;
            }

            @Override
            public @Nullable GeyserEntityDefinition definition() {
                return bedrockEntityDefinition;
            }

            @Override
            public void definition(@Nullable GeyserEntityDefinition entityDefinition) {
                if (entityDefinition == null) {
                    bedrockEntityDefinition = null;
                    return;
                }

                if (!(entityDefinition instanceof BedrockEntityDefinition definition)) {
                    throw new IllegalStateException("Unknown implementation of GeyserEntityDefinition! Got: " + entityDefinition.getClass().getSimpleName());
                }

                if (!entityDefinition.registered()) {
                    throw new IllegalStateException("%s is not registered!".formatted(entityDefinition.identifier()));
                }

                bedrockEntityDefinition = definition;
            }

            @Override
            public boolean isCancelled() {
                return cancelled || bedrockEntityDefinition == null;
            }

            @Override
            public void setCancelled(boolean cancelled) {
                this.cancelled = cancelled;
            }

            @Override
            public void preSpawnConsumer(@NonNull Consumer<@NonNull GeyserEntity> consumer) {
                Objects.requireNonNull(consumer, "consumer cannot be null!");
                if (consumers == null) {
                    consumers = new ArrayList<>();
                }
                consumers.add(consumer);
            }
        };

        GeyserImpl.getInstance().eventBus().fire(event);
        return bedrockEntityDefinition != null && !event.isCancelled();
    }

    // Not assigned by default - preparation for cancellable entity spawning
    public long geyserId() {
        if (geyserId == null) {
            return geyserId = session.getEntityCache().nextEntityId();
        }
        return geyserId;
    }
}
