package eu.mhsl.minecraft.pixelblocks.pixelblock;

import co.aikar.taskchain.TaskChain;
import eu.mhsl.minecraft.pixelblocks.Main;
import eu.mhsl.minecraft.pixelblocks.PixelBlockItem;
import eu.mhsl.minecraft.pixelblocks.utils.Direction;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.Sound;
import org.bukkit.World;
import org.bukkit.entity.Item;
import org.bukkit.entity.Player;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.UUID;

public class PixelBlock {
    private boolean isAccessible = false;
    private PixelBlockWorld pixelWorld;

    private final Location pixelBlockLocation;
    private final Direction facingDirection;
    private Pixels pixels;
    private List<PixelBlockWorld.PixelData> pixelData;

    private PixelBlockHitbox hitbox;
    private PixelBlockPlaceholder placeholder;

    private Location lastEntryLocation;
    private final UUID ownerUUID;
    private final UUID blockUUID;

    public static @Nullable PixelBlock getPixelBlockFromBlockWorld(World world) {
        UUID worldUUID = PixelBlockWorld.getUUIDFromWorld(world);
        if(worldUUID == null) return null;
        return Main.pixelBlocks.stream()
            .filter(block -> block.blockUUID.equals(worldUUID))
            .findFirst()
            .orElse(null);
    }

    public static boolean exists(@NotNull UUID blockUUID) {
        return Main.pixelBlocks.stream().anyMatch(pixelBlock -> pixelBlock.blockUUID.equals(blockUUID));
    }

    public static @Nullable PixelBlock getPixelBlockFromPlacedLocation(@NotNull Location placedLocation) {
        Location searchLocation = placedLocation.clone().toBlockLocation();
        searchLocation.setPitch(0);
        searchLocation.setYaw(0);

        return Main.pixelBlocks.stream()
            .filter(block -> Objects.equals(block.pixelBlockLocation, searchLocation))
            .findFirst()
            .orElse(null);
    }

    public static PixelBlock fromExisting(UUID blockUUID, UUID ownerUUID, Location pixelBlockLocation, Direction direction, Location lastEntryLocation) {
        return new PixelBlock(blockUUID, ownerUUID, pixelBlockLocation, direction, lastEntryLocation);
    }

    private PixelBlock(UUID blockUUID, UUID ownerUUID, Location pixelBlockLocation, Direction direction, Location lastEntryLocation) {
        this.blockUUID = blockUUID;
        this.ownerUUID = ownerUUID;
        this.pixelBlockLocation = pixelBlockLocation;
        this.pixelBlockLocation.setYaw(0);
        this.pixelBlockLocation.setPitch(0);
        this.facingDirection = direction;
        this.lastEntryLocation = lastEntryLocation;

        this.pixelWorld = new PixelBlockWorld(this);
        this.pixelData = this.pixelWorld.getPixels(this.facingDirection);
        this.pixels = new Pixels(this);
        this.placeholder = new PixelBlockPlaceholder(this);
        this.hitbox = new PixelBlockHitbox(this);

        this.getBlockTaskChain().sync(() -> this.isAccessible = true).execute();

        Main.logger().info(String.format("Loaded existing pixelblock '%s'", this.blockUUID));
    }

    public static PixelBlock createPixelBlock(UUID blockUUID, UUID ownerUUID, Location pixelBlockLocation, Direction direction) {
        return new PixelBlock(blockUUID, ownerUUID, pixelBlockLocation, direction);
    }

    private PixelBlock(UUID blockUUID, UUID ownerUUID, Location pixelBlockLocation, Direction direction) {
        if(exists(blockUUID))
            throw new IllegalStateException(String.format("PixelBlock '%s' already exists in the world!", blockUUID));

        this.blockUUID = blockUUID;
        this.ownerUUID = ownerUUID;
        this.pixelBlockLocation = pixelBlockLocation;
        this.pixelBlockLocation.setYaw(0);
        this.pixelBlockLocation.setPitch(0);
        this.facingDirection = direction;

        this.getBlockTaskChain()
            .sync(() -> {
                this.pixelWorld = new PixelBlockWorld(this);
                this.pixelData = this.pixelWorld.getPixels(this.facingDirection);
                this.pixels = new Pixels(this);
                this.placeholder = new PixelBlockPlaceholder(this);
                this.hitbox = new PixelBlockHitbox(this);
            })
            .execute();

        this.scheduleEntityUpdate();

        Main.database().savePixelBlock(this);
        Main.pixelBlocks.add(this);

        this.getBlockTaskChain().sync(() -> this.isAccessible = true).execute();
    }

    public <T> TaskChain<T> getBlockTaskChain() {
        return Main.sharedChain(this.blockUUID.toString());
    }

    public void enterBlock(@NotNull Player player) {
        if(Main.configuration().onlyEditableByOwner() && !player.getUniqueId().equals(ownerUUID)) {
            player.sendMessage(Component.translatable("pixelblocks.error.not-owner").color(NamedTextColor.RED));
            return;
        }

        this.lastEntryLocation = player.getLocation();
        Main.database().savePixelBlock(this);

        getBlockTaskChain()
            .sync(() -> {
                if(!this.isAccessible) return;
                player.teleport(this.pixelWorld.getSpawnLocation());
            })
            .current(() -> Main.logger()
                .info(String.format("'%s' entered PixelBlock '%s' at %s", player.getName(), this.blockUUID, this.pixelBlockLocation.toString())))
            .execute();
    }

    public void exitBlock(@NotNull Player player) {
        this.getBlockTaskChain()
            .sync(() -> player.teleport(this.getReturnLocation()))
            .sync(() -> this.pixelData = this.pixelWorld.getPixels(this.facingDirection))
            .current(() -> Main.logger().info(String.format("%s exited PixelBlock", player.getName())))
            .delay(1)
            .execute();

        this.scheduleEntityUpdate();
    }

    private void scheduleEntityUpdate() {
        this.getBlockTaskChain()
            .sync(this::ensureChunksLoaded)
            .sync(this::removeEntities)
            .delay(1)
            .async(() -> Collections.shuffle(this.pixelData))
            .delay(1)
            .sync(this::ensureChunksLoaded)
            .syncLast((pixelData) -> {
                this.pixels.spawn();
                this.hitbox.spawn();
                this.placeholder.spawn();
            })
            .execute();
    }

    public void destroy(Player destroyedBy, boolean force) {
        if(!this.isAccessible) return;
        if(!force && Main.configuration().onlyBreakableByOwner() && !destroyedBy.getUniqueId().equals(ownerUUID)) {
            destroyedBy.sendMessage(Component.translatable("pixelblocks.error.not-owner").color(NamedTextColor.RED));
            return;
        }

        Location returnLocation = this.getReturnLocation();
        this.pixelWorld.getPlayersInWorld().forEach(p -> {
            p.sendMessage(Component.translatable("pixelblocks.info.destroyed-by-other").color(NamedTextColor.RED));
            p.teleport(returnLocation);
        });

        Main.logger().info(String.format("Destroying PixelBlock '%s' at %s", this.blockUUID, pixelBlockLocation));
        this.isAccessible = false;

        this.pixelWorld.getEntitiesInWorld().stream()
            .filter(entity -> entity instanceof Item)
            .forEach(entity -> entity.teleport(returnLocation));

        this.getBlockTaskChain()
            .sync(() -> {
                this.removeEntities();
                World world = this.pixelBlockLocation.getWorld();
                world.playSound(this.pixelBlockLocation, Sound.BLOCK_COPPER_BULB_BREAK, 1.0F, 2.0F);
                world.dropItem(this.getPixelBlockLocation().add(new Vector(0.5, 0.5, 0.5)), PixelBlockItem.getBlockAsItem(this, destroyedBy.locale()));
            })
            .execute();

        Main.database().deletePixelBlock(this);
        Main.pixelBlocks.remove(this);
    }

    private void removeEntities() {
        this.ensureChunksLoaded();
        this.pixels.destroy();
        this.placeholder.destroy();
        this.hitbox.destroy();
    }

    private void ensureChunksLoaded() {
        Chunk chunk = this.pixelBlockLocation.getChunk();
        if(!chunk.isLoaded() || !chunk.isEntitiesLoaded()) {
            Main.logger().info(String.format("Loading chunk '%d, %d' for pixelblock '%s'", chunk.getX(), chunk.getZ(), this.blockUUID));
            chunk.load();
            chunk.getEntities();
        }
    }

    public @NotNull PixelBlockWorld getPixelWorld() {
        return pixelWorld;
    }

    public Location getPixelBlockLocation() {
        return pixelBlockLocation.clone();
    }

    public Direction getFacingDirection() {
        return facingDirection;
    }

    public boolean hasLastEntryLocation() {
        return this.lastEntryLocation != null;
    }

    private @NotNull Location getReturnLocation() {
        return this.hasLastEntryLocation()
            ? this.lastEntryLocation.clone()
            : this.getPixelBlockLocation().add(0.5, 0, 0.5);
    }

    public List<PixelBlockWorld.PixelData> getPixelData() {
        return pixelData;
    }

    public Location getLastEntryLocation() {
        return lastEntryLocation.clone();
    }

    public UUID getOwnerUUID() {
        return ownerUUID;
    }

    public UUID getBlockUUID() {
        return blockUUID;
    }
}
