/*
 * SPDX-FileCopyrightText: 2026 KOKA99CAB
 * SPDX-License-Identifier: LGPL-3.0-or-later
 */

package net.koka99cab.portals.portal;

import java.util.Optional;
import java.util.function.Predicate;

import net.koka99cab.portals.block.OrePortalBlock;
import net.koka99cab.portals.world.ModDimensions;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.phys.Vec3;

public final class PortalTeleporter {
	private static final int PORTAL_SEARCH_RADIUS = 32;
	private static final int SAFE_SEARCH_RADIUS = 48;

	private PortalTeleporter() {
	}

	public static Optional<Vec3> findOrCreateExit(ServerLevel targetLevel, Entity entity, BlockPos entryPos, Direction.Axis axis, Block frameBlock, Predicate<BlockState> framePredicate, Block portalBlock) {
		BlockPos targetPos = clampedTargetPos(targetLevel, entryPos);
		Optional<PortalFrame> existingFrame = findNearbyPortal(targetLevel, targetPos, axis, framePredicate, portalBlock);

		if (existingFrame.isPresent()) {
			return Optional.of(exitPosition(existingFrame.get()));
		}

		BlockPos safeTargetPos = findSafeTargetPos(targetLevel, targetPos);
		PortalFrame frame = createExitFrame(targetLevel, safeTargetPos, axis, frameBlock, portalBlock);

		if (!hasClearExitPocket(targetLevel, frame)) {
			clearExitPocket(targetLevel, frame);
		}

		return Optional.of(exitPosition(frame));
	}

	private static Optional<PortalFrame> findNearbyPortal(ServerLevel level, BlockPos center, Direction.Axis axis, Predicate<BlockState> framePredicate, Block portalBlock) {
		PortalFrame bestFrame = null;
		double bestDistance = Double.MAX_VALUE;
		int minY = level.getMinY();
		int maxY = level.getMaxY() - 1;

		for (BlockPos pos : BlockPos.betweenClosed(center.offset(-PORTAL_SEARCH_RADIUS, 0, -PORTAL_SEARCH_RADIUS).atY(minY), center.offset(PORTAL_SEARCH_RADIUS, 0, PORTAL_SEARCH_RADIUS).atY(maxY))) {
			BlockState state = level.getBlockState(pos);

			if (!state.is(portalBlock)) {
				continue;
			}

			Direction.Axis candidateAxis = state.getOptionalValue(OrePortalBlock.AXIS).orElse(axis);
			Optional<PortalFrame> frame = PortalFrame.findExisting(level, pos, candidateAxis, framePredicate, portalBlock);

			if (frame.isEmpty()) {
				frame = PortalFrame.findExisting(level, pos, axis, framePredicate, portalBlock);
			}

			if (frame.isEmpty()) {
				continue;
			}

			double distance = frame.get().center().distSqr(center);

			if (distance < bestDistance) {
				bestFrame = frame.get();
				bestDistance = distance;
			}
		}

		return Optional.ofNullable(bestFrame);
	}

	private static PortalFrame createExitFrame(ServerLevel level, BlockPos targetPos, Direction.Axis axis, Block frameBlock, Block portalBlock) {
		BlockPos origin = switch (axis) {
			case X -> targetPos.west(1).below(1);
			case Z -> targetPos.north(1).below(1);
			default -> targetPos.below(1);
		};
		PortalFrame frame = new PortalFrame(axis, origin);

		for (int horizontal = 0; horizontal < PortalFrame.OUTER_WIDTH; horizontal++) {
			for (int vertical = 0; vertical < PortalFrame.OUTER_HEIGHT; vertical++) {
				boolean border = horizontal == 0 || horizontal == PortalFrame.OUTER_WIDTH - 1 || vertical == 0 || vertical == PortalFrame.OUTER_HEIGHT - 1;
				BlockPos pos = portalPos(origin, axis, horizontal, vertical);

				if (border) {
					level.setBlockAndUpdate(pos, frameBlock.defaultBlockState());
				}
			}
		}

		frame.fill(level, portalBlock.defaultBlockState());

		return frame;
	}

	private static void clearExitPocket(ServerLevel level, PortalFrame frame) {
		Direction.Axis axis = frame.axis();

		for (int horizontal = -1; horizontal <= PortalFrame.OUTER_WIDTH; horizontal++) {
			for (int vertical = 1; vertical <= PortalFrame.OUTER_HEIGHT; vertical++) {
				for (int depth = -2; depth <= 2; depth++) {
					if (depth == 0 && horizontal >= 0 && horizontal < PortalFrame.OUTER_WIDTH) {
						continue;
					}

					level.setBlockAndUpdate(pocketPos(frame.origin(), axis, horizontal, vertical, depth), Blocks.AIR.defaultBlockState());
				}
			}
		}
	}

	private static boolean hasClearExitPocket(ServerLevel level, PortalFrame frame) {
		Direction.Axis axis = frame.axis();

		for (int horizontal = 1; horizontal <= PortalFrame.INTERIOR_WIDTH; horizontal++) {
			for (int vertical = 1; vertical <= PortalFrame.INTERIOR_HEIGHT; vertical++) {
				for (int depth = -1; depth <= 1; depth++) {
					BlockPos pos = pocketPos(frame.origin(), axis, horizontal, vertical, depth);

					if (depth == 0) {
						continue;
					}

					BlockState state = level.getBlockState(pos);

					if (!state.isAir() && !state.canBeReplaced()) {
						return false;
					}
				}
			}
		}

		return true;
	}

	private static Vec3 exitPosition(PortalFrame frame) {
		BlockPos origin = frame.origin();

		return switch (frame.axis()) {
			case X -> new Vec3(origin.getX() + 2.0D, origin.getY() + 1.0D, origin.getZ() + 0.5D);
			case Z -> new Vec3(origin.getX() + 0.5D, origin.getY() + 1.0D, origin.getZ() + 2.0D);
			default -> origin.getCenter();
		};
	}

	private static BlockPos clampedTargetPos(ServerLevel level, BlockPos entryPos) {
		int x = entryPos.getX();
		int z = entryPos.getZ();
		int minY = level.getMinY() + 2;
		int maxY = level.getMaxY() - PortalFrame.OUTER_HEIGHT - 2;
		int y = Mth.clamp(entryPos.getY(), minY, maxY);

		if (usesSurfaceExit(level)) {
			int surfaceY = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z);

			if (surfaceY > level.getMinY() + 8) {
				y = Mth.clamp(surfaceY, minY, maxY);
			}
		}

		return new BlockPos(x, y, z);
	}

	private static boolean usesSurfaceExit(ServerLevel level) {
		return level.dimension().equals(ModDimensions.SKELETON_WASTE) || level.dimensionType().hasSkyLight() && !level.dimensionType().hasCeiling();
	}

	private static BlockPos findSafeTargetPos(ServerLevel level, BlockPos targetPos) {
		Optional<BlockPos> safePos = usesSurfaceExit(level) ? findSafeSurfacePos(level, targetPos) : findSafeCavePos(level, targetPos);
		return safePos.orElseGet(() -> targetPos.atY(Mth.clamp(targetPos.getY(), level.getMinY() + 12, level.getMaxY() - PortalFrame.OUTER_HEIGHT - 2)));
	}

	private static Optional<BlockPos> findSafeSurfacePos(ServerLevel level, BlockPos targetPos) {
		BlockPos bestPos = null;
		double bestDistance = Double.MAX_VALUE;

		for (int radius = 0; radius <= SAFE_SEARCH_RADIUS; radius++) {
			for (int dx = -radius; dx <= radius; dx++) {
				for (int dz = -radius; dz <= radius; dz++) {
					if (Math.abs(dx) != radius && Math.abs(dz) != radius) {
						continue;
					}

					int x = targetPos.getX() + dx;
					int z = targetPos.getZ() + dz;
					int y = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z);
					BlockPos candidate = new BlockPos(x, y, z);

					if (!isSafeStandingPos(level, candidate)) {
						continue;
					}

					double distance = candidate.distSqr(targetPos);

					if (distance < bestDistance) {
						bestPos = candidate;
						bestDistance = distance;
					}
				}
			}

			if (bestPos != null) {
				return Optional.of(bestPos);
			}
		}

		return Optional.empty();
	}

	private static Optional<BlockPos> findSafeCavePos(ServerLevel level, BlockPos targetPos) {
		BlockPos bestPos = null;
		double bestDistance = Double.MAX_VALUE;
		int minY = level.getMinY() + 10;
		int maxY = level.getMaxY() - PortalFrame.OUTER_HEIGHT - 2;

		for (int radius = 0; radius <= SAFE_SEARCH_RADIUS / 2; radius += 4) {
			for (int dx = -radius; dx <= radius; dx += 4) {
				for (int dz = -radius; dz <= radius; dz += 4) {
					for (int dy = 0; dy <= 48; dy++) {
						int upY = targetPos.getY() + dy;
						int downY = targetPos.getY() - dy;

						if (upY <= maxY) {
							bestPos = chooseCloserSafePos(level, targetPos, new BlockPos(targetPos.getX() + dx, upY, targetPos.getZ() + dz), bestPos);
						}

						if (downY >= minY) {
							bestPos = chooseCloserSafePos(level, targetPos, new BlockPos(targetPos.getX() + dx, downY, targetPos.getZ() + dz), bestPos);
						}

						if (bestPos != null) {
							bestDistance = bestPos.distSqr(targetPos);
						}
					}
				}
			}

			if (bestPos != null && bestDistance <= (radius + 8) * (radius + 8)) {
				return Optional.of(bestPos);
			}
		}

		return Optional.ofNullable(bestPos);
	}

	private static BlockPos chooseCloserSafePos(ServerLevel level, BlockPos targetPos, BlockPos candidate, BlockPos currentBest) {
		if (!isSafeStandingPos(level, candidate)) {
			return currentBest;
		}

		if (currentBest == null || candidate.distSqr(targetPos) < currentBest.distSqr(targetPos)) {
			return candidate;
		}

		return currentBest;
	}

	private static boolean isSafeStandingPos(ServerLevel level, BlockPos pos) {
		if (pos.getY() <= level.getMinY() + 8 || pos.getY() >= level.getMaxY() - PortalFrame.OUTER_HEIGHT - 2) {
			return false;
		}

		BlockState floor = level.getBlockState(pos.below());

		if (floor.isAir() || floor.liquid()) {
			return false;
		}

		return isReplaceableAir(level, pos) && isReplaceableAir(level, pos.above()) && isReplaceableAir(level, pos.above(2));
	}

	private static boolean isReplaceableAir(ServerLevel level, BlockPos pos) {
		BlockState state = level.getBlockState(pos);
		return state.isAir() || state.canBeReplaced();
	}

	private static BlockPos portalPos(BlockPos origin, Direction.Axis axis, int horizontal, int vertical) {
		return switch (axis) {
			case X -> origin.east(horizontal).above(vertical);
			case Z -> origin.south(horizontal).above(vertical);
			default -> origin;
		};
	}

	private static BlockPos pocketPos(BlockPos origin, Direction.Axis axis, int horizontal, int vertical, int depth) {
		return switch (axis) {
			case X -> origin.east(horizontal).north(depth).above(vertical);
			case Z -> origin.south(horizontal).east(depth).above(vertical);
			default -> origin;
		};
	}
}
