package land.chipmunk.chipmunkmod.modules;

import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import land.chipmunk.chipmunkmod.ChipmunkMod;
import land.chipmunk.chipmunkmod.listeners.Listener;
import land.chipmunk.chipmunkmod.listeners.ListenerManager;
import land.chipmunk.chipmunkmod.mixin.AbstractDebugChartAccessor;
import land.chipmunk.chipmunkmod.mixin.ClientLevelAccessor;
import land.chipmunk.chipmunkmod.mixin.DebugScreenOverlayAccessor;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.debugchart.PingDebugChart;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.client.multiplayer.MultiPlayerGameMode;
import net.minecraft.client.multiplayer.prediction.BlockStatePredictionHandler;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Vec3i;
import net.minecraft.core.component.DataComponentMap;
import net.minecraft.core.component.DataComponents;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.Connection;
import net.minecraft.network.protocol.game.*;
import net.minecraft.tags.BlockTags;
import net.minecraft.util.Tuple;
import net.minecraft.util.Util;
import net.minecraft.util.debugchart.SampleStorage;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.item.component.TypedEntityData;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.CommandBlock;
import net.minecraft.world.level.block.FallingBlock;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.entity.CommandBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.Property;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.NotNull;

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

public class CommandCore implements Listener {
    public static final CommandCore INSTANCE = new CommandCore(Minecraft.getInstance());

    private final Minecraft client;

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

    private final ObjectArrayList<String> pendingCommands = new ObjectArrayList<>();
    private Timer timer;
    private boolean shouldRefill = false;
    private int refillTriesUsingPlaceBlock = 0;
    private DimensionType oldDimension;

    public CommandCore (final Minecraft client) {
        this.client = client;
        reloadFromConfig();
        ListenerManager.addListener(this);
    }

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

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

        timer = new Timer();

        timer.schedule(task, 50, 50);

        if (client.player == null) return;

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

    private void tick () {
        final ClientPacketListener networkHandler = client.getConnection();

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

        reloadFromConfig();
    }

    public void reloadFromConfig () {
        noPos = ChipmunkMod.CONFIG.core.relativeArea;
        isSingleBlock = noPos.getLength().equals(Vec3i.ZERO);
    }

    @Override
    public void timeUpdate () {
        if (alreadyFilled) {
            alreadyFilled = false;
            return;
        }

        check();

        if (!shouldRefill) {
            refillTriesUsingPlaceBlock = 0;
            synchronized (pendingCommands) {
                if (pendingCommands.isEmpty()) return;
                for (final String command : pendingCommands) {
                    run(command);
                }
                pendingCommands.clear();
            }
            return;
        }

        refill();

        shouldRefill = false;
    }

    public void check () {
        if (isSingleBlock) return;

        final ClientPacketListener networkHandler = client.getConnection();

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

        final LocalPlayer player = client.player;
        final ClientLevel world = client.level;

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

        if (oldDimension != null && !oldDimension.equals(world.dimensionType())) move(client.player.getEyePosition());

        oldDimension = world.dimensionType();

        try {
            for (int x = withPos.minX(); x <= withPos.maxX(); x++) {
                for (int y = withPos.minY(); y <= withPos.maxY(); y++) {
                    for (int z = withPos.minZ(); z <= withPos.maxZ(); 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 Vec3 position) {
        final ClientLevel world = client.level;
        if (world == null || noPos == null) return;

        // also delete the old core
        if (withPos != null && !isSingleBlock) {
            pendingCommands.add(
                    String.format(
                            "fill %d %d %d %d %d %d air",

                            withPos.minX(),
                            withPos.minY(),
                            withPos.minZ(),

                            withPos.maxX(),
                            withPos.maxY(),
                            withPos.maxZ()
                    )
            );
        }

        final DimensionType dimension = world.dimensionType();

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

        origin = new BlockPos(
                ((int) position.x() / 16) * 16,
                noPos.minY() + yOffset,
                ((int) position.z() / 16) * 16
        );

        withPos = noPos.moved(origin.getX(), yOffset, origin.getZ());
        block = new BlockPos(withPos.minX(), withPos.minY(), withPos.minZ());
        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 resetIfNeeded (final Vec3 currentPosition) {
        if (origin == null) {
            move(currentPosition);
            return;
        }

        final LocalPlayer player = client.player;
        final ClientPacketListener connection = client.getConnection();
        if (player == null || connection == null) return;

        final int distanceSquared = player.chunkPosition().distanceSquared(ChunkPos.containing(origin));
        final int distance = (int) Math.sqrt(Math.abs(distanceSquared));

        final int simulationDistance = connection.getLevel().getServerSimulationDistance();

        if (distance >= simulationDistance) {
            alreadyFilled = true;
            move(currentPosition);
        }
    }

    public void refill () {
        final ClientPacketListener networkHandler = client.getConnection();
        final LocalPlayer player = client.player;

        if (
                !runFillCommand || isSingleBlock
                        || player == null
                        || !player.hasInfiniteMaterials()
                        || !player.canUseGameMasterBlocks()
                        || client.level == null
                        || networkHandler == null
                        || withPos == null
        ) return;

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

                withPos.minX(),
                withPos.minY(),
                withPos.minZ(),

                withPos.maxX(),
                withPos.maxY(),
                withPos.maxZ()
        );

        if (refillTriesUsingPlaceBlock > 5) {
            networkHandler.sendCommand(command);
            refillTriesUsingPlaceBlock = 0;
        } else {
            runPlaceBlock(command, true, true);
            refillTriesUsingPlaceBlock++;
        }
    }

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

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

        if (x > withPos.maxX()) {
            x = withPos.minX();
            z++;
            if (z > withPos.maxZ()) {
                z = withPos.minZ();
                y++;
                if (y > withPos.maxY()) {
                    y = withPos.minY();
                }
            }
        }

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

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

        if (isSingleBlock) {
            runPlaceBlock(command, false, true);
            return;
        }

        final ClientPacketListener networkHandler = client.getConnection();
        if (networkHandler == null) return;

        final Connection connection = networkHandler.getConnection();

        if (block == null) return;

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

        if (KaboomCheck.INSTANCE.isKaboom) {
            connection.send(
                    new ServerboundSetCommandBlockPacket(
                            block,
                            command,
                            CommandBlockEntity.Mode.AUTO,
                            false,
                            false,
                            true
                    )
            );
        } else {
            connection.send(
                    new ServerboundSetCommandBlockPacket(
                            block,
                            "",
                            CommandBlockEntity.Mode.REDSTONE,
                            false,
                            false,
                            false
                    )
            );

            connection.send(
                    new ServerboundSetCommandBlockPacket(
                            block,
                            command,
                            CommandBlockEntity.Mode.REDSTONE,
                            false,
                            false,
                            true
                    )
            );
        }

        incrementCurrentBlock();
    }

    public CompletableFuture<CompoundTag> runTracked (final String command) {
        final ClientPacketListener networkHandler = client.getConnection();

        if (networkHandler == null) return CompletableFuture.completedFuture(null);

        final Connection connection = networkHandler.getConnection();

        if (block == null) return CompletableFuture.completedFuture(null);

        final BlockPos oldBlock;

        if (isSingleBlock) {
            oldBlock = runPlaceBlock(command, false, false);
        } else if (KaboomCheck.INSTANCE.isKaboom) {
            connection.send(
                    new ServerboundSetCommandBlockPacket(
                            block,
                            command,
                            CommandBlockEntity.Mode.AUTO,
                            true,
                            false,
                            true
                    )
            );

            oldBlock = block;
        } else {
            connection.send(
                    new ServerboundSetCommandBlockPacket(
                            block,
                            "",
                            CommandBlockEntity.Mode.REDSTONE,
                            true,
                            false,
                            false
                    )
            );

            connection.send(
                    new ServerboundSetCommandBlockPacket(
                            block,
                            command,
                            CommandBlockEntity.Mode.REDSTONE,
                            true,
                            false,
                            true
                    )
            );

            oldBlock = block;
        }

        if (oldBlock == null) return CompletableFuture.completedFuture(null);

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

        final Timer timer = new Timer();

        final TimerTask queryTask = new TimerTask() {
            public void run () {
                client.getConnection().getDebugQueryHandler().queryBlockEntityTag(oldBlock, result -> {
                    future.complete(result);
                    if (isSingleBlock && client.gameMode != null) {
                        client.gameMode.destroyBlock(oldBlock);
                    }
                });
            }
        };

        timer.schedule(queryTask, 50 + getNextTickWaitTime());

        incrementCurrentBlock();

        return future;
    }

    public BlockPos runPlaceBlock (final String command, final boolean fallbackToChat, final boolean replaceOldBlock) {
        final ClientPacketListener networkHandler = client.getConnection();
        if (networkHandler == null) return null;

        final Connection connection = networkHandler.getConnection();

        final LocalPlayer player = client.player;
        final ClientLevel world = client.level;

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

        final DimensionType dimensionType = world.dimensionType();

        final MultiPlayerGameMode interactionManager = client.gameMode;
        if (interactionManager == null) return null;

        // stolen from two five hundred million dollars

        final Tuple<@NotNull BlockPos, @NotNull BlockState> pair = findBlockLocation();

        if (
                pair == null
                        || pair.getA().getY() < dimensionType.minY()
                        || pair.getA().getY() > dimensionType.height() + dimensionType.minY()
        ) {
            if (fallbackToChat) networkHandler.sendCommand(command);
            return null;
        }

        final BlockPos position = pair.getA();
        final BlockState oldBlockState = pair.getB();

        final int freeHotBarSlot = player.getInventory().getFreeSlot();
        if (freeHotBarSlot == -1 || freeHotBarSlot > 8) {
            if (fallbackToChat) networkHandler.sendCommand(command);
            return null;
        }
        final int slot = 36 + freeHotBarSlot;

        final int oldSelectedSlot = player.getInventory().getSelectedSlot();
        final ItemStack oldStack = player.getInventory().getItem(slot).copy();

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

        final CompoundTag data = new CompoundTag();
        data.putString("Command", command);
        data.putBoolean("auto", true);

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

        commandBlock.applyComponents(
                DataComponentMap.builder()
                        .set(DataComponents.CUSTOM_DATA, CustomData.of(blockEntityTag))
                        .set(DataComponents.BLOCK_ENTITY_DATA, TypedEntityData.of(BlockEntityType.COMMAND_BLOCK, data))
                        .build()
        );

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

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

        try (final BlockStatePredictionHandler pendingUpdateManager = ((ClientLevelAccessor) world).getBlockStatePredictionHandler()) {
            connection.send(
                    new ServerboundPlayerActionPacket(
                            ServerboundPlayerActionPacket.Action.START_DESTROY_BLOCK,
                            position,
                            Direction.UP,
                            pendingUpdateManager.startPredicting().currentSequence()
                    )
            );
            connection.send(
                    new ServerboundUseItemOnPacket(
                            InteractionHand.MAIN_HAND,
                            BlockHitResult.miss(
                                    new Vec3(position),
                                    Direction.UP,
                                    position
                            ),
                            pendingUpdateManager.startPredicting().currentSequence()
                    )
            );
        }

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

        if (ChipmunkMod.CONFIG.core.logCommands)
            ChipmunkMod.LOGGER.info("Executing place block core command: {}", command);

        if (!replaceOldBlock) return position;

        if (isSingleBlock) {
            final Timer timer = new Timer();
            timer.schedule(new TimerTask() {
                @Override
                public void run () {
                    // we're gonna just only break the block for now
                    try (final BlockStatePredictionHandler pendingUpdateManager = ((ClientLevelAccessor) world).getBlockStatePredictionHandler()) {
                        connection.send(
                                new ServerboundPlayerActionPacket(
                                        ServerboundPlayerActionPacket.Action.START_DESTROY_BLOCK,
                                        position,
                                        Direction.UP,
                                        pendingUpdateManager.startPredicting().currentSequence()
                                )
                        );
                    }
                }
            }, 50 + getNextTickWaitTime());
        } else {
            final StringBuilder oldBlockString = new StringBuilder(BuiltInRegistries.BLOCK.getKey(oldBlockState.getBlock()).toString());
            if (!oldBlockState.getProperties().isEmpty()) {
                oldBlockString.append('[');

                for (final Property<?> property : oldBlockState.getProperties()) {
                    oldBlockString.append(property.getName())
                            .append('=')
                            .append(Util.getPropertyName(property, property.getPossibleValues().getLast())) // water[level=15] instead of water[level=0,level=1,....,level=15]
                            .append(',');
                }

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

                oldBlockString.append(']');
            }

            pendingCommands.add(
                    String.format(
                            "setblock %d %d %d %s",

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

                            oldBlockString
                    )
            );
        }

        return position;
    }

    // also from two five hundred million dollars
    private Tuple<@NotNull BlockPos, @NotNull BlockState> findBlockLocation () {
        final LocalPlayer player = client.player;
        if (player == null) return null;
        final BlockPos position = player.blockPosition();
        final AABB boundingBox = player.getBoundingBox();
        boundingBox.inflate(1.0);
        for (int x = position.getX() - 2; x <= position.getX() + 2; x++) {
            for (int y = position.getY() - 2; y <= position.getY() + 2; y++) {
                for (int z = position.getZ() - 2; z <= position.getZ() + 2; z++) {
                    final BlockPos blockPos = new BlockPos(x, y, z);
                    if (!player.isWithinBlockInteractionRange(blockPos, 1.0)) continue;
                    // we don't want to place a block inside ourselves
                    if (boundingBox.intersects(new Vec3(blockPos), new Vec3(blockPos))) continue;
                    try (final Level level = player.level()) {
                        final BlockState blockState = level.getBlockState(blockPos);
                        if (blockState.getBlock() instanceof FallingBlock
                                || blockState.getBlock() instanceof CommandBlock)
                            continue;
                        final boolean replaceable = blockState.is(BlockTags.REPLACEABLE);
                        if (
                                !replaceable
                                        && !blockState.is(BlockTags.MINEABLE_WITH_AXE)
                                        && !blockState.is(BlockTags.MINEABLE_WITH_HOE)
                                        && !blockState.is(BlockTags.MINEABLE_WITH_SHOVEL)
                                        && !blockState.is(BlockTags.MINEABLE_WITH_PICKAXE)
                        ) {
                            continue;
                        }
                        return new Tuple<>(blockPos, blockState);
                    } catch (final IOException ignored) { }
                }
            }
        }

        return null;
    }

    private int getNextTickWaitTime () {
        final PingDebugChart pingChart = ((DebugScreenOverlayAccessor) client.getDebugOverlay()).getPingChart();
        final SampleStorage log = ((AbstractDebugChartAccessor) pingChart).getSampleStorage();

        long sum = 0L;

        for (int i = 0; i < log.size(); ++i) {
            sum += log.get(0);
        }

        return Math.min(Math.round((float) sum / log.size()), 5 * 1000);
    }

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

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

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