package com.xfestudio.mydimension.client.builder;

import com.xfestudio.mydimension.builder.BuilderMode;
import com.xfestudio.mydimension.builder.BuilderBlockCompatibility;
import com.xfestudio.mydimension.builder.BuilderInteractionPolicy;
import com.xfestudio.mydimension.builder.BuilderReplacementPolicy;
import com.xfestudio.mydimension.builder.BuilderTags;
import com.xfestudio.mydimension.builder.SurfacePlaneTraversal;
import com.xfestudio.mydimension.builder.SurfaceMatchMode;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;

import java.util.ArrayList;
import java.util.List;

/** Lightweight client prediction; the server still recomputes and authorizes every target. */
public final class BuilderSurfacePreviewPlanner {
    private static final int VALIDATION_CELLS_PER_TICK = 256;
    private static final int TARGET_MISS_GRACE_TICKS = 2;
    private static PreviewKey lastKey;
    private static boolean showingSurfacePreview;
    private static BuilderPreviewState.Snapshot lastPlannedSnapshot;
    private static List<BuilderPreviewState.Cell> lastCells = List.of();
    private static int validationCursor;
    private static Direction lastFace;
    private static BlockState lastOverride;
    private static int targetMissTicks;

    private BuilderSurfacePreviewPlanner() { }

    public static void update(Minecraft minecraft) {
        if (minecraft.player == null || minecraft.level == null) return;
        BuilderPreviewState.Snapshot workflow = BuilderPreviewState.get().snapshot();
        // A movable deployment owns the placement target. Source selections and material-waiting
        // jobs are independent overlays and must not suppress ordinary surface prediction.
        if (workflow != null && workflow.blueprintPreview()) {
            reset();
            return;
        }
        BuilderClientSnapshot settings = BuilderClientServices.snapshot();
        HitResult picked = minecraft.player.pick(settings.reach(), 1.0F, false);
        if (!(picked instanceof BlockHitResult hit) || picked.getType() != HitResult.Type.BLOCK) {
            if (showingSurfacePreview && ++targetMissTicks < TARGET_MISS_GRACE_TICKS) return;
            if (showingSurfacePreview) BuilderPreviewState.get().clearSurfacePreview();
            reset();
            return;
        }
        targetMissTicks = 0;
        BlockState targetState = minecraft.level.getBlockState(hit.getBlockPos());
        if (!BuilderInteractionPolicy.permitsScepter(Screen.hasShiftDown(), targetState)) {
            // Interaction-priority targets hide a previous surface preview immediately. The
            // ordinary right-click path is then free to reach the block itself.
            BuilderPreviewState.get().clearSurfacePreview();
            reset();
            return;
        }
        BlockState override = minecraft.player.getOffhandItem().getItem() instanceof BlockItem blockItem
                ? blockItem.getBlock().defaultBlockState() : null;
        PreviewKey key = new PreviewKey(minecraft.level, hit.getBlockPos().immutable(), hit.getDirection(),
                targetState, settings.mode(), settings.surfaceMatch(),
                settings.buildLimit(), settings.demolishLimit(), settings.allowReplacement(),
                minecraft.player.isCreative(), override,
                minecraft.level.getBlockState(hit.getBlockPos().relative(hit.getDirection())));
        boolean snapshotReplaced = showingSurfacePreview && workflow != lastPlannedSnapshot;
        if (key.equals(lastKey) && !snapshotReplaced
                && !hasPreviewDrift(minecraft.level, key)) return;

        List<BuilderPreviewState.Cell> cells = plan(minecraft.level, hit, settings, override);
        BuilderPreviewState.get().updateSurfacePreview(minecraft.level.dimension(), cells);
        lastKey = key;
        showingSurfacePreview = true;
        lastPlannedSnapshot = BuilderPreviewState.get().snapshot();
        lastCells = cells;
        validationCursor = 0;
        lastFace = hit.getDirection();
        lastOverride = override;
    }

    public static void reset() {
        lastKey = null;
        showingSurfacePreview = false;
        lastPlannedSnapshot = null;
        lastCells = List.of();
        validationCursor = 0;
        lastFace = null;
        lastOverride = null;
        targetMissTicks = 0;
    }

    private static List<BuilderPreviewState.Cell> plan(ClientLevel level, BlockHitResult hit,
                                                        BuilderClientSnapshot settings,
                                                        BlockState override) {
        BlockState seed = level.getBlockState(hit.getBlockPos());
        if (!SurfacePlaneTraversal.isReference(seed)
                || !isVisibleBuildReference(level, hit.getBlockPos(), hit.getDirection(), seed,
                settings.mode(), settings.allowReplacement())) return List.of();
        BuilderMode mode = settings.mode();
        boolean creative = Minecraft.getInstance().player != null
                && Minecraft.getInstance().player.isCreative();
        int limit = mode == BuilderMode.BUILD ? settings.buildLimit() : settings.demolishLimit();
        int scanLimit = Math.min(65_536, Math.max(limit, limit * 16));
        SurfacePlaneTraversal.Result traversal = SurfacePlaneTraversal.traverse(
                hit.getBlockPos(), hit.getDirection().getAxis(), scanLimit, limit,
                pos -> {
                    BlockState state = level.getBlockState(pos);
                    return SurfacePlaneTraversal.isReference(state)
                            && (settings.surfaceMatch() != SurfaceMatchMode.SAME_BLOCK
                            || BuilderBlockCompatibility.sameSurfaceType(seed, state))
                            && isVisibleBuildReference(level, pos, hit.getDirection(), state,
                            mode, settings.allowReplacement());
                }, pos -> {
                    if (mode == BuilderMode.DEMOLISH) return true;
                    BlockPos target = pos.relative(hit.getDirection());
                    if (target.getY() < level.getMinBuildHeight()
                            || target.getY() >= level.getMaxBuildHeight()
                            || !level.getWorldBorder().isWithinBounds(target)) return false;
                    BlockState source = level.getBlockState(pos);
                    BlockState desired = override == null ? source : override;
                    BlockState current = level.getBlockState(target);
                    // Match SurfacePlanner's operation-budget predicate. Already-matching cells are
                    // useful context, but must not consume a slot and hide a later executable cell.
                    return !current.equals(desired)
                            && (current.canBeReplaced() || settings.allowReplacement());
                });
        List<BuilderPreviewState.Cell> result = new ArrayList<>(traversal.nodes().size());
        for (SurfacePlaneTraversal.Node node : traversal.nodes()) {
            BlockState source = level.getBlockState(node.pos());
            if (mode == BuilderMode.DEMOLISH) {
                BuilderPreviewState.Kind kind = source.is(BuilderTags.CONSTRUCTION_PROTECTED)
                        || source.is(BuilderTags.TRANSACTION_UNSAFE)
                        ? BuilderPreviewState.Kind.INVALID : BuilderPreviewState.Kind.DEMOLISH;
                result.add(new BuilderPreviewState.Cell(node.pos(), source, kind, false));
            } else {
                BlockPos target = node.pos().relative(hit.getDirection());
                BlockState desired = override == null ? source : override;
                BlockState current = level.getBlockState(target);
                BuilderPreviewState.Kind kind = current.equals(desired)
                        ? BuilderPreviewState.Kind.BLUEPRINT
                        : BuilderReplacementPolicy.canReplaceTarget(current, level, target,
                        settings.allowReplacement(), creative) && desired.canSurvive(level, target)
                        && !desired.is(BuilderTags.CONSTRUCTION_PROTECTED)
                        && !desired.is(BuilderTags.TRANSACTION_UNSAFE)
                        ? BuilderPreviewState.Kind.BUILD : BuilderPreviewState.Kind.INVALID;
                result.add(new BuilderPreviewState.Cell(target, desired, kind,
                        kind == BuilderPreviewState.Kind.BUILD));
            }
        }
        return List.copyOf(result);
    }

    /** Checks a bounded slice of the previous result and only reruns BFS after observable drift. */
    private static boolean hasPreviewDrift(ClientLevel level, PreviewKey key) {
        if (lastCells.isEmpty() || lastFace == null) return false;
        int checks = Math.min(VALIDATION_CELLS_PER_TICK, lastCells.size());
        for (int count = 0; count < checks; count++) {
            if (validationCursor >= lastCells.size()) validationCursor = 0;
            BuilderPreviewState.Cell cell = lastCells.get(validationCursor++);
            BlockState current = level.getBlockState(cell.pos());
            if (key.mode() == BuilderMode.DEMOLISH) {
                if (!SurfacePlaneTraversal.hasExposedReferenceFace(
                        level, cell.pos(), lastFace)) return true;
                BuilderPreviewState.Kind currentKind = current.is(BuilderTags.CONSTRUCTION_PROTECTED)
                        || current.is(BuilderTags.TRANSACTION_UNSAFE)
                        ? BuilderPreviewState.Kind.INVALID : BuilderPreviewState.Kind.DEMOLISH;
                if (!current.equals(cell.state()) || currentKind != cell.kind()) return true;
                continue;
            }

            BuilderPreviewState.Kind currentKind = current.equals(cell.state())
                    ? BuilderPreviewState.Kind.BLUEPRINT
                    : BuilderReplacementPolicy.canReplaceTarget(current, level, cell.pos(),
                    key.allowReplacement(), key.creative())
                    && cell.state().canSurvive(level, cell.pos())
                    && !cell.state().is(BuilderTags.CONSTRUCTION_PROTECTED)
                    && !cell.state().is(BuilderTags.TRANSACTION_UNSAFE)
                    ? BuilderPreviewState.Kind.BUILD : BuilderPreviewState.Kind.INVALID;
            if (currentKind != cell.kind()) return true;

            BlockState source = level.getBlockState(cell.pos().relative(lastFace.getOpposite()));
            if (!isVisibleBuildReference(level, cell.pos().relative(lastFace.getOpposite()),
                    lastFace, source, key.mode(), key.allowReplacement())) return true;
            if (lastOverride == null) {
                if (!source.equals(cell.state())) return true;
            } else if (!SurfacePlaneTraversal.isReference(source)
                    || key.match() == SurfaceMatchMode.SAME_BLOCK
                    && !BuilderBlockCompatibility.sameSurfaceType(key.seedState(), source)) {
                return true;
            }
        }
        return false;
    }

    /** Client mirror of SurfacePlanner's visible reference/replacement surface boundary. */
    private static boolean isVisibleBuildReference(ClientLevel level, BlockPos referencePos,
                                                   Direction face, BlockState reference,
                                                   BuilderMode mode, boolean allowReplacement) {
        if (SurfacePlaneTraversal.hasExposedReferenceFace(
                level, referencePos, face, reference)) return true;
        if (mode != BuilderMode.BUILD || !allowReplacement) return false;
        BlockPos target = referencePos.relative(face);
        BlockState obstacle = level.getBlockState(target);
        return BuilderReplacementPolicy.requiresBreaking(obstacle)
                && SurfacePlaneTraversal.hasExposedReferenceFace(level, target, face, obstacle);
    }

    private record PreviewKey(ClientLevel level, BlockPos seed, Direction face, BlockState seedState,
                              BuilderMode mode, SurfaceMatchMode match, int buildLimit,
                              int demolishLimit, boolean allowReplacement, boolean creative,
                              BlockState override, BlockState targetSeedState) { }
}
