package net.minestom.server.collision;

import net.minestom.server.MinecraftServer;
import net.minestom.server.api.Env;
import net.minestom.server.api.EnvTest;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityProjectile;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.LivingEntity;
import net.minestom.server.event.EventNode;
import net.minestom.server.event.entity.projectile.ProjectileCollideWithBlockEvent;
import net.minestom.server.event.entity.projectile.ProjectileCollideWithEntityEvent;
import net.minestom.server.event.entity.projectile.ProjectileUncollideEvent;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.block.Block;
import net.minestom.server.utils.time.TimeUnit;
import org.junit.jupiter.api.Test;

import java.util.concurrent.atomic.AtomicReference;

import static org.junit.jupiter.api.Assertions.*;

@EnvTest
public class EntityProjectileCollisionIntegrationTest {

    @Test
    public void blockShootAndBlockRemoval(Env env) {
        final Instance instance = env.createFlatInstance();
        instance.getWorldBorder().setDiameter(1000.0);

        final Entity shooter = new Entity(EntityType.SKELETON);
        shooter.setInstance(instance, new Pos(0, 40, 0)).join();

        final EntityProjectile projectile = new EntityProjectile(shooter, EntityType.ARROW);
        projectile.setInstance(instance, shooter.getPosition().withY(y -> y + shooter.getEyeHeight())).join();

        final Point blockPosition = new Vec(5, 40, 0);
        final Block block = Block.GRASS_BLOCK;
        instance.setBlock(blockPosition, block);
        projectile.shoot(blockPosition, 1, 0);

        final var eventRef = new AtomicReference<ProjectileCollideWithBlockEvent>();
        MinecraftServer.getGlobalEventHandler().addListener(ProjectileCollideWithBlockEvent.class, eventRef::set);

        final long tick = TimeUnit.SERVER_TICK.getDuration().toMillis();
        for (int i = 0; i < MinecraftServer.TICK_PER_SECOND; ++i) {
            projectile.tick(i * tick);
        }

        var event = eventRef.get();
        assertNotNull(event);
        assertEquals(blockPosition, new Vec(event.getCollisionPosition().blockX(), event.getCollisionPosition().blockY(), event.getCollisionPosition().blockZ()));
        assertEquals(block, event.getBlock());

        final var eventRef2 = new AtomicReference<ProjectileUncollideEvent>();
        MinecraftServer.getGlobalEventHandler().addListener(ProjectileUncollideEvent.class, eventRef2::set);
        eventRef.set(null);
        instance.setBlock(blockPosition, Block.AIR);

        for (int i = 0; i < MinecraftServer.TICK_PER_SECOND; ++i) {
            projectile.tick((MinecraftServer.TICK_PER_SECOND + i) * tick);
        }
        event = eventRef.get();
        final var event2 = eventRef2.get();
        assertNotNull(event);
        assertNotNull(event2);
        assertEquals(blockPosition.withY(y -> y - 1), new Vec(event.getCollisionPosition().blockX(), event.getCollisionPosition().blockY(), event.getCollisionPosition().blockZ()));
    }

    @Test
    public void entityShoot(Env env) {
        final Instance instance = env.createFlatInstance();
        instance.getWorldBorder().setDiameter(1000.0);

        final Entity shooter = new Entity(EntityType.SKELETON);
        shooter.setInstance(instance, new Pos(0, 40, 0)).join();

        for (double dx = 1; dx <= 3; dx += .2) {
            singleEntityShoot(instance, shooter, new Vec(dx, 40, 0));
        }
    }

    private void singleEntityShoot(
            Instance instance,
            Entity shooter,
            final Point targetPosition
    ) {
        final EntityProjectile projectile = new EntityProjectile(shooter, EntityType.ARROW);
        projectile.setInstance(instance, shooter.getPosition().withY(y -> y + shooter.getEyeHeight())).join();

        final LivingEntity target = new LivingEntity(EntityType.RABBIT);
        target.setInstance(instance, Pos.fromPoint(targetPosition)).join();
        projectile.shoot(targetPosition, 1, 0);

        final var eventRef = new AtomicReference<ProjectileCollideWithEntityEvent>();
        final var eventNode = EventNode.all("projectile-test");
        eventNode.addListener(ProjectileCollideWithEntityEvent.class, event -> {
            event.getEntity().remove();
            eventRef.set(event);
            MinecraftServer.getGlobalEventHandler().removeChild(eventNode);
        });
        MinecraftServer.getGlobalEventHandler().addChild(eventNode);

        final long tick = TimeUnit.SERVER_TICK.getDuration().toMillis();
        for (int i = 0; i < MinecraftServer.TICK_PER_SECOND; ++i) {
            if (!projectile.isRemoved()) {
                projectile.tick(i * tick);
            }
        }

        final var event = eventRef.get();
        assertNotNull(event, "Could not hit entity at " + targetPosition);
        assertSame(target, event.getTarget());
        assertTrue(projectile.getBoundingBox().intersectEntity(event.getCollisionPosition(), target));
        target.remove();
    }

    @Test
    public void entitySelfShoot(Env env) {
        final Instance instance = env.createFlatInstance();
        instance.getWorldBorder().setDiameter(1000.0);

        final LivingEntity shooter = new LivingEntity(EntityType.SKELETON);
        shooter.setInstance(instance, new Pos(0, 40, 0)).join();

        final EntityProjectile projectile = new EntityProjectile(shooter, EntityType.ARROW);
        projectile.setInstance(instance, shooter.getPosition().withY(y -> y + shooter.getEyeHeight())).join();

        projectile.shoot(new Vec(0, 60, 0), 1, 0);

        final var eventRef = new AtomicReference<ProjectileCollideWithEntityEvent>();
        MinecraftServer.getGlobalEventHandler().addListener(ProjectileCollideWithEntityEvent.class, event -> {
            event.getEntity().remove();
            eventRef.set(event);
        });

        final long tick = TimeUnit.SERVER_TICK.getDuration().toMillis();
        for (int i = 0; i < MinecraftServer.TICK_PER_SECOND * 5; ++i) {
            if (!projectile.isRemoved()) {
                projectile.tick(i * tick);
            }
        }

        final var event = eventRef.get();
        assertNotNull(event);
        assertSame(shooter, event.getTarget());
        assertTrue(shooter.getBoundingBox().intersectEntity(shooter.getPosition(), projectile));
    }

}
