package land.chipmunk.chipmunkmod.modules;

import land.chipmunk.chipmunkmod.ChipmunkMod;
import land.chipmunk.chipmunkmod.listeners.Listener;
import land.chipmunk.chipmunkmod.listeners.ListenerManager;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.block.CommandBlock;
import net.minecraft.block.entity.CommandBlockBlockEntity;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.ClientPlayNetworkHandler;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.client.network.ClientPlayerInteractionManager;
import net.minecraft.client.world.ClientWorld;
import net.minecraft.component.ComponentMap;
import net.minecraft.component.DataComponentTypes;
import net.minecraft.component.type.NbtComponent;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.network.ClientConnection;
import net.minecraft.network.packet.c2s.play.CreativeInventoryActionC2SPacket;
import net.minecraft.network.packet.c2s.play.UpdateCommandBlockC2SPacket;
import net.minecraft.network.packet.c2s.play.UpdateSelectedSlotC2SPacket;
import net.minecraft.registry.Registries;
import net.minecraft.state.property.Property;
import net.minecraft.util.Hand;
import net.minecraft.util.Util;
import net.minecraft.util.hit.BlockHitResult;
import net.minecraft.util.math.*;
import net.minecraft.world.chunk.Chunk;
import net.minecraft.world.dimension.DimensionType;

import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CompletableFuture;

public class CommandCore {
    public static CommandCore INSTANCE = new CommandCore(MinecraftClient.getInstance());

    private final MinecraftClient client;

    public boolean ready = false;
    public BlockPos origin;
    public BlockBox noPos;
    public BlockBox withPos;
    public BlockPos block;
    public boolean runFillCommand = true;
    public boolean alreadyFilled = false;

    private Timer timer;
    private boolean shouldRefill = false;
    private DimensionType oldDimension;

    public CommandCore (final MinecraftClient client) {
        this.client = client;
        reloadRelativeArea();
    }

    public void init () {
        if (timer != null) cleanup();

        final TimerTask task = new TimerTask() {
            public void run () {
                tick();
            }
        };

        final TimerTask refillTask = new TimerTask() {
            @Override
            public void run () {
                if (alreadyFilled) {
                    alreadyFilled = false;
                    return;
                }

                check();

                if (!shouldRefill) return;

                refill();

                shouldRefill = false;
            }
        };

        timer = new Timer();

        timer.schedule(task, 50, 50);

        timer.schedule(refillTask, 50, 1000);

        if (client.player == null) return;

        move(client.player.getPos());
    }

    private void tick () {
        final ClientPlayNetworkHandler networkHandler = client.getNetworkHandler();

        if (networkHandler == null) {
            cleanup();
            return;
        }

        reloadRelativeArea();
    }

    public void reloadRelativeArea () {
        noPos = ChipmunkMod.CONFIG.core.relativeArea;
    }

    public void check () {
        final ClientPlayNetworkHandler networkHandler = client.getNetworkHandler();

        if (networkHandler == null || withPos == null || !ready) return;

        final ClientPlayerEntity player = client.player;
        final ClientWorld world = client.world;

        if (player == null || world == null) return;

        if (oldDimension != null && !oldDimension.equals(world.getDimension())) move(client.player.getPos());

        oldDimension = world.getDimension();

        try {
            for (int x = withPos.getMinX(); x <= withPos.getMaxX(); x++) {
                for (int y = withPos.getMinY(); y <= withPos.getMaxY(); y++) {
                    for (int z = withPos.getMinZ(); z <= withPos.getMaxZ(); z++) {
                        final BlockPos pos = new BlockPos(x, y, z);

                        final Block block = world.getBlockState(pos).getBlock();

                        if (block instanceof CommandBlock) continue;

                        shouldRefill = true;

                        return;
                    }
                }
            }
        } catch (final Exception e) {
            ChipmunkMod.LOGGER.error("Error while checking if core needs refilling", e);
        }
    }

    public void move (final Vec3d position) {
        final ClientWorld world = client.world;
        if (world == null || noPos == null) return;

        final DimensionType dimension = world.getDimension();

        final int dimMinY = dimension.minY();
        final int dimMaxY = dimension.height() + dimMinY - 1; // -1 accounts for block at Y=0
        int yOffset = 0;
        if (noPos.getMinY() < dimMinY) {
            yOffset = dimMinY - noPos.getMinY();
        } else if (noPos.getMaxY() > dimMaxY) {
            yOffset = dimMaxY - noPos.getMaxY();
        }

        origin = new BlockPos(
                ((int) position.getX() / 16) * 16,
                noPos.getMinY() + yOffset,
                ((int) position.getZ() / 16) * 16
        );

        withPos = noPos.offset(origin.getX(), yOffset, origin.getZ());
        block = new BlockPos(withPos.getMinX(), withPos.getMinY(), withPos.getMinZ());
        refill();

        final Timer timer = new Timer();

        final TimerTask task = new TimerTask() {
            @Override
            public void run () {
                for (final Listener listener : ListenerManager.listeners) listener.coreMoved();
                if (!ready) {
                    ready = true;

                    for (final Listener listener : ListenerManager.listeners) listener.coreReady();
                }
            }
        };

        timer.schedule(task, 75);
    }

    public void refill () {
        final ClientPlayNetworkHandler networkHandler = client.getNetworkHandler();

        if (!runFillCommand || client.world == null || networkHandler == null || withPos == null) return;

        final Chunk chunk = client.world.getChunk(withPos.getCenter());

        if (chunk == null) return;

        final String command = String.format(
                "fill %s %s %s %s %s %s command_block",

                withPos.getMinX(),
                withPos.getMinY(),
                withPos.getMinZ(),

                withPos.getMaxX(),
                withPos.getMaxY(),
                withPos.getMaxZ()
        );

        runPlaceBlock(command);
    }

    public void incrementCurrentBlock () {
        if (withPos == null) return;

        int x = block.getX() + 1;
        int y = block.getY();
        int z = block.getZ();

        if (x > withPos.getMaxX()) {
            x = withPos.getMinX();
            z++;
            if (z > withPos.getMaxZ()) {
                z = withPos.getMinZ();
                y++;
                if (y > withPos.getMaxY()) {
                    y = withPos.getMinY();
                }
            }
        }

        block = new BlockPos(x, y, z);
    }

    public void run (final String command) {
        if (command.length() > 32767) return;

        final ClientPlayNetworkHandler networkHandler = client.getNetworkHandler();

        if (networkHandler == null) return;

        final ClientConnection connection = networkHandler.getConnection();

        if (block == null) return;

        ChipmunkMod.LOGGER.info("Executing core command: {}", command);

        if (KaboomCheck.INSTANCE.isKaboom) {
            connection.send(
                    new UpdateCommandBlockC2SPacket(
                            block,
                            command,
                            CommandBlockBlockEntity.Type.AUTO,
                            false,
                            false,
                            true
                    )
            );
        } else {
            connection.send(
                    new UpdateCommandBlockC2SPacket(
                            block,
                            "",
                            CommandBlockBlockEntity.Type.REDSTONE,
                            false,
                            false,
                            false
                    )
            );

            connection.send(
                    new UpdateCommandBlockC2SPacket(
                            block,
                            command,
                            CommandBlockBlockEntity.Type.REDSTONE,
                            false,
                            false,
                            true
                    )
            );
        }

        incrementCurrentBlock();
    }

    public CompletableFuture<NbtCompound> runTracked (final String command) {
        final ClientPlayNetworkHandler networkHandler = client.getNetworkHandler();

        if (networkHandler == null) return new CompletableFuture<>();

        final ClientConnection connection = networkHandler.getConnection();

        if (block == null) return new CompletableFuture<>();

        if (KaboomCheck.INSTANCE.isKaboom) {
            connection.send(
                    new UpdateCommandBlockC2SPacket(
                            block,
                            command,
                            CommandBlockBlockEntity.Type.AUTO,
                            true,
                            false,
                            true
                    )
            );
        } else {
            connection.send(
                    new UpdateCommandBlockC2SPacket(
                            block,
                            "",
                            CommandBlockBlockEntity.Type.REDSTONE,
                            true,
                            false,
                            false
                    )
            );

            connection.send(
                    new UpdateCommandBlockC2SPacket(
                            block,
                            command,
                            CommandBlockBlockEntity.Type.REDSTONE,
                            true,
                            false,
                            true
                    )
            );
        }

        final CompletableFuture<NbtCompound> future = new CompletableFuture<>();

        final Timer timer = new Timer();

        final BlockPos oldBlock = block;

        final TimerTask queryTask = new TimerTask() {
            public void run () {
                client.getNetworkHandler().getDataQueryHandler().queryBlockNbt(oldBlock, future::complete);
            }
        };

        timer.schedule(queryTask, 50);

        incrementCurrentBlock();

        return future;
    }

    public void runPlaceBlock (final String command) {
        final ClientPlayNetworkHandler networkHandler = client.getNetworkHandler();
        if (networkHandler == null) return;

        final ClientConnection connection = networkHandler.getConnection();

        final ClientPlayerEntity player = client.player;
        final ClientWorld world = client.world;

        if (player == null || world == null || !player.isInCreativeMode()) return;

        final DimensionType dimensionType = world.getDimension();

        if (
                player.getPos().getY() < dimensionType.minY()
                    || player.getPos().getY() > dimensionType.height() + dimensionType.minY()
        ) {
            networkHandler.sendChatCommand(command);
            return;
        }

        final ClientPlayerInteractionManager interactionManager = client.interactionManager;
        if (interactionManager == null) return;

        final float yaw = player.getYaw();
        final float pitch = player.getPitch();

        // Anti Block when flying around
        final BlockPos position = player.getBlockPos().add(
                (int) Math.round(3 * Math.cos(Math.toRadians(yaw))),
                3 * (pitch < 0 ? -1 : 1),
                (int) Math.round(3 * Math.sin(Math.toRadians(yaw)))
        );

        // stolen from two five hundred million dollars
        final BlockState oldBlockState = world.getBlockState(position);

        final int freeHotBarSlot = player.getInventory().getEmptySlot();
        final int slot = 36 + freeHotBarSlot;

        final int oldSelectedSlot = player.getInventory().selectedSlot;
        final ItemStack oldStack = player.getInventory().getStack(slot).copy();

        final ItemStack commandBlock = new ItemStack(Items.COMMAND_BLOCK);

        final NbtCompound data = new NbtCompound();
        data.putString("id", "minecraft:command_block");
        data.putString("Command", command);
        data.putBoolean("auto", true);

        final NbtCompound blockEntityTag = new NbtCompound();
        blockEntityTag.put("BlockEntityTag", data);

        commandBlock.applyComponentsFrom(
                ComponentMap.builder()
                        .add(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(blockEntityTag))
                        .add(DataComponentTypes.BLOCK_ENTITY_DATA, NbtComponent.of(data))
                        .build()
        );

        connection.send(new CreativeInventoryActionC2SPacket(slot, commandBlock));

        if (oldSelectedSlot != freeHotBarSlot) {
            connection.send(new UpdateSelectedSlotC2SPacket(freeHotBarSlot));
        }

        interactionManager.breakBlock(position);
        interactionManager.interactBlock(
                player,
                Hand.MAIN_HAND,
                new BlockHitResult(
                        new Vec3d(position),
                        Direction.UP,
                        position,
                        false
                )
        );

        if (oldSelectedSlot != freeHotBarSlot) {
            connection.send(new UpdateSelectedSlotC2SPacket(oldSelectedSlot));
        }
        connection.send(new CreativeInventoryActionC2SPacket(slot, oldStack));

        final Timer timer = new Timer();

        final TimerTask task = new TimerTask() {
            @Override
            public void run () {
                final StringBuilder command = new StringBuilder(
                        String.format(
                                "setblock %d %d %d %s",

                                position.getX(),
                                position.getY(),
                                position.getZ(),

                                Registries.BLOCK.getId(oldBlockState.getBlock())
                        )
                );

                if (!oldBlockState.getProperties().isEmpty()) {
                    command.append('[');

                    for (final Property<?> property : oldBlockState.getProperties()) {
                        command.append(property.getName())
                                .append('=')
                                .append(Util.getValueAsString(property, property.getType()))
                                .append(',');
                    }

                    command.deleteCharAt(command.length() - 1);

                    command.append(']');
                }

                CommandCore.this.run(command.toString());
            }
        };

        timer.schedule(task, 100); // assuming the core has already been filled at this point
    }

    public void cleanup () {
        if (timer == null) return;

        timer.cancel();
        timer.purge();

        withPos = null;
        block = null;
        ready = false;
    }
}
