package com.leisuretimedock.blasttravelreborn.mixin;

import com.leisuretimedock.blasttravelreborn.BlastTravelReborn;
import com.leisuretimedock.blasttravelreborn.api.events.PlayerCannonFlightTickEvent;
import com.leisuretimedock.blasttravelreborn.network.BTRNetwork;
import com.leisuretimedock.blasttravelreborn.network.toClient.SyncFlightStatePayload;
import com.leisuretimedock.blasttravelreborn.network.toServer.StopCannonFlightServerPayload;
import com.leisuretimedock.blasttravelreborn.util.PlayerEntityDuck;
import net.minecraft.client.Minecraft;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.util.Mth;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.entity.EntityDimensions;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Pose;
import net.minecraft.world.entity.player.Abilities;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.entity.EntityTypeTest;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.loading.FMLEnvironment;
import net.minecraftforge.network.NetworkDirection;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;

@Mixin(Player.class)
public abstract class PlayerEntityMixin extends LivingEntity implements PlayerEntityDuck {
	@Shadow
	public abstract boolean isLocalPlayer();

	@Shadow
	public abstract Abilities getAbilities();

	@Shadow public abstract boolean isSpectator();

	@Unique
	private static final EntityDimensions CANNON_FLIGHT_DIMENSIONS = EntityDimensions.scalable(0.6f, 0.6f);
	@Unique
	private Vec3 blasttravel$vel = Vec3.ZERO;
	@Unique
	private Vec3 blasttravel$trackingVel = Vec3.ZERO;
	@Unique
	private Vec3 blasttravel$prevVel = Vec3.ZERO;

	@Unique
	private boolean blasttravel$inCannonFlight = false;
	@Unique
	private boolean blasttravel$cancelFallDamage = false;

	@Unique
	private int blasttravel$ticksFlying = 0;

	protected PlayerEntityMixin(EntityType<? extends LivingEntity> entityType, Level world) {
		super(entityType, world);
	}

	@Inject(method = "tick", at = @At("HEAD"))
	private void blasttravel$beginTick(CallbackInfo ci) {
		this.blasttravel$prevVel = this.isLocalPlayer() ? blasttravel$vel : blasttravel$trackingVel;
	}

	@Inject(method = "tick", at = @At("TAIL"))
	private void blasttravel$endTick(CallbackInfo ci) {
		var self = this;
		this.blasttravel$vel = self.position().subtract(self.xo, self.yo, self.zo);

		if (this.blasttravel$inCannonFlight()) {
			this.noPhysics = true;
			MinecraftForge.EVENT_BUS.post(new PlayerCannonFlightTickEvent(self, blasttravel$vel, self.position(), blasttravel$ticksFlying));
			if (!self.level().isClientSide()) {
				var vel = self.getDeltaMovement();
				var frontBox = self.getBoundingBox().expandTowards(0.2, 0.2, 0.2);
				for (var entity : self.level().getEntities(EntityTypeTest.forClass(LivingEntity.class), frontBox, entity -> entity != self)) {
					if (!entity.isInvulnerable()) {
						DamageSource source = self.damageSources().source(
								ResourceKey.create(Registries.DAMAGE_TYPE, BlastTravelReborn.id("cannon")), self);
						entity.hurt(source, (float)(vel.length() * 4));
					}
				}
				try {
					ServerPlayer cast = ServerPlayer.class.cast(self);
					BTRNetwork.CHANNEL.sendTo(new SyncFlightStatePayload(self.getId(), vel, self.position(), true, noPhysics), cast.connection.connection, NetworkDirection.PLAY_TO_CLIENT);
				} catch(Exception ignored){}
			}

			if (self.isLocalPlayer() &&
					this.blasttravel$ticksFlying > 8 &&
					(self.onGround() || self.isFallFlying() || self.getAbilities().flying || self.isUnderWater())) {
				this.blasttravel$setCannonFlight(false);
				BTRNetwork.CHANNEL.sendToServer(new StopCannonFlightServerPayload(self.onGround()));
			}

			this.blasttravel$ticksFlying++;
		} else {
			this.blasttravel$ticksFlying = 0;
			if(!self.isSpectator()) this.noPhysics = false;
		}

		if (!self.isLocalPlayer()) {
			this.blasttravel$trackingVel = this.blasttravel$trackingVel.add(
					this.blasttravel$vel.subtract(this.blasttravel$trackingVel)
							.scale(1f / self.getType().updateInterval()));
		}
	}

	@Inject(method = "causeFallDamage", at = @At("HEAD"), cancellable = true)
	private void blasttravel$cancelFallDamage(float fallDistance, float damageMultiplier, DamageSource damageSource, CallbackInfoReturnable<Boolean> cir) {
		if (this.blasttravel$cancelFallDamage) {
			this.blasttravel$cancelFallDamage = false;
			cir.setReturnValue(false);
		}
	}

	@Inject(method = "getDimensions", at = @At("HEAD"), cancellable = true)
	private void blasttravel$setFlyingPose(Pose pose, CallbackInfoReturnable<EntityDimensions> cir) {
		if (this.blasttravel$inCannonFlight()) {
			cir.setReturnValue(CANNON_FLIGHT_DIMENSIONS);
		}
	}

	@Inject(method = "getStandingEyeHeight", at = @At("HEAD"), cancellable = true)
	private void blasttravel$setFlyingEyeHeight(Pose pose, EntityDimensions dimensions, CallbackInfoReturnable<Float> cir) {
		if (this.blasttravel$inCannonFlight()) {
			cir.setReturnValue(dimensions.height * 0.5f);
		}
	}

	@Override
	public void blasttravel$setCannonFlight(boolean inFlight) {
		var self = this;

		if (inFlight && !this.blasttravel$inCannonFlight) {
			this.blasttravel$vel =
					this.blasttravel$trackingVel =
							this.blasttravel$prevVel = self.getDeltaMovement();
			this.blasttravel$cancelFallDamage = true;

			self.setJumping(false);
		}

		this.blasttravel$inCannonFlight = inFlight;
		((LivingEntityAccess)this).blasttravel$setNoDrag(inFlight);

		self.refreshDimensions();
	}

	@Override
	public boolean blasttravel$inCannonFlight() {
		return this.blasttravel$inCannonFlight;
	}

	@Override
	public Vec3 blasttravel$getVelocityLerped(float delta) {
		var vel = this.isLocalPlayer() ? blasttravel$vel : blasttravel$trackingVel;
		return new Vec3(
				Mth.lerp(delta, blasttravel$prevVel.x, vel.x),
				Mth.lerp(delta, blasttravel$prevVel.y, vel.y),
				Mth.lerp(delta, blasttravel$prevVel.z, vel.z));
	}
}
