package eu.mhsl.minenet.minigames.instance.game.stateless.types.turtleGame;

import eu.mhsl.minenet.minigames.instance.Dimension;
import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.turtleGame.gameObjects.Turtle;
import eu.mhsl.minenet.minigames.score.PointsWinScore;
import net.kyori.adventure.sound.Sound;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.minestom.server.MinecraftServer;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.*;
import net.minestom.server.entity.metadata.other.FallingBlockMeta;
import net.minestom.server.event.player.PlayerStartSneakingEvent;
import net.minestom.server.event.player.PlayerStopSneakingEvent;
import net.minestom.server.event.player.PlayerTickEvent;
import net.minestom.server.instance.block.Block;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import net.minestom.server.network.packet.server.play.ParticlePacket;
import net.minestom.server.particle.Particle;
import net.minestom.server.sound.SoundEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;
import java.util.concurrent.CompletableFuture;

class TurtleGame extends StatelessGame {
    private final int radius;
    private final Map<Player, Turtle> turtlePlayerMap = new WeakHashMap<>();
    private final ArrayList<Entity> snacks = new ArrayList<>();
    private final ArrayList<Entity> bombs = new ArrayList<>();
    private final Block snackBlock = Block.SUNFLOWER.withProperty("half", "upper");
    private final double startSpeed;

    public TurtleGame(int radius, int startSpeed) {
        super(Dimension.OVERWORLD.key, "Turtle Game", new PointsWinScore());
        this.radius = radius;
        this.startSpeed = startSpeed;

        this.eventNode()
            .addListener(PlayerTickEvent.class, this::onPlayerTick)
            .addListener(PlayerStartSneakingEvent.class, this::onPlayerStartSneak)
            .addListener(PlayerStopSneakingEvent.class, this::onPlayerStopSneak);
    }

    @Override
    protected void onLoad(@NotNull CompletableFuture<Void> callback) {
        for(int x = -this.radius - 1; x <= this.radius + 1; x++) {
            for(int z = -this.radius - 1; z <= this.radius + 1; z++) {
                double distance = new Pos(x, 0, z).distance(new Pos(0, 0, 0));
                if(distance <= this.radius) {
                    boolean isEdge = this.radius - 1 < distance;
                    this.setBlock(x, 0, z, Block.SAND);
                    this.setBlock(x, 1, z, isEdge ? Block.STONE : Block.AIR);
                    this.setBlock(x, 2, z, isEdge ? Block.STONE_SLAB : Block.AIR);
                } else {
                    this.setBlock(x, 0, z, Block.AIR);
                }
            }
        }
    }

    private void onPlayerStartSneak(@NotNull PlayerStartSneakingEvent event) {
        Player p = event.getPlayer();
        this.turtlePlayerMap.get(p).boostSpeed();
    }

    private void onPlayerStopSneak(@NotNull PlayerStopSneakingEvent event) {
        Player p = event.getPlayer();
        this.turtlePlayerMap.get(p).cancelBoost();
    }

    protected void onPlayerTick(@NotNull PlayerTickEvent event) {
        Player p = event.getPlayer();
        if(p.getGameMode() == GameMode.SPECTATOR) return;
        Turtle turtle = this.turtlePlayerMap.get(p);
        turtle.adaptView();
        if(this.isRunning()) {
            turtle.move();
            this.snacks.stream()
                .filter(turtle::checkCollisionWithEntity)
                .toList()
                .forEach(snack -> {
                    this.eat(p, snack);
                    this.generateNewSnack();
                    if(this.turtlePlayerMap.get(p).getScore() % 5 == 0) {
                        this.generateNewBomb();
                        this.addSpeed(0.4, p);
                    }
                });
            this.bombs.stream()
                .filter(turtle::checkCollisionWithEntity)
                .toList()
                .forEach(bomb -> {
                    this.explode(p, bomb);
                    this.generateNewBombs(2);
                    this.addGlobalSpeed(0.3);
                });
        }
    }

    protected void addSpeed(double amount, Player p) {
        this.turtlePlayerMap.get(p).addSpeed(amount);
    }

    protected void addGlobalSpeed(double amount) {
        this.turtlePlayerMap.values().forEach(turtle -> turtle.addSpeed(amount));
    }

    protected void eat(Player p, Entity snack) {
        p.playSound(Sound.sound(SoundEvent.ENTITY_GENERIC_EAT, Sound.Source.MASTER, 1f, 1f), snack.getPosition());
        Material snackMaterial = Objects.requireNonNull(this.snackBlock.registry().material());
        p.sendPacket(new ParticlePacket(Particle.ITEM.withItem(ItemStack.of(snackMaterial)), p.getPosition(), new Pos(0.5, 0.5, 0.5), 0, 8));
        this.snacks.remove(snack);
        snack.remove();
        this.turtlePlayerMap.get(p).increaseScore();
        this.turtlePlayerMap.get(p).increaseBoostChargeLevel(0.04f);
    }

    protected void explode(Player p, Entity bomb) {
        this.letPlayerLoose(p);
        p.playSound(Sound.sound(SoundEvent.ENTITY_GENERIC_EXPLODE, Sound.Source.MASTER, 2f, 1f), bomb.getPosition());
        p.playSound(Sound.sound(SoundEvent.ENTITY_GENERIC_EXPLODE, Sound.Source.MASTER, 2f, 1f), bomb.getPosition());
        p.sendPacket(new ParticlePacket(Particle.EXPLOSION, p.getPosition(), new Pos(0.5, 0.5, 0.5), 0, 8));
        if(this.getLeftPlayers().size() == 1) this.setTimeLimit(10);
        this.bombs.remove(bomb);
        bomb.remove();
    }

    protected void letPlayerLoose(Player p) {
        p.setGameMode(GameMode.SPECTATOR);
        p.setFlying(true);
        this.turtlePlayerMap.get(p).destroy();
        this.getScore().insertResult(p, this.turtlePlayerMap.get(p).getScore());
    }

    protected List<Player> getLeftPlayers() {
        return this.turtlePlayerMap.keySet().stream()
            .filter(player -> !this.getScore().hasResult(player))
            .toList();
    }

    @Override
    protected boolean onPlayerJoin(Player p) {
        this.turtlePlayerMap.putIfAbsent(p, new Turtle(p, this.startSpeed));
        p.setLevel(this.turtlePlayerMap.get(p).getScore());

        Turtle turtle = this.turtlePlayerMap.get(p);
        MinecraftServer.getSchedulerManager().scheduleNextTick(turtle::spawnTurtle);
        return super.onPlayerJoin(p);
    }

    @Override
    protected void onPlayerLeave(Player p) {
        Turtle turtle = this.turtlePlayerMap.get(p);
        turtle.remove();
    }

    @Override
    public Pos getSpawn() {
        double theta = this.rnd.nextDouble() * 2 * Math.PI;

        double spawnRadius = this.radius - 4;
        double x = spawnRadius * Math.cos(theta);
        double z = spawnRadius * Math.sin(theta);

        return new Pos(x, 1, z).withLookAt(new Pos(0, 0, 0));
    }

    private void generateNewSnacks(int count) {
        for (int i = 0; i < count; i++) {
            this.generateNewSnack();
        }
    }

    private void generateNewSnack() {
        Entity snack = new Entity(EntityType.FALLING_BLOCK);
        FallingBlockMeta meta = (FallingBlockMeta) snack.getEntityMeta();
        meta.setBlock(this.snackBlock);
        meta.setCustomName(Component.text("Snack"));
        meta.setCustomNameVisible(true);
        Pos spawnPosition = this.newSpawnPosition(snack);
        if(spawnPosition == null) {
            snack.remove();
            return;
        }
        snack.setInstance(this, spawnPosition);
        this.snacks.add(snack);
    }

    private void generateNewBombs(int count) {
        for (int i = 0; i < count; i++) {
            this.generateNewBomb();
        }
    }

    private void generateNewBomb() {
        Entity bomb = new Entity(EntityType.FALLING_BLOCK);
        FallingBlockMeta meta = (FallingBlockMeta) bomb.getEntityMeta();
        meta.setBlock(Block.TNT);
        meta.setCustomName(Component.text("Bomb").color(NamedTextColor.RED).decorate(TextDecoration.BOLD));
        meta.setCustomNameVisible(true);
        Pos spawnPosition = this.newSpawnPosition(bomb, false);
        if(spawnPosition == null) {
            bomb.remove();
            return;
        }
        bomb.setInstance(this, spawnPosition);
        this.bombs.add(bomb);
    }

    private @Nullable Pos newSpawnPosition(Entity entity) {
        return this.newSpawnPosition(entity, true);
    }

    private @Nullable Pos newSpawnPosition(Entity entity, boolean nearPlayers) {
        Pos spawnPosition;
        int counter = 0;
        boolean isInRadius, collides;
        do {
            if(counter > 200) return null;
            int x = this.rnd.nextInt(-this.radius+2, this.radius-2);
            int z = this.rnd.nextInt(-this.radius+2, this.radius-2);
            spawnPosition = new Pos(x, 1, z).add(0.5, 0, 0.5);
            Pos checkPosition = spawnPosition;
            isInRadius = checkPosition.distance(0, 1, 0) < this.radius-2;
            collides = this.getEntities().stream()
                .filter(e -> !e.equals(entity))
                .anyMatch(e -> entity.getBoundingBox().intersectBox(e.getPosition().sub(checkPosition), e.getBoundingBox()));
            if(!collides && !nearPlayers) collides = this.turtlePlayerMap.values().stream()
                    .filter(turtle -> !turtle.equals(entity))
                    .anyMatch(turtle -> entity.getBoundingBox()
                            .growSymmetrically(turtle.getBombBorder(), 1, turtle.getBombBorder())
                            .intersectBox(turtle.getPosition().sub(checkPosition), turtle.getBoundingBox()));
            counter++;
        } while (!isInRadius || collides);
        return spawnPosition;
    }

    @Override
    protected void onStart() {
        this.generateNewSnacks(this.turtlePlayerMap.size() + 1);
        this.generateNewBombs((int) Math.ceil(this.snacks.size() * 0.5));
        this.turtlePlayerMap.values().forEach(Turtle::startBoostRefill);
    }

    @Override
    protected void onStop() {
        this.getLeftPlayers().forEach(this::letPlayerLoose);
    }
}
