/*
 *  Copyright 2025-2026 R3944Realms
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package top.r3944realms.eroticdungeongame.content.entity.npc.ai.navigation.pathfinder;

import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import net.minecraft.core.BlockPos;
import net.minecraft.util.Mth;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.PathNavigationRegion;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.pathfinder.BlockPathTypes;
import net.minecraft.world.level.pathfinder.Node;
import net.minecraft.world.level.pathfinder.Target;
import net.minecraft.world.phys.AABB;
import org.jetbrains.annotations.Nullable;
import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer;

import java.util.EnumSet;
import java.util.List;

public class NPCFlyNodeEvaluator extends NPCWalkNodeEvaluator {
    private final Long2ObjectMap<BlockPathTypes> pathTypeByPosCache = new Long2ObjectOpenHashMap<>();
    private static final float SMALL_MOB_INFLATED_START_NODE_BOUNDING_BOX = 1.5F;
    private static final int MAX_START_NODE_CANDIDATES = 10;

    public void prepare(PathNavigationRegion level, NPCServerPlayer npc) {
        super.prepare(level, npc);
        this.pathTypeByPosCache.clear();
        npc.onPathfindingStart();
    }

    public void done() {
        this.npc.onPathfindingDone();
        this.pathTypeByPosCache.clear();
        super.done();
    }

    public Node getStart() {
        int i;
        if (this.canFloat() && this.npc.isInWater()) {
            i = this.npc.getBlockY();
            BlockPos.MutableBlockPos blockpos$mutableblockpos = new BlockPos.MutableBlockPos(this.npc.getX(), (double)i, this.npc.getZ());

            for(BlockState blockstate = this.level.getBlockState(blockpos$mutableblockpos); blockstate.is(Blocks.WATER); blockstate = this.level.getBlockState(blockpos$mutableblockpos)) {
                ++i;
                blockpos$mutableblockpos.set(this.npc.getX(), (double)i, this.npc.getZ());
            }
        } else {
            i = Mth.floor(this.npc.getY() + 0.5D);
        }

        BlockPos blockpos1 = BlockPos.containing(this.npc.getX(), (double)i, this.npc.getZ());
        if (!this.canStartAt(blockpos1)) {
            for(BlockPos blockpos : this.iteratePathfindingStartNodeCandidatePositions(this.npc)) {
                if (this.canStartAt(blockpos)) {
                    return super.getStartNode(blockpos);
                }
            }
        }

        return super.getStartNode(blockpos1);
    }

    protected boolean canStartAt(BlockPos pos) {
        BlockPathTypes blockpathtypes = this.getBlockPathType(this.npc, pos);
        return this.npc.getPathfindingMalus(blockpathtypes) >= 0.0F;
    }

    public Target getGoal(double x, double y, double z) {
        return this.getTargetFromNode(this.getNode(Mth.floor(x), Mth.floor(y), Mth.floor(z)));
    }

    public int getNeighbors(Node[] outputArray, Node p_node) {
        int i = 0;
        Node node = this.findAcceptedNode(p_node.x, p_node.y, p_node.z + 1);
        if (this.isOpen(node)) {
            outputArray[i++] = node;
        }

        Node node1 = this.findAcceptedNode(p_node.x - 1, p_node.y, p_node.z);
        if (this.isOpen(node1)) {
            outputArray[i++] = node1;
        }

        Node node2 = this.findAcceptedNode(p_node.x + 1, p_node.y, p_node.z);
        if (this.isOpen(node2)) {
            outputArray[i++] = node2;
        }

        Node node3 = this.findAcceptedNode(p_node.x, p_node.y, p_node.z - 1);
        if (this.isOpen(node3)) {
            outputArray[i++] = node3;
        }

        Node node4 = this.findAcceptedNode(p_node.x, p_node.y + 1, p_node.z);
        if (this.isOpen(node4)) {
            outputArray[i++] = node4;
        }

        Node node5 = this.findAcceptedNode(p_node.x, p_node.y - 1, p_node.z);
        if (this.isOpen(node5)) {
            outputArray[i++] = node5;
        }

        Node node6 = this.findAcceptedNode(p_node.x, p_node.y + 1, p_node.z + 1);
        if (this.isOpen(node6) && this.hasMalus(node) && this.hasMalus(node4)) {
            outputArray[i++] = node6;
        }

        Node node7 = this.findAcceptedNode(p_node.x - 1, p_node.y + 1, p_node.z);
        if (this.isOpen(node7) && this.hasMalus(node1) && this.hasMalus(node4)) {
            outputArray[i++] = node7;
        }

        Node node8 = this.findAcceptedNode(p_node.x + 1, p_node.y + 1, p_node.z);
        if (this.isOpen(node8) && this.hasMalus(node2) && this.hasMalus(node4)) {
            outputArray[i++] = node8;
        }

        Node node9 = this.findAcceptedNode(p_node.x, p_node.y + 1, p_node.z - 1);
        if (this.isOpen(node9) && this.hasMalus(node3) && this.hasMalus(node4)) {
            outputArray[i++] = node9;
        }

        Node node10 = this.findAcceptedNode(p_node.x, p_node.y - 1, p_node.z + 1);
        if (this.isOpen(node10) && this.hasMalus(node) && this.hasMalus(node5)) {
            outputArray[i++] = node10;
        }

        Node node11 = this.findAcceptedNode(p_node.x - 1, p_node.y - 1, p_node.z);
        if (this.isOpen(node11) && this.hasMalus(node1) && this.hasMalus(node5)) {
            outputArray[i++] = node11;
        }

        Node node12 = this.findAcceptedNode(p_node.x + 1, p_node.y - 1, p_node.z);
        if (this.isOpen(node12) && this.hasMalus(node2) && this.hasMalus(node5)) {
            outputArray[i++] = node12;
        }

        Node node13 = this.findAcceptedNode(p_node.x, p_node.y - 1, p_node.z - 1);
        if (this.isOpen(node13) && this.hasMalus(node3) && this.hasMalus(node5)) {
            outputArray[i++] = node13;
        }

        Node node14 = this.findAcceptedNode(p_node.x + 1, p_node.y, p_node.z - 1);
        if (this.isOpen(node14) && this.hasMalus(node3) && this.hasMalus(node2)) {
            outputArray[i++] = node14;
        }

        Node node15 = this.findAcceptedNode(p_node.x + 1, p_node.y, p_node.z + 1);
        if (this.isOpen(node15) && this.hasMalus(node) && this.hasMalus(node2)) {
            outputArray[i++] = node15;
        }

        Node node16 = this.findAcceptedNode(p_node.x - 1, p_node.y, p_node.z - 1);
        if (this.isOpen(node16) && this.hasMalus(node3) && this.hasMalus(node1)) {
            outputArray[i++] = node16;
        }

        Node node17 = this.findAcceptedNode(p_node.x - 1, p_node.y, p_node.z + 1);
        if (this.isOpen(node17) && this.hasMalus(node) && this.hasMalus(node1)) {
            outputArray[i++] = node17;
        }

        Node node18 = this.findAcceptedNode(p_node.x + 1, p_node.y + 1, p_node.z - 1);
        if (this.isOpen(node18) && this.hasMalus(node14) && this.hasMalus(node3) && this.hasMalus(node2) && this.hasMalus(node4) && this.hasMalus(node9) && this.hasMalus(node8)) {
            outputArray[i++] = node18;
        }

        Node node19 = this.findAcceptedNode(p_node.x + 1, p_node.y + 1, p_node.z + 1);
        if (this.isOpen(node19) && this.hasMalus(node15) && this.hasMalus(node) && this.hasMalus(node2) && this.hasMalus(node4) && this.hasMalus(node6) && this.hasMalus(node8)) {
            outputArray[i++] = node19;
        }

        Node node20 = this.findAcceptedNode(p_node.x - 1, p_node.y + 1, p_node.z - 1);
        if (this.isOpen(node20) && this.hasMalus(node16) && this.hasMalus(node3) && this.hasMalus(node1) && this.hasMalus(node4) && this.hasMalus(node9) && this.hasMalus(node7)) {
            outputArray[i++] = node20;
        }

        Node node21 = this.findAcceptedNode(p_node.x - 1, p_node.y + 1, p_node.z + 1);
        if (this.isOpen(node21) && this.hasMalus(node17) && this.hasMalus(node) && this.hasMalus(node1) && this.hasMalus(node4) && this.hasMalus(node6) && this.hasMalus(node7)) {
            outputArray[i++] = node21;
        }

        Node node22 = this.findAcceptedNode(p_node.x + 1, p_node.y - 1, p_node.z - 1);
        if (this.isOpen(node22) && this.hasMalus(node14) && this.hasMalus(node3) && this.hasMalus(node2) && this.hasMalus(node5) && this.hasMalus(node13) && this.hasMalus(node12)) {
            outputArray[i++] = node22;
        }

        Node node23 = this.findAcceptedNode(p_node.x + 1, p_node.y - 1, p_node.z + 1);
        if (this.isOpen(node23) && this.hasMalus(node15) && this.hasMalus(node) && this.hasMalus(node2) && this.hasMalus(node5) && this.hasMalus(node10) && this.hasMalus(node12)) {
            outputArray[i++] = node23;
        }

        Node node24 = this.findAcceptedNode(p_node.x - 1, p_node.y - 1, p_node.z - 1);
        if (this.isOpen(node24) && this.hasMalus(node16) && this.hasMalus(node3) && this.hasMalus(node1) && this.hasMalus(node5) && this.hasMalus(node13) && this.hasMalus(node11)) {
            outputArray[i++] = node24;
        }

        Node node25 = this.findAcceptedNode(p_node.x - 1, p_node.y - 1, p_node.z + 1);
        if (this.isOpen(node25) && this.hasMalus(node17) && this.hasMalus(node) && this.hasMalus(node1) && this.hasMalus(node5) && this.hasMalus(node10) && this.hasMalus(node11)) {
            outputArray[i++] = node25;
        }

        return i;
    }

    private boolean hasMalus(@Nullable Node node) {
        return node != null && node.costMalus >= 0.0F;
    }

    private boolean isOpen(@Nullable Node node) {
        return node != null && !node.closed;
    }

    @Nullable
    protected Node findAcceptedNode(int x, int y, int z) {
        Node node = null;
        BlockPathTypes blockpathtypes = this.getCachedBlockPathType(x, y, z);
        float f = this.npc.getPathfindingMalus(blockpathtypes);
        if (f >= 0.0F) {
            node = this.getNode(x, y, z);
            node.type = blockpathtypes;
            node.costMalus = Math.max(node.costMalus, f);
            if (blockpathtypes == BlockPathTypes.WALKABLE) {
                ++node.costMalus;
            }
        }

        return node;
    }

    private BlockPathTypes getCachedBlockPathType(int x, int y, int z) {
        return this.pathTypeByPosCache.computeIfAbsent(BlockPos.asLong(x, y, z), (p_265010_) -> {
            return this.getBlockPathType(this.level, x, y, z, this.npc);
        });
    }

    public BlockPathTypes getBlockPathType(BlockGetter level, int x, int y, int z, NPCServerPlayer npc) {
        EnumSet<BlockPathTypes> enumset = EnumSet.noneOf(BlockPathTypes.class);
        BlockPathTypes blockpathtypes = BlockPathTypes.BLOCKED;
        BlockPos blockpos = npc.blockPosition();
        blockpathtypes = super.getBlockPathTypes(level, x, y, z, enumset, blockpathtypes, blockpos);
        if (enumset.contains(BlockPathTypes.FENCE)) {
            return BlockPathTypes.FENCE;
        } else {
            BlockPathTypes blockpathtypes1 = BlockPathTypes.BLOCKED;

            for(BlockPathTypes blockpathtypes2 : enumset) {
                if (npc.getPathfindingMalus(blockpathtypes2) < 0.0F) {
                    return blockpathtypes2;
                }

                if (npc.getPathfindingMalus(blockpathtypes2) >= npc.getPathfindingMalus(blockpathtypes1)) {
                    blockpathtypes1 = blockpathtypes2;
                }
            }

            return blockpathtypes == BlockPathTypes.OPEN && npc.getPathfindingMalus(blockpathtypes1) == 0.0F ? BlockPathTypes.OPEN : blockpathtypes1;
        }
    }

    /**
     * Returns the node type at the specified postion taking the block below into account
     */
    public BlockPathTypes getBlockPathType(BlockGetter level, int x, int y, int z) {
        BlockPos.MutableBlockPos blockpos$mutableblockpos = new BlockPos.MutableBlockPos();
        BlockPathTypes blockpathtypes = getBlockPathTypeRaw(level, blockpos$mutableblockpos.set(x, y, z));
        if (blockpathtypes == BlockPathTypes.OPEN && y >= level.getMinBuildHeight() + 1) {
            BlockPathTypes blockpathtypes1 = getBlockPathTypeRaw(level, blockpos$mutableblockpos.set(x, y - 1, z));
            if (blockpathtypes1 != BlockPathTypes.DAMAGE_FIRE && blockpathtypes1 != BlockPathTypes.LAVA) {
                if (blockpathtypes1 == BlockPathTypes.DAMAGE_OTHER) {
                    blockpathtypes = BlockPathTypes.DAMAGE_OTHER;
                } else if (blockpathtypes1 == BlockPathTypes.COCOA) {
                    blockpathtypes = BlockPathTypes.COCOA;
                } else if (blockpathtypes1 == BlockPathTypes.FENCE) {
                    if (!blockpos$mutableblockpos.equals(this.npc.blockPosition())) {
                        blockpathtypes = BlockPathTypes.FENCE;
                    }
                } else {
                    blockpathtypes = blockpathtypes1 != BlockPathTypes.WALKABLE && blockpathtypes1 != BlockPathTypes.OPEN && blockpathtypes1 != BlockPathTypes.WATER ? BlockPathTypes.WALKABLE : BlockPathTypes.OPEN;
                }
            } else {
                blockpathtypes = BlockPathTypes.DAMAGE_FIRE;
            }
        }

        if (blockpathtypes == BlockPathTypes.WALKABLE || blockpathtypes == BlockPathTypes.OPEN) {
            blockpathtypes = checkNeighbourBlocks(level, blockpos$mutableblockpos.set(x, y, z), blockpathtypes);
        }

        return blockpathtypes;
    }

    private Iterable<BlockPos> iteratePathfindingStartNodeCandidatePositions(NPCServerPlayer npc) {
        float f = 1.0F;
        AABB aabb = npc.getBoundingBox();
        boolean flag = aabb.getSize() < 1.0D;
        if (!flag) {
            return List.of(BlockPos.containing(aabb.minX, (double)npc.getBlockY(), aabb.minZ), BlockPos.containing(aabb.minX, (double)npc.getBlockY(), aabb.maxZ), BlockPos.containing(aabb.maxX, (double)npc.getBlockY(), aabb.minZ), BlockPos.containing(aabb.maxX, (double)npc.getBlockY(), aabb.maxZ));
        } else {
            double d0 = Math.max(0.0D, (1.5D - aabb.getZsize()) / 2.0D);
            double d1 = Math.max(0.0D, (1.5D - aabb.getXsize()) / 2.0D);
            double d2 = Math.max(0.0D, (1.5D - aabb.getYsize()) / 2.0D);
            AABB aabb1 = aabb.inflate(d1, d2, d0);
            return BlockPos.randomBetweenClosed(npc.getRandom(), 10, Mth.floor(aabb1.minX), Mth.floor(aabb1.minY), Mth.floor(aabb1.minZ), Mth.floor(aabb1.maxX), Mth.floor(aabb1.maxY), Mth.floor(aabb1.maxZ));
        }
    }
}
