package com.xfestudio.mydimension.client.builder;

import com.mojang.blaze3d.platform.InputConstants;
import com.xfestudio.mydimension.MyDimension;
import com.xfestudio.mydimension.builder.BuilderInteractionPolicy;
import com.xfestudio.mydimension.builder.BuilderMode;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.core.BlockPos;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.InputEvent;
import net.minecraftforge.client.event.RenderGuiOverlayEvent;
import net.minecraftforge.client.event.RenderLevelStageEvent;
import net.minecraftforge.client.gui.overlay.VanillaGuiOverlay;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.eventbus.api.EventPriority;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import org.lwjgl.glfw.GLFW;

/** Self-registering client input, preview, and HUD hooks. */
@Mod.EventBusSubscriber(modid = MyDimension.MOD_ID, bus = Mod.EventBusSubscriber.Bus.FORGE, value = Dist.CLIENT)
public final class BuilderClientEvents {
    private static final BlueprintAltActionController ALT_ACTIONS = new BlueprintAltActionController();
    private static final BuilderControlTargetController CONTROL_TARGET =
            new BuilderControlTargetController();

    private BuilderClientEvents() {
    }

    public static BlueprintAltActionController altActions() {
        return ALT_ACTIONS;
    }

    @SubscribeEvent(priority = EventPriority.HIGHEST)
    public static void interaction(InputEvent.InteractionKeyMappingTriggered event) {
        Minecraft minecraft = Minecraft.getInstance();
        if (minecraft.screen != null || !BuilderClientServices.isHoldingRealmwright(minecraft)) {
            return;
        }

        if (event.isPickBlock()) {
            if (!Screen.hasAltDown() && !Screen.hasControlDown() && Screen.hasShiftDown()) {
                drain(BuilderKeyMappings.OPEN_MENU);
                drain(BuilderKeyMappings.TOGGLE_MODE);
                openMenu();
                cancel(event);
            } else if (!Screen.hasAltDown() && !Screen.hasControlDown() && !Screen.hasShiftDown()) {
                drain(BuilderKeyMappings.OPEN_MENU);
                drain(BuilderKeyMappings.TOGGLE_MODE);
                toggleMode();
                cancel(event);
            }
            return;
        }

        if (event.isUseItem() && event.getHand() == InteractionHand.MAIN_HAND) {
            if (Screen.hasAltDown() && BuilderPreviewState.get().hasBlueprintWheelActions()) {
                ALT_ACTIONS.openNavigation();
                ALT_ACTIONS.activateOrConfirm();
                cancel(event);
                return;
            }

            BuilderPreviewState preview = BuilderPreviewState.get();
            BuilderPreviewVisibility visibility = BuilderPreviewVisibility.forModifiers(
                    Screen.hasControlDown(), Screen.hasAltDown(),
                    preview.isBlueprintPreviewActive());
            BuilderClientCommand.Target commandTarget;
            if (visibility.controlGesture()) {
                commandTarget = CONTROL_TARGET.target();
                if (commandTarget == null) {
                    BlockHitResult blockTarget = extendedBlockHit(minecraft);
                    if (blockTarget == null) return;
                    commandTarget = target(blockTarget, false);
                }
                if (visibility.placesBlueprintOnUse()) {
                    BuilderClientServices.send(new BuilderClientCommand.UseTarget(commandTarget,
                            BuilderClientCommand.UseKind.PLACE_BLUEPRINT, preview.activeJobId()));
                } else if (visibility.selectsBlueprintPointOnUse()) {
                    BuilderClientServices.send(new BuilderClientCommand.SelectBlueprintPoint(commandTarget));
                }
                CONTROL_TARGET.consumeCurrent();
            } else {
                BuilderPreviewState.Cell virtualTarget = preview.virtualTarget();
                if (virtualTarget != null) {
                    // Resuming is job based; a yellow cell is a real custom-ray target even when
                    // there is no vanilla block behind it for player.pick() to return.
                    commandTarget = new BuilderClientCommand.Target(virtualTarget.pos(),
                            net.minecraft.core.Direction.UP,
                            net.minecraft.world.phys.Vec3.atCenterOf(virtualTarget.pos()),
                            false, true);
                    BuilderClientServices.send(new BuilderClientCommand.UseTarget(commandTarget,
                            BuilderClientCommand.UseKind.RESUME_MISSING, preview.activeJobId()));
                    cancel(event);
                    return;
                }
                BlockHitResult blockTarget = extendedBlockHit(minecraft);
                if (blockTarget == null) return;
                commandTarget = target(blockTarget, false);
                BuilderClientCommand.UseKind kind = preview.isBlueprintPreviewActive()
                        ? BuilderClientCommand.UseKind.PLACE_BLUEPRINT
                        : BuilderClientCommand.UseKind.AUTO;
                if (kind == BuilderClientCommand.UseKind.AUTO && !Screen.hasShiftDown()) {
                    boolean sameVanillaTarget = sameVanillaBlockTarget(
                            minecraft.hitResult, blockTarget);
                    if (BuilderInteractionPolicy.prioritizesBlock(
                            minecraft.level.getBlockState(blockTarget.getBlockPos()))) {
                        // Let vanilla call the block first. If it returns PASS, the ordinary item
                        // policy below still refuses the scepter. During a one-frame reach mismatch,
                        // cancel instead of accidentally using a different cached vanilla target.
                        if (!sameVanillaTarget) cancel(event);
                        return;
                    }
                    if (sameVanillaTarget) {
                        // A non-interactive block returns PASS and naturally reaches Item#useOn.
                        return;
                    }
                    // The configured snapshot and the reach attribute normally agree. If they
                    // are briefly out of sync, retain extended-range use only for a non-interactive
                    // surface; an interaction-priority target is never sent through this fallback.
                }
                BuilderClientServices.send(new BuilderClientCommand.UseTarget(
                        commandTarget, kind, preview.activeJobId(), Screen.hasShiftDown()));
            }
            cancel(event);
            return;
        }

        if (event.isAttack()) {
            BuilderPreviewState preview = BuilderPreviewState.get();
            BuilderPreviewVisibility visibility = BuilderPreviewVisibility.forModifiers(
                    Screen.hasControlDown(), Screen.hasAltDown(),
                    preview.isBlueprintPreviewActive());
            BuilderPreviewState.Focus focus = preview.focus();
            if (focus != null && !visibility.showsFocus(focus.kind())) return;
            BuilderPreviewState.CancelTarget cancelTarget = preview.focusedCancelTarget();
            if (cancelTarget == null) return;
            switch (cancelTarget) {
                case DEPLOYMENT -> {
                    BuilderClientNetworkBridge.cancelPlacementPreview();
                }
                case SELECTION -> BuilderClientNetworkBridge.cancelSourceSelection();
                case MISSING -> {
                    BuilderClientServices.send(new BuilderClientCommand.CancelActive(preview.activeJobId()));
                    preview.clearMissingPreview();
                }
            }
            cancel(event);
        }
    }

    @SubscribeEvent(priority = EventPriority.HIGHEST)
    public static void scroll(InputEvent.MouseScrollingEvent event) {
        Minecraft minecraft = Minecraft.getInstance();
        if (minecraft.screen != null || !BuilderClientServices.isHoldingRealmwright(minecraft)) {
            return;
        }
        if (Screen.hasControlDown() && !Screen.hasAltDown()) {
            int reach = Math.max(1, BuilderClientServices.snapshot().reach());
            if (CONTROL_TARGET.scroll(minecraft, event.getScrollDelta(), reach)) {
                BuilderClientNetworkBridge.requestBlueprintPreviewRefresh();
                event.setCanceled(true);
            }
            return;
        }
        if (!Screen.hasAltDown() || !BuilderPreviewState.get().hasBlueprintWheelActions()) return;
        ALT_ACTIONS.openNavigation();
        if (ALT_ACTIONS.scroll(event.getScrollDelta())) {
            event.setCanceled(true);
        }
    }

    @SubscribeEvent
    public static void key(InputEvent.Key event) {
        if ((event.getKey() == GLFW.GLFW_KEY_LEFT_ALT || event.getKey() == GLFW.GLFW_KEY_RIGHT_ALT)
                && event.getAction() == InputConstants.RELEASE) {
            ALT_ACTIONS.closeOnAltRelease();
        }
        if ((event.getKey() == GLFW.GLFW_KEY_LEFT_CONTROL
                || event.getKey() == GLFW.GLFW_KEY_RIGHT_CONTROL)
                && event.getAction() == InputConstants.RELEASE) {
            CONTROL_TARGET.reset();
        }
    }

    @SubscribeEvent
    public static void tick(TickEvent.ClientTickEvent event) {
        if (event.phase != TickEvent.Phase.END) {
            return;
        }
        Minecraft minecraft = Minecraft.getInstance();
        BuilderAnchorPreviewTracker.tick(minecraft);
        if (!BuilderClientServices.isHoldingRealmwright(minecraft)) {
            drain(BuilderKeyMappings.TOGGLE_MODE);
            drain(BuilderKeyMappings.OPEN_MENU);
            drain(BuilderKeyMappings.UNDO);
            drain(BuilderKeyMappings.REDO);
            ALT_ACTIONS.reset();
            CONTROL_TARGET.reset();
            BuilderPreviewState.get().clearHover();
            BuilderSurfacePreviewPlanner.reset();
            BuilderPreviewRenderer.clearCache();
            return;
        }
        if (minecraft.screen != null) {
            drain(BuilderKeyMappings.TOGGLE_MODE);
            drain(BuilderKeyMappings.OPEN_MENU);
            drain(BuilderKeyMappings.UNDO);
            drain(BuilderKeyMappings.REDO);
            ALT_ACTIONS.reset();
            CONTROL_TARGET.reset();
            BuilderPreviewState.get().clearHover();
            BuilderSurfacePreviewPlanner.reset();
            // Menus temporarily pause targeting, but the immutable preview and its GPU cache stay
            // alive behind the screen. Closing a menu therefore cannot restart from an empty VBO.
            BuilderPreviewRenderer.tick(minecraft);
            return;
        }

        while (BuilderKeyMappings.OPEN_MENU.consumeClick()) {
            // Forge records every mapping sharing a mouse button before applying modifiers.
            // Consume the Shift binding on every middle click, but act only for exact Shift.
            if (Screen.hasShiftDown() && !Screen.hasAltDown() && !Screen.hasControlDown()) {
                drain(BuilderKeyMappings.TOGGLE_MODE);
                openMenu();
            }
        }
        while (BuilderKeyMappings.TOGGLE_MODE.consumeClick()) {
            if (!Screen.hasShiftDown() && !Screen.hasAltDown() && !Screen.hasControlDown()) {
                toggleMode();
            }
        }
        while (BuilderKeyMappings.UNDO.consumeClick()) {
            BuilderClientServices.send(new BuilderClientCommand.Undo());
        }
        while (BuilderKeyMappings.REDO.consumeClick()) {
            BuilderClientServices.send(new BuilderClientCommand.Redo());
        }

        int reach = Math.max(1, BuilderClientServices.snapshot().reach());
        CONTROL_TARGET.tick(minecraft, reach);
        BuilderPreviewState preview = BuilderPreviewState.get();
        BuilderPreviewVisibility visibility = BuilderPreviewVisibility.forModifiers(
                Screen.hasControlDown(), Screen.hasAltDown(),
                preview.isBlueprintPreviewActive());
        if (!visibility.pausesSurfacePlanning()) {
            BuilderSurfacePreviewPlanner.update(minecraft);
        }
        preview.updateHoveredTarget(minecraft, 1.0F, reach, visibility);
        BuilderPreviewRenderer.tick(minecraft);
    }

    @SubscribeEvent
    public static void renderWorld(RenderLevelStageEvent event) {
        BuilderPreviewRenderer.render(event);
    }

    @SubscribeEvent
    public static void renderHud(RenderGuiOverlayEvent.Post event) {
        if (event.getOverlay().id().equals(VanillaGuiOverlay.HOTBAR.id())) {
            BuilderHudRenderer.render(event.getGuiGraphics(), event.getWindow(), ALT_ACTIONS);
        }
    }

    private static void toggleMode() {
        BuilderMode next = BuilderClientServices.snapshot().mode().toggle();
        BuilderClientServices.send(new BuilderClientCommand.SetMode(next));
    }

    private static void openMenu() {
        BuilderClientServices.send(new BuilderClientCommand.OpenMenu());
        BuilderClientServices.openToolScreen();
    }

    private static void cancel(InputEvent.InteractionKeyMappingTriggered event) {
        event.setSwingHand(false);
        event.setCanceled(true);
    }

    private static void drain(net.minecraft.client.KeyMapping mapping) {
        while (mapping.consumeClick()) {
            // Prevent a default mouse click handled above from toggling again on the next tick.
        }
    }

    private static BlockHitResult extendedBlockHit(Minecraft minecraft) {
        if (minecraft.player == null) {
            return null;
        }
        HitResult result = minecraft.player.pick(Math.max(1, BuilderClientServices.snapshot().reach()), 1.0F, false);
        return result instanceof BlockHitResult blockHit && result.getType() == HitResult.Type.BLOCK
                ? blockHit : null;
    }

    static BuilderClientCommand.Target target(BlockHitResult result, boolean direct) {
        BlockPos pos = result.getBlockPos();
        return new BuilderClientCommand.Target(pos, result.getDirection(), result.getLocation(),
                result.isInside(), direct);
    }

    static boolean sameVanillaBlockTarget(HitResult vanillaTarget, BlockHitResult requested) {
        return vanillaTarget instanceof BlockHitResult blockTarget
                && vanillaTarget.getType() == HitResult.Type.BLOCK
                && blockTarget.getBlockPos().equals(requested.getBlockPos())
                && blockTarget.getDirection() == requested.getDirection()
                && blockTarget.isInside() == requested.isInside();
    }

    @javax.annotation.Nullable
    static BuilderClientCommand.Target airPlacementTarget() {
        return CONTROL_TARGET.isAirMode() ? CONTROL_TARGET.target() : null;
    }

    static void updateControlTargetForRender(Minecraft minecraft) {
        CONTROL_TARGET.tick(minecraft,
                Math.max(1, BuilderClientServices.snapshot().reach()));
    }
}
