package fr.koka99cab.spiderman.grapple;

import fr.koka99cab.spiderman.entity.GrapplingHookEntity;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Input;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.Vec3;

public final class GrappleController {
	private static final Map<UUID, GrappleSession> ACTIVE = new HashMap<>();

	private static final double CAST_SPEED = 2.35D;
	private static final double CAST_PLAYER_VELOCITY_FACTOR = 0.9D;
	private static final double MAX_INITIAL_HOOK_SPEED = 4.25D;
	private static final double CAST_FORWARD_OFFSET = 0.65D;
	private static final double CAST_DOWN_OFFSET = 0.18D;
	private static final double INITIAL_SLACK = 5.0D;
	private static final double HELD_ROPE_SLACK = 0.02D;
	private static final double ROPE_TAUT_TOLERANCE = 0.12D;
	private static final double MIN_ROPE_LENGTH = 1.0D;
	private static final double MAX_EXTRA_ROPE = 64.0D;
	private static final double MAX_SESSION_DISTANCE = 152.0D;
	private static final double SWING_PUMP = 0.055D;
	private static final double REEL_IN_SPEED = 0.18D;
	private static final double STUCK_REEL_IN_SPEED = 0.24D;
	private static final double STUCK_SPEED_SQ = 0.018D;
	private static final int STUCK_REEL_DELAY = 8;
	private static final double PENDULUM_GRAVITY = 0.08D;
	private static final double SWING_DAMPING = 0.998D;
	private static final double FALL_INERTIA_TO_SWING = 1.22D;
	private static final double CATCH_LIFT_FROM_SPEED = 0.18D;
	private static final double CATCH_LIFT_FROM_FALL = 0.22D;
	private static final double MAX_CATCH_LIFT = 0.72D;
	private static final double MAX_SWING_SPEED = 6.5D;
	private static final double PLAYER_CENTER_FACTOR = 0.5D;
	private static final double RELEASE_BOOST_SPEED = 0.85D;
	private static final double RELEASE_BOOST_FROM_SWING_SPEED = 0.12D;
	private static final double MAX_RELEASE_SPEED_BOOST = 0.65D;
	private static final double MAX_RELEASE_SPEED = 8.0D;
	private static final double MIN_RELEASE_SWING_SPEED_SQ = 0.05D;

	private GrappleController() {
	}

	public static void register() {
		ServerTickEvents.END_SERVER_TICK.register(server -> {
			Set<UUID> onlinePlayers = new HashSet<>();

			for (ServerPlayer player : server.getPlayerList().getPlayers()) {
				onlinePlayers.add(player.getUUID());
				tick(player);
			}

			ACTIVE.keySet().removeIf(uuid -> !onlinePlayers.contains(uuid));
		});
	}

	public static boolean start(ServerLevel level, ServerPlayer player, InteractionHand hand, ItemStack stack) {
		GrappleSession existingSession = ACTIVE.get(player.getUUID());
		if (existingSession != null) {
			stop(player, true);
			playRetrieveSound(level, player);
			return true;
		}

		Vec3 look = player.calculateViewVector(player.getXRot(), player.getYRot()).normalize();
		Vec3 startPosition = player.getEyePosition().add(look.scale(CAST_FORWARD_OFFSET)).add(0.0D, -CAST_DOWN_OFFSET, 0.0D);
		Vec3 velocity = initialHookVelocity(player, look);
		GrapplingHookEntity hook = new GrapplingHookEntity(level, player, startPosition, velocity);

		level.addFreshEntity(hook);
		ACTIVE.put(player.getUUID(), new GrappleSession(level.dimension(), startPosition, BlockPos.containing(startPosition), hook.getId()));

		stack.hurtAndBreak(1, player, hand);
		level.playSound(null, player.getX(), player.getY(), player.getZ(), SoundEvents.FISHING_BOBBER_THROW, SoundSource.PLAYERS, 0.8F, 1.15F);
		return true;
	}

	public static void attach(ServerPlayer player, GrapplingHookEntity hook, BlockHitResult hitResult) {
		GrappleSession session = ACTIVE.get(player.getUUID());
		if (session == null || session.visualHookId != hook.getId()) {
			hook.discard();
			return;
		}

		Vec3 anchorPosition = hitResult.getLocation();
		Vec3 playerCenter = playerCenter(player);

		session.anchored = true;
		session.anchorPosition = anchorPosition;
		session.anchorBlockPos = hitResult.getBlockPos();
		session.ropeLength = playerCenter.distanceTo(anchorPosition) + INITIAL_SLACK;
		session.maxRopeLength = session.ropeLength + MAX_EXTRA_ROPE;
		session.swingVelocity = player.getDeltaMovement();
		session.held = false;
		session.ropeTaut = false;
		session.manualReel = false;
		session.jumpHeldLastTick = false;
		session.stillTicks = 0;

		levelSound(player, SoundEvents.LEAD_TIED, 0.8F, 1.1F);
	}

	public static void expire(ServerPlayer player, GrapplingHookEntity hook) {
		GrappleSession session = ACTIVE.get(player.getUUID());
		if (session != null && session.visualHookId == hook.getId() && !session.anchored) {
			stop(player);
			playMissSound((ServerLevel) player.level(), player);
		}
	}

	public static boolean stop(ServerPlayer player) {
		return stop(player, false);
	}

	private static boolean stop(ServerPlayer player, boolean boostIfJumping) {
		GrappleSession session = ACTIVE.remove(player.getUUID());
		if (session == null) {
			return false;
		}

		if (boostIfJumping) {
			applyReleaseBoost(player, session);
		}

		discardVisualHook((ServerLevel) player.level(), session);
		return true;
	}

	private static void tick(ServerPlayer player) {
		GrappleSession session = ACTIVE.get(player.getUUID());
		if (session == null) {
			return;
		}

		if (!canKeepGrappling(player, session)) {
			stop(player);
			return;
		}

		Entity visualHook = ((ServerLevel) player.level()).getEntity(session.visualHookId);
		if (!(visualHook instanceof GrapplingHookEntity hook)) {
			ACTIVE.remove(player.getUUID());
			return;
		}

		if (!session.anchored) {
			tickFlyingHook(player, session, hook);
			return;
		}

		Vec3 center = playerCenter(player);
		double distance = center.distanceTo(session.anchorPosition);
		if (distance > MAX_SESSION_DISTANCE || distance < 0.001D) {
			stop(player);
			return;
		}

		Input input = player.getLastClientInput();
		updateReelState(session, input);
		if (!shouldTensionRope(session, distance)) {
			session.held = false;
			session.ropeTaut = false;
			session.swingVelocity = player.getDeltaMovement();
			session.jumpHeldLastTick = session.manualReel;
			return;
		}

		boolean firstCatch = tensionRope(session, distance) || !session.ropeTaut;
		reelInIfRequested(session);
		simulatePendulum(player, session, center, firstCatch);
		session.ropeTaut = true;
		session.jumpHeldLastTick = session.manualReel;

		player.resetFallDistance();
	}

	private static void tickFlyingHook(ServerPlayer player, GrappleSession session, GrapplingHookEntity hook) {
		session.anchorPosition = hook.position();
		session.anchorBlockPos = hook.blockPosition();
		session.swingVelocity = player.getDeltaMovement();
	}

	private static void updateReelState(GrappleSession session, Input input) {
		if (session.swingVelocity.lengthSqr() < STUCK_SPEED_SQ) {
			session.stillTicks++;
		} else {
			session.stillTicks = 0;
		}

		session.manualReel = input.jump();
	}

	private static boolean shouldTensionRope(GrappleSession session, double distance) {
		return session.manualReel || session.ropeTaut || distance >= session.ropeLength - ROPE_TAUT_TOLERANCE;
	}

	private static boolean tensionRope(GrappleSession session, double distance) {
		boolean firstHold = session.manualReel && !session.jumpHeldLastTick;
		if (firstHold || (session.manualReel && !session.held)) {
			double heldLength = Math.max(MIN_ROPE_LENGTH, distance + HELD_ROPE_SLACK);
			session.ropeLength = Math.min(session.ropeLength, heldLength);
		}

		session.held = session.manualReel;
		return firstHold;
	}

	private static void reelInIfRequested(GrappleSession session) {
		if (session.manualReel) {
			double reelSpeed = session.stillTicks >= STUCK_REEL_DELAY ? STUCK_REEL_IN_SPEED : REEL_IN_SPEED;
			session.ropeLength = Math.max(MIN_ROPE_LENGTH, session.ropeLength - reelSpeed);
		}
	}

	private static void simulatePendulum(ServerPlayer player, GrappleSession session, Vec3 center, boolean firstCatch) {
		Vec3 radial = center.subtract(session.anchorPosition).normalize();
		Vec3 currentCenter = session.anchorPosition.add(radial.scale(session.ropeLength));
		Vec3 swingVelocity = firstCatch ? preservedCatchVelocity(player, radial) : projectOntoSwingPlane(session.swingVelocity, radial);

		Vec3 gravity = new Vec3(0.0D, -PENDULUM_GRAVITY, 0.0D);
		swingVelocity = swingVelocity.add(projectOntoSwingPlane(gravity, radial));
		swingVelocity = swingVelocity.add(swingInput(player, radial)).scale(SWING_DAMPING);
		swingVelocity = limitSpeed(swingVelocity);

		Vec3 nextCenter = currentCenter.add(swingVelocity);
		Vec3 nextRadial = nextCenter.subtract(session.anchorPosition);
		if (nextRadial.lengthSqr() < 0.001D) {
			nextRadial = radial;
		} else {
			nextRadial = nextRadial.normalize();
		}

		Vec3 constrainedCenter = session.anchorPosition.add(nextRadial.scale(session.ropeLength));
		Vec3 actualVelocity = limitSpeed(constrainedCenter.subtract(currentCenter));
		double centerOffset = player.getBbHeight() * PLAYER_CENTER_FACTOR;

		session.swingVelocity = actualVelocity;
		player.snapTo(constrainedCenter.x, constrainedCenter.y - centerOffset, constrainedCenter.z);
		player.setDeltaMovement(actualVelocity);
		player.hurtMarked = true;
	}

	private static Vec3 preservedCatchVelocity(ServerPlayer player, Vec3 radial) {
		Vec3 incomingVelocity = player.getDeltaMovement();
		Vec3 tangentVelocity = projectOntoSwingPlane(incomingVelocity, radial).add(catchLift(radial, incomingVelocity));
		double fallSpeed = Math.max(0.0D, -incomingVelocity.y);
		double outwardSpeed = Math.max(0.0D, incomingVelocity.dot(radial));
		double targetSwingSpeed = Math.max(fallSpeed, outwardSpeed) * FALL_INERTIA_TO_SWING;

		if (targetSwingSpeed <= tangentVelocity.length()) {
			return tangentVelocity;
		}

		Vec3 swingDirection = catchSwingDirection(player, radial, tangentVelocity);
		return swingDirection.scale(targetSwingSpeed);
	}

	private static Vec3 catchLift(Vec3 radial, Vec3 incomingVelocity) {
		double speed = incomingVelocity.length();
		if (speed < 0.08D) {
			return Vec3.ZERO;
		}

		Vec3 upwardTangent = projectOntoSwingPlane(new Vec3(0.0D, 1.0D, 0.0D), radial);
		if (upwardTangent.lengthSqr() < 0.001D) {
			return Vec3.ZERO;
		}

		double fallSpeed = Math.max(0.0D, -incomingVelocity.y);
		double lift = Math.min(MAX_CATCH_LIFT, speed * CATCH_LIFT_FROM_SPEED + fallSpeed * CATCH_LIFT_FROM_FALL);
		return upwardTangent.normalize().scale(lift);
	}

	private static Vec3 swingInput(ServerPlayer player, Vec3 radial) {
		Vec3 tangentIntent = projectOntoSwingPlane(player.getLastClientMoveIntent(), radial);
		if (tangentIntent.lengthSqr() < 0.001D) {
			return Vec3.ZERO;
		}

		return tangentIntent.normalize().scale(SWING_PUMP);
	}

	private static Vec3 catchSwingDirection(ServerPlayer player, Vec3 radial, Vec3 tangentVelocity) {
		if (tangentVelocity.lengthSqr() > 0.001D) {
			return tangentVelocity.normalize();
		}

		Vec3 moveIntent = player.getLastClientMoveIntent();
		Vec3 tangentIntent = projectOntoSwingPlane(moveIntent, radial);
		if (tangentIntent.lengthSqr() > 0.001D) {
			return tangentIntent.normalize();
		}

		Vec3 look = player.calculateViewVector(player.getXRot(), player.getYRot());
		Vec3 tangentLook = projectOntoSwingPlane(look, radial);
		if (tangentLook.lengthSqr() > 0.001D) {
			return tangentLook.normalize();
		}

		Vec3 horizontalFallback = new Vec3(-radial.z, 0.0D, radial.x);
		if (horizontalFallback.lengthSqr() > 0.001D) {
			return horizontalFallback.normalize();
		}

		return new Vec3(1.0D, 0.0D, 0.0D);
	}

	private static Vec3 projectOntoSwingPlane(Vec3 direction, Vec3 radial) {
		return direction.subtract(radial.scale(direction.dot(radial)));
	}

	private static Vec3 limitSpeed(Vec3 velocity) {
		double speed = velocity.length();
		if (speed <= MAX_SWING_SPEED) {
			return velocity;
		}

		return velocity.scale(MAX_SWING_SPEED / speed);
	}

	private static void applyReleaseBoost(ServerPlayer player, GrappleSession session) {
		if (!player.getLastClientInput().sprint() || !canBoostOnRelease(session)) {
			return;
		}

		Vec3 currentVelocity = player.getDeltaMovement();
		Vec3 baseVelocity = currentVelocity.lengthSqr() >= session.swingVelocity.lengthSqr() ? currentVelocity : session.swingVelocity;
		Vec3 look = player.calculateViewVector(player.getXRot(), player.getYRot()).normalize();
		double baseSpeed = baseVelocity.length();
		double boostSpeed = RELEASE_BOOST_SPEED + Math.min(MAX_RELEASE_SPEED_BOOST, baseSpeed * RELEASE_BOOST_FROM_SWING_SPEED);
		Vec3 boostedVelocity = softLimitReleaseSpeed(baseVelocity.add(look.scale(boostSpeed)), baseSpeed + boostSpeed);

		player.setDeltaMovement(boostedVelocity);
		player.hurtMarked = true;
	}

	private static boolean canBoostOnRelease(GrappleSession session) {
		return session.ropeTaut || session.swingVelocity.lengthSqr() >= MIN_RELEASE_SWING_SPEED_SQ;
	}

	private static Vec3 softLimitReleaseSpeed(Vec3 velocity, double carriedSpeed) {
		double speed = velocity.length();
		double maxAllowedSpeed = Math.max(MAX_RELEASE_SPEED, carriedSpeed);
		if (speed <= maxAllowedSpeed) {
			return velocity;
		}

		return velocity.scale(maxAllowedSpeed / speed);
	}

	private static boolean canKeepGrappling(ServerPlayer player, GrappleSession session) {
		return player.isAlive()
				&& player.level().dimension() == session.dimension;
	}

	private static Vec3 playerCenter(ServerPlayer player) {
		return player.position().add(0.0D, player.getBbHeight() * PLAYER_CENTER_FACTOR, 0.0D);
	}

	private static Vec3 initialHookVelocity(ServerPlayer player, Vec3 look) {
		Vec3 velocity = look.scale(CAST_SPEED).add(player.getDeltaMovement().scale(CAST_PLAYER_VELOCITY_FACTOR));
		double speed = velocity.length();
		if (speed <= MAX_INITIAL_HOOK_SPEED) {
			return velocity;
		}

		return velocity.scale(MAX_INITIAL_HOOK_SPEED / speed);
	}

	private static void discardVisualHook(ServerLevel level, GrappleSession session) {
		Entity entity = level.getEntity(session.visualHookId);
		if (entity instanceof GrapplingHookEntity) {
			entity.discard();
		}
	}

	private static void playRetrieveSound(ServerLevel level, ServerPlayer player) {
		level.playSound(null, player.getX(), player.getY(), player.getZ(), SoundEvents.LEAD_UNTIED, SoundSource.PLAYERS, 0.9F, 1.0F);
	}

	private static void playMissSound(ServerLevel level, ServerPlayer player) {
		level.playSound(null, player.getX(), player.getY(), player.getZ(), SoundEvents.LEAD_UNTIED, SoundSource.PLAYERS, 0.35F, 0.8F);
	}

	private static void levelSound(ServerPlayer player, net.minecraft.sounds.SoundEvent soundEvent, float volume, float pitch) {
		player.level().playSound(null, player.getX(), player.getY(), player.getZ(), soundEvent, SoundSource.PLAYERS, volume, pitch);
	}

	private static final class GrappleSession {
		private final ResourceKey<Level> dimension;
		private Vec3 anchorPosition;
		private BlockPos anchorBlockPos;
		private double maxRopeLength;
		private final int visualHookId;
		private double ropeLength;
		private Vec3 swingVelocity = Vec3.ZERO;
		private boolean anchored;
		private boolean held;
		private boolean ropeTaut;
		private boolean manualReel;
		private boolean jumpHeldLastTick;
		private int stillTicks;

		private GrappleSession(ResourceKey<Level> dimension, Vec3 anchorPosition, BlockPos anchorBlockPos, int visualHookId) {
			this.dimension = dimension;
			this.anchorPosition = anchorPosition;
			this.anchorBlockPos = anchorBlockPos;
			this.visualHookId = visualHookId;
		}
	}
}
