package zenith.zov.utility.world;

import com.google.common.collect.Sets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
import net.minecraft.enchantment.EnchantmentHelper;
import net.minecraft.enchantment.Enchantments;
import net.minecraft.entity.Entity;
import net.minecraft.entity.ExperienceOrbEntity;
import net.minecraft.entity.ItemEntity;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.attribute.EntityAttributes;
import net.minecraft.entity.decoration.EndCrystalEntity;
import net.minecraft.entity.effect.StatusEffects;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.projectile.ProjectileUtil;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import net.minecraft.network.packet.c2s.play.ClientCommandC2SPacket;
import net.minecraft.network.packet.c2s.play.HandSwingC2SPacket;
import net.minecraft.network.packet.c2s.play.PlayerInteractEntityC2SPacket;
import net.minecraft.network.packet.c2s.play.ClientCommandC2SPacket.Mode;
import net.minecraft.registry.RegistryKeys;
import net.minecraft.registry.tag.FluidTags;
import net.minecraft.util.Hand;
import net.minecraft.util.hit.BlockHitResult;
import net.minecraft.util.hit.EntityHitResult;
import net.minecraft.util.hit.HitResult;
import net.minecraft.util.hit.HitResult.Type;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Box;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.MathHelper;
import net.minecraft.util.math.Vec3d;
import net.minecraft.world.RaycastContext;
import net.minecraft.world.World;
import net.minecraft.world.RaycastContext.FluidHandling;
import net.minecraft.world.RaycastContext.ShapeType;
import zenith.zov.utility.interfaces.IMinecraft;
import zenith.zov.utility.render.RenderPosition;

public class WorldUtils implements IMinecraft {
    public static Set<Block> RIGHT_CLICKABLE_BLOCKS = Sets.newHashSet(
        new Block[]{
            Blocks.CHEST,
            Blocks.TRAPPED_CHEST,
            Blocks.ENDER_CHEST,
            Blocks.WHITE_SHULKER_BOX,
            Blocks.ORANGE_SHULKER_BOX,
            Blocks.MAGENTA_SHULKER_BOX,
            Blocks.LIGHT_BLUE_SHULKER_BOX,
            Blocks.YELLOW_SHULKER_BOX,
            Blocks.LIME_SHULKER_BOX,
            Blocks.PINK_SHULKER_BOX,
            Blocks.GRAY_SHULKER_BOX,
            Blocks.LIGHT_GRAY_SHULKER_BOX,
            Blocks.CYAN_SHULKER_BOX,
            Blocks.PURPLE_SHULKER_BOX,
            Blocks.BLUE_SHULKER_BOX,
            Blocks.BROWN_SHULKER_BOX,
            Blocks.GREEN_SHULKER_BOX,
            Blocks.RED_SHULKER_BOX,
            Blocks.BLACK_SHULKER_BOX,
            Blocks.ANVIL,
            Blocks.BELL,
            Blocks.OAK_BUTTON,
            Blocks.ACACIA_BUTTON,
            Blocks.BIRCH_BUTTON,
            Blocks.DARK_OAK_BUTTON,
            Blocks.JUNGLE_BUTTON,
            Blocks.SPRUCE_BUTTON,
            Blocks.STONE_BUTTON,
            Blocks.COMPARATOR,
            Blocks.REPEATER,
            Blocks.OAK_FENCE_GATE,
            Blocks.SPRUCE_FENCE_GATE,
            Blocks.BIRCH_FENCE_GATE,
            Blocks.JUNGLE_FENCE_GATE,
            Blocks.DARK_OAK_FENCE_GATE,
            Blocks.ACACIA_FENCE_GATE,
            Blocks.BREWING_STAND,
            Blocks.DISPENSER,
            Blocks.DROPPER,
            Blocks.LEVER,
            Blocks.NOTE_BLOCK,
            Blocks.JUKEBOX,
            Blocks.BEACON,
            Blocks.BLACK_BED,
            Blocks.BLUE_BED,
            Blocks.BROWN_BED,
            Blocks.CYAN_BED,
            Blocks.GRAY_BED,
            Blocks.GREEN_BED,
            Blocks.LIGHT_BLUE_BED,
            Blocks.LIGHT_GRAY_BED,
            Blocks.LIME_BED,
            Blocks.MAGENTA_BED,
            Blocks.ORANGE_BED,
            Blocks.PINK_BED,
            Blocks.PURPLE_BED,
            Blocks.RED_BED,
            Blocks.WHITE_BED,
            Blocks.YELLOW_BED,
            Blocks.FURNACE,
            Blocks.OAK_DOOR,
            Blocks.SPRUCE_DOOR,
            Blocks.BIRCH_DOOR,
            Blocks.JUNGLE_DOOR,
            Blocks.ACACIA_DOOR,
            Blocks.DARK_OAK_DOOR,
            Blocks.CAKE,
            Blocks.ENCHANTING_TABLE,
            Blocks.DRAGON_EGG,
            Blocks.HOPPER,
            Blocks.REPEATING_COMMAND_BLOCK,
            Blocks.COMMAND_BLOCK,
            Blocks.CHAIN_COMMAND_BLOCK,
            Blocks.CRAFTING_TABLE,
            Blocks.ACACIA_TRAPDOOR,
            Blocks.BIRCH_TRAPDOOR,
            Blocks.DARK_OAK_TRAPDOOR,
            Blocks.JUNGLE_TRAPDOOR,
            Blocks.OAK_TRAPDOOR,
            Blocks.SPRUCE_TRAPDOOR,
            Blocks.CAKE,
            Blocks.ACACIA_SIGN,
            Blocks.ACACIA_WALL_SIGN,
            Blocks.BIRCH_SIGN,
            Blocks.BIRCH_WALL_SIGN,
            Blocks.DARK_OAK_SIGN,
            Blocks.DARK_OAK_WALL_SIGN,
            Blocks.JUNGLE_SIGN,
            Blocks.JUNGLE_WALL_SIGN,
            Blocks.OAK_SIGN,
            Blocks.OAK_WALL_SIGN,
            Blocks.SPRUCE_SIGN,
            Blocks.SPRUCE_WALL_SIGN,
            Blocks.CRIMSON_SIGN,
            Blocks.CRIMSON_WALL_SIGN,
            Blocks.WARPED_SIGN,
            Blocks.WARPED_WALL_SIGN,
            Blocks.BLAST_FURNACE,
            Blocks.SMOKER,
            Blocks.CARTOGRAPHY_TABLE,
            Blocks.GRINDSTONE,
            Blocks.LECTERN,
            Blocks.LOOM,
            Blocks.STONECUTTER,
            Blocks.SMITHING_TABLE
        }
    );
    private static final ItemStack NETHERITE_PICKAXE = new ItemStack(Items.NETHERITE_PICKAXE);

    public static boolean canSee(Entity entity) {
        return canSee(entity.getX(), entity.getY(), entity.getZ());
    }

    public static boolean canSee(BlockPos position) {
        return canSee((double)position.getX() + 0.5, (double)position.getY() + 0.5, (double)position.getZ() + 0.5);
    }

    public static boolean canSee(Vec3d vec3d) {
        return canSee(vec3d.getX(), vec3d.getY(), vec3d.getZ());
    }

    public static boolean canSee(double x, double y, double z) {
        return mc.world
                .raycast(
                    new RaycastContext(
                        new Vec3d(mc.player.getX(), mc.player.getEyeY(), mc.player.getZ()),
                        new Vec3d(x, y, z),
                        ShapeType.COLLIDER,
                        FluidHandling.NONE,
                        mc.player
                    )
                )
                .getType()
            == Type.MISS;
    }

    public static boolean canSeeOutline(Vec3d vec3d) {
        return canSeeOutline(vec3d.getX(), vec3d.getY(), vec3d.getZ());
    }

    public static boolean canSeeOutline(double x, double y, double z) {
        return mc.world
                .raycast(
                    new RaycastContext(
                        new Vec3d(mc.player.getX(), mc.player.getEyeY(), mc.player.getZ()),
                        new Vec3d(x, y, z),
                        ShapeType.OUTLINE,
                        FluidHandling.NONE,
                        mc.player
                    )
                )
                .getType()
            == Type.MISS;
    }

    public static List<PlayerEntity> getCollisions(BlockPos pos) {
        List<PlayerEntity> collisions = new ArrayList<>();

        for (PlayerEntity player : mc.world.getPlayers()) {
            if (player != null && !player.isDead() && player.getBoundingBox().intersects(new Box(pos))) {
                collisions.add(player);
            }
        }

        return collisions;
    }

    public static Direction getDirection(BlockPos position, boolean strictDirection) {
        return getDirection(position, null, strictDirection);
    }

    public static Direction getDirection(BlockPos position, List<BlockPos> exceptions, boolean strictDirection) {
        List<Direction> strictDirections = new ArrayList<>();
        if (strictDirection) {
            strictDirections = getStrictDirections(mc.player.getEyePos(), Vec3d.ofCenter(position));
        }

        for (Direction direction : Direction.values()) {
            BlockPos offset = position.offset(direction);
            if ((!strictDirection || strictDirections.contains(direction.getOpposite()))
                && (!mc.world.getBlockState(offset).isReplaceable() || exceptions != null && exceptions.contains(offset))) {
                return direction;
            }
        }

        return null;
    }

    public static List<Direction> getStrictDirections(Vec3d eyePos, Vec3d blockPos) {
        List<Direction> directions = new ArrayList<>();
        double differenceX = eyePos.getX() - blockPos.getX();
        double differenceY = eyePos.getY() - blockPos.getY();
        double differenceZ = eyePos.getZ() - blockPos.getZ();
        if (differenceY > 0.5) {
            directions.add(Direction.UP);
        } else if (differenceY < -0.5) {
            directions.add(Direction.DOWN);
        } else {
            directions.add(Direction.UP);
            directions.add(Direction.DOWN);
        }

        if (differenceX > 0.5) {
            directions.add(Direction.EAST);
        } else if (differenceX < -0.5) {
            directions.add(Direction.WEST);
        } else {
            directions.add(Direction.EAST);
            directions.add(Direction.WEST);
        }

        if (differenceZ > 0.5) {
            directions.add(Direction.SOUTH);
        } else if (differenceZ < -0.5) {
            directions.add(Direction.NORTH);
        } else {
            directions.add(Direction.SOUTH);
            directions.add(Direction.NORTH);
        }

        return directions;
    }

    public static boolean isPlaceable(BlockPos position) {
        return isPlaceable(position, false);
    }

    public static boolean isPlaceable(BlockPos position, boolean excludeSelf) {
        return !mc.world.getBlockState(position).isReplaceable()
            ? false
            : mc.world
                .getOtherEntities(null, new Box(position))
                .stream()
                .noneMatch(
                    entity -> !(entity instanceof EndCrystalEntity)
                            && !(entity instanceof ExperienceOrbEntity)
                            && !(entity instanceof ItemEntity)
                            && (!entity.equals(mc.player) || !excludeSelf)
                );
    }

    public static void placeBlock(BlockPos position, Direction direction, Hand hand, boolean rotate, boolean crystalDestruction, boolean render) {
        placeBlock(position, direction, hand, null, rotate, crystalDestruction, render);
    }

    public static void placeBlock(
        BlockPos position, Direction direction, Hand hand, Runnable runnable, boolean rotate, boolean crystalDestruction, boolean render
    ) {
        Vec3d vec3d = position.toCenterPos();
        BlockPos offsetPosition;
        if (direction == null) {
            direction = Direction.UP;
            offsetPosition = position;
        } else {
            offsetPosition = position.offset(direction);
            vec3d = vec3d.add((double)direction.getOffsetX() / 2.0, (double)direction.getOffsetY() / 2.0, (double)direction.getOffsetZ() / 2.0);
        }

        if (rotate) {
        }

        if (crystalDestruction) {
            destroyCrystals(position);
        }

        if (runnable != null) {
            runnable.run();
        }

        boolean sprint = mc.player.isSprinting();
        boolean sneak = RIGHT_CLICKABLE_BLOCKS.contains(mc.world.getBlockState(offsetPosition).getBlock()) && !mc.player.isSneaking();
        if (sprint) {
            mc.player.networkHandler.sendPacket(new ClientCommandC2SPacket(mc.player, Mode.STOP_SPRINTING));
        }

        if (sneak) {
            mc.player.networkHandler.sendPacket(new ClientCommandC2SPacket(mc.player, Mode.PRESS_SHIFT_KEY));
        }

        BlockHitResult blockHitResult = new BlockHitResult(vec3d, direction.getOpposite(), offsetPosition, false);
        mc.interactionManager.interactBlock(mc.player, hand, blockHitResult);
        mc.getNetworkHandler().sendPacket(new HandSwingC2SPacket(hand));
        if (sneak) {
            mc.player.networkHandler.sendPacket(new ClientCommandC2SPacket(mc.player, Mode.RELEASE_SHIFT_KEY));
        }

        if (sprint) {
            mc.player.networkHandler.sendPacket(new ClientCommandC2SPacket(mc.player, Mode.START_SPRINTING));
        }

        if (render) {
            new RenderPosition(position);
        }
    }

    public static void destroyCrystals(BlockPos position) {
        List<Entity> surroundingCrystals = mc.world
            .getOtherEntities(null, new Box(position))
            .stream()
            .filter(entityx -> entityx instanceof EndCrystalEntity)
            .toList();
        if (!surroundingCrystals.isEmpty()) {
            Iterator var2 = surroundingCrystals.iterator();
            if (var2.hasNext()) {
                Entity entity = (Entity)var2.next();
                mc.player.networkHandler.sendPacket(PlayerInteractEntityC2SPacket.attack(entity, mc.player.isSneaking()));
                mc.player.networkHandler.sendPacket(new HandSwingC2SPacket(Hand.MAIN_HAND));
            }
        }
    }

    public static Direction getClosestDirection(BlockPos position, boolean strictDirection) {
        if (strictDirection) {
            if (mc.player.getY() >= (double)position.getY()) {
                return Direction.UP;
            } else {
                BlockHitResult result = mc.world
                    .raycast(
                        new RaycastContext(
                            mc.player.getEyePos(), Vec3d.ofCenter(position), ShapeType.OUTLINE, FluidHandling.NONE, mc.player
                        )
                    );
                return result != null && result.getType() == Type.BLOCK && result.getSide() != null
                    ? result.getSide()
                    : getClosestDirection(position);
            }
        } else {
            return getClosestDirection(position);
        }
    }

    private static Direction getClosestDirection(BlockPos position) {
        Direction closestDirection = null;
        Vec3d offsetPosition = null;

        for (Direction direction : Direction.values()) {
            Vec3d newOffset = getHitVector(position, direction);
            if (closestDirection == null) {
                closestDirection = direction;
                offsetPosition = newOffset;
            } else if (mc.player.squaredDistanceTo(newOffset) < mc.player.squaredDistanceTo(offsetPosition)) {
                closestDirection = direction;
                offsetPosition = newOffset;
            }
        }

        return closestDirection;
    }

    public static Vec3d getHitVector(BlockPos position, Direction direction) {
        return position.toCenterPos()
            .add((double)direction.getOffsetX() / 2.0, (double)direction.getOffsetY() / 2.0, (double)direction.getOffsetZ() / 2.0);
    }

    public static float getMineSpeed(BlockState state, int slot) {
        if (mc.player == null) {
            return 0.0F;
        } else {
            float speed = ((ItemStack)mc.player.getInventory().main.get(slot)).getMiningSpeedMultiplier(state);
            if (speed > 1.0F) {
                ItemStack stack = mc.player.getInventory().getStack(slot);
                int efficiency = EnchantmentHelper.getLevel(
                    mc.world.getRegistryManager().getOrThrow(RegistryKeys.ENCHANTMENT).getOrThrow(Enchantments.EFFICIENCY), stack
                );
                if (efficiency > 0 && !stack.isEmpty()) {
                    speed += (float)(StrictMath.pow((double)efficiency, 2.0) + 1.0);
                }
            }

            if (mc.player.hasStatusEffect(StatusEffects.HASTE)) {
                speed *= 1.0F + (float)(mc.player.getStatusEffect(StatusEffects.HASTE).getAmplifier() + 1) * 0.2F;
            }

            if (mc.player.hasStatusEffect(StatusEffects.MINING_FATIGUE)) {
                speed *= (float)Math.pow(0.3F, (double)(mc.player.getStatusEffect(StatusEffects.MINING_FATIGUE).getAmplifier() + 1));
            }

            if (mc.player.isSubmergedIn(FluidTags.WATER)) {
                speed *= (float)mc.player.getAttributeInstance(EntityAttributes.SUBMERGED_MINING_SPEED).getValue();
            }

            if (!mc.player.isOnGround()) {
                speed /= 5.0F;
            }

            speed = speed < 0.0F ? 0.0F : speed;
            return speed
                / state.getBlock().getHardness()
                / (float)(state.isToolRequired() && !((ItemStack)mc.player.getInventory().main.get(slot)).isSuitableFor(state) ? 100 : 30);
        }
    }

    public static boolean isReplaceable(BlockPos... pos) {
        return Arrays.stream(pos).allMatch(blockPos -> mc.world.getBlockState(blockPos).isReplaceable());
    }

    public static boolean isCrystalPlaceable(BlockPos position) {
        return !mc.world.getBlockState(position).isReplaceable()
            ? false
            : mc.world
                .getOtherEntities(null, new Box(position))
                .stream()
                .noneMatch(entity -> !(entity instanceof EndCrystalEntity) && !(entity instanceof ExperienceOrbEntity));
    }

    public static boolean canBreak(BlockPos... pos) {
        return Arrays.stream(pos).allMatch(blockPos -> mc.world.getBlockState(blockPos).getBlock().getHardness() != -1.0F);
    }

    public static Block getBlock(BlockPos pos) {
        return mc.world.getBlockState(pos).getBlock();
    }

    public static boolean blocksMovement(BlockState state) {
        return state.getBlock() != Blocks.COBWEB && state.getBlock() != Blocks.BAMBOO_SAPLING && !state.isReplaceable();
    }

    public static float getBreakTime(PlayerEntity player, BlockState blockState) {
        if (player == null) {
            return 0.0F;
        } else {
            float speed = NETHERITE_PICKAXE.getMiningSpeedMultiplier(blockState) + 26.0F;
            return 1.0F / (speed / blockState.getBlock().getHardness() / 30.0F) * 50.0F;
        }
    }

    public static boolean equals(BlockPos x, BlockPos y) {
        if (x == null && y == null) {
            return true;
        } else {
            return x != null && y != null ? x.equals(y) : false;
        }
    }

    public static HitResult getRaytraceTarget(float yaw, float pitch, double x, double y, double z) {
        Vec3d rotationVector = new Vec3d(
            (double)(MathHelper.sin(-yaw * (float) (Math.PI / 180.0)) * MathHelper.cos(pitch * (float) (Math.PI / 180.0))),
            (double)(-MathHelper.sin(pitch * (float) (Math.PI / 180.0))),
            (double)(MathHelper.cos(-yaw * (float) (Math.PI / 180.0)) * MathHelper.cos(pitch * (float) (Math.PI / 180.0)))
        );
        HitResult result = mc.world
            .raycast(
                new RaycastContext(
                    new Vec3d(x, y, z),
                    new Vec3d(x + rotationVector.x * 5.0, y + rotationVector.y * 5.0, z + rotationVector.z * 5.0),
                    ShapeType.OUTLINE,
                    FluidHandling.NONE,
                    mc.player
                )
            );
        Vec3d vec3d = new Vec3d(x, y + (double)mc.player.getEyeHeight(mc.player.getPose()), z);
        double distance = 25.0;
        if (result != null) {
            distance = result.getPos().squaredDistanceTo(vec3d);
        }

        Vec3d multipliedVector = vec3d.add(rotationVector.x * 5.0, rotationVector.y * 5.0, rotationVector.z * 5.0);
        Box box = new Box(x - 0.3, y, z - 0.3, x + 0.3, y + 1.8, z + 0.3).stretch(rotationVector.multiply(5.0)).expand(1.0, 1.0, 1.0);
        EntityHitResult entityHitResult = ProjectileUtil.raycast(
            mc.player, vec3d, multipliedVector, box, entity -> !entity.isSpectator() && entity.canHit(), distance
        );
        return (HitResult)(entityHitResult != null
                && (vec3d.squaredDistanceTo(entityHitResult.getPos()) < distance || result == null)
                && entityHitResult.getEntity() instanceof LivingEntity
            ? entityHitResult
            : result);
    }

    public static String getMovementDirection(Direction direction) {
        if (direction.getName().equalsIgnoreCase("North")) {
            return "-Z";
        } else if (direction.getName().equalsIgnoreCase("East")) {
            return "+X";
        } else if (direction.getName().equalsIgnoreCase("South")) {
            return "+Z";
        } else {
            return direction.getName().equalsIgnoreCase("West") ? "-X" : "N/A";
        }
    }

    public static int getNetherPosition(int position) {
        return mc.player.getWorld().getRegistryKey() == World.NETHER ? position * 8 : position / 8;
    }

    public static List<BlockPos> getBoxes(Box box) {
        List<BlockPos> boxes = new ArrayList<>();

        for (int x = (int)Math.floor(box.minX); (double)x < Math.ceil(box.maxX); x++) {
            for (int y = (int)Math.floor(box.minY); (double)y < Math.ceil(box.maxY); y++) {
                for (int z = (int)Math.floor(box.minZ); (double)z < Math.ceil(box.maxZ); z++) {
                    boxes.add(new BlockPos(x, y, z));
                }
            }
        }

        return boxes;
    }
}
