/*
 * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
 * by the Epic Fight Team, licensed under GPLv3.
 * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
 */

package com.tiedup.remake.rig.anim.types;

import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import com.google.common.collect.Maps;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;

import io.netty.util.internal.StringUtil;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import com.tiedup.remake.rig.anim.AnimationClip;
import com.tiedup.remake.rig.anim.AnimationManager;
import com.tiedup.remake.rig.anim.AnimationManager.AnimationAccessor;
import com.tiedup.remake.rig.anim.AnimationVariables;
import com.tiedup.remake.rig.anim.AnimationVariables.IndependentAnimationVariableKey;
import com.tiedup.remake.rig.armature.JointTransform;
import com.tiedup.remake.rig.anim.Keyframe;
import com.tiedup.remake.rig.anim.Pose;
import com.tiedup.remake.rig.anim.TransformSheet;
import com.tiedup.remake.rig.anim.property.AnimationEvent;
import com.tiedup.remake.rig.anim.property.AnimationEvent.SimpleEvent;
import com.tiedup.remake.rig.anim.property.AnimationParameters;
import com.tiedup.remake.rig.anim.property.AnimationProperty;
import com.tiedup.remake.rig.anim.property.AnimationProperty.ActionAnimationProperty;
import com.tiedup.remake.rig.anim.property.AnimationProperty.PlaybackSpeedModifier;
import com.tiedup.remake.rig.anim.property.AnimationProperty.StaticAnimationProperty;
import com.tiedup.remake.rig.anim.types.EntityState.StateFactor;
import com.tiedup.remake.rig.asset.AssetAccessor;
import com.tiedup.remake.rig.asset.JsonAssetLoader;
import com.tiedup.remake.rig.anim.client.Layer;
import com.tiedup.remake.rig.anim.client.Layer.LayerType;
import com.tiedup.remake.rig.anim.client.property.ClientAnimationProperties;
import com.tiedup.remake.rig.anim.client.property.JointMaskEntry;
import com.tiedup.remake.rig.anim.client.property.TrailInfo;
import com.tiedup.remake.rig.exception.AssetLoadingException;
import com.tiedup.remake.rig.armature.Armature;
import com.tiedup.remake.rig.physics.ik.InverseKinematicsProvider;
import com.tiedup.remake.rig.physics.ik.InverseKinematicsSimulatable;
import com.tiedup.remake.rig.physics.ik.InverseKinematicsSimulator;
import com.tiedup.remake.rig.physics.ik.InverseKinematicsSimulator.BakedInverseKinematicsDefinition;
import com.tiedup.remake.rig.physics.ik.InverseKinematicsSimulator.InverseKinematicsObject;
import com.tiedup.remake.rig.util.datastruct.TypeFlexibleHashMap;
import com.tiedup.remake.rig.math.OpenMatrix4f;
import com.tiedup.remake.rig.math.Vec3f;
import com.tiedup.remake.rig.render.TiedUpRenderTypes;
import com.tiedup.remake.rig.TiedUpRigRegistry;
import com.tiedup.remake.rig.TiedUpRigConstants;
import com.tiedup.remake.rig.patch.LivingEntityPatch;
import com.tiedup.remake.rig.patch.PlayerPatch;

public class StaticAnimation extends DynamicAnimation implements InverseKinematicsProvider {
	public static final IndependentAnimationVariableKey<Boolean> HAD_NO_PHYSICS = AnimationVariables.independent((animator) -> false, true);
	
	public static String getFileHash(ResourceLocation rl) {
		String fileHash;
		
		try {
			JsonAssetLoader jsonfile = new JsonAssetLoader(AnimationManager.getAnimationResourceManager(), rl);
			fileHash = jsonfile.getFileHash();
		} catch (AssetLoadingException e) {
			fileHash = StringUtil.EMPTY_STRING;
		}
		
		return fileHash;
	}
	
	protected final Map<AnimationProperty<?>, Object> properties = Maps.newHashMap();
	
	/**
	 * States will bind into animation on {@link AnimationManager#apply}
	 */
	protected final StateSpectrum.Blueprint stateSpectrumBlueprint = new StateSpectrum.Blueprint();
	protected final StateSpectrum stateSpectrum = new StateSpectrum();
	protected final AssetAccessor<? extends Armature> armature;
	protected ResourceLocation resourceLocation;
	protected AnimationAccessor<? extends StaticAnimation> accessor;
	private final String filehash;
	
	public StaticAnimation() {
		super(0.0F, true);

		this.resourceLocation = TiedUpRigConstants.identifier("emtpy");
		this.armature = null;
		this.filehash = StringUtil.EMPTY_STRING;
	}
	
	public StaticAnimation(boolean isRepeat, AnimationAccessor<? extends StaticAnimation> accessor, AssetAccessor<? extends Armature> armature) {
		this(TiedUpRigConstants.GENERAL_ANIMATION_TRANSITION_TIME, isRepeat, accessor, armature);
	}
	
	public StaticAnimation(float transitionTime, boolean isRepeat, AnimationAccessor<? extends StaticAnimation> accessor, AssetAccessor<? extends Armature> armature) {
		super(transitionTime, isRepeat);
		
		this.resourceLocation = ResourceLocation.fromNamespaceAndPath(accessor.registryName().getNamespace(), "animmodels/animations/" + accessor.registryName().getPath() + ".json");
		
		this.armature = armature;
		this.accessor = accessor;
		this.filehash = getFileHash(this.resourceLocation);
	}
	
	/* Resourcepack animations — transitionTime par défaut */
	public StaticAnimation(boolean isRepeat, String path, AssetAccessor<? extends Armature> armature) {
		this(TiedUpRigConstants.GENERAL_ANIMATION_TRANSITION_TIME, isRepeat, path, armature);
	}

	/* Resourcepack animations */
	public StaticAnimation(float transitionTime, boolean isRepeat, String path, AssetAccessor<? extends Armature> armature) {
		super(transitionTime, isRepeat);
		
		ResourceLocation registryName = ResourceLocation.parse(path);
		this.resourceLocation = ResourceLocation.fromNamespaceAndPath(registryName.getNamespace(), "animmodels/animations/" + registryName.getPath() + ".json");
		this.armature = armature;
		this.filehash = StringUtil.EMPTY_STRING;
	}
	
	/* Multilayer Constructor */
	public StaticAnimation(ResourceLocation fileLocation, float transitionTime, boolean isRepeat, String registryName, AssetAccessor<? extends Armature> armature) {
		super(transitionTime, isRepeat);
		
		this.resourceLocation = fileLocation;
		this.armature = armature;
		this.filehash = StringUtil.EMPTY_STRING;
	}
	
	public void loadAnimation() {
		if (!this.isMetaAnimation()) {
			if (this.properties.containsKey(StaticAnimationProperty.IK_DEFINITION)) {
				this.animationClip = AnimationManager.getInstance().loadAnimationClip(this, JsonAssetLoader::loadAllJointsClipForAnimation);
				
				this.getProperty(StaticAnimationProperty.IK_DEFINITION).ifPresent(ikDefinitions -> {
					boolean correctY = this.getProperty(ActionAnimationProperty.MOVE_VERTICAL).orElse(false);
					boolean correctZ = this.isMainFrameAnimation();
					
					List<BakedInverseKinematicsDefinition> bakedIKDefinitionList = ikDefinitions.stream().map(ikDefinition -> ikDefinition.bake(this.armature, this.animationClip.getJointTransforms(), correctY, correctZ)).toList();
					this.addProperty(StaticAnimationProperty.BAKED_IK_DEFINITION, bakedIKDefinitionList);
					
					// Remove the unbaked data
					this.properties.remove(StaticAnimationProperty.IK_DEFINITION);
				});
			} else {
				this.animationClip = AnimationManager.getInstance().loadAnimationClip(this, JsonAssetLoader::loadClipForAnimation);
			}
			
			this.animationClip.bakeKeyframes();
		}
	}
	
	public void postInit() {
		this.stateSpectrum.readFrom(this.stateSpectrumBlueprint);
	}
	
	@Override
	public AnimationClip getAnimationClip() {
		if (this.animationClip == null) {
			this.loadAnimation();
		}
		
		return this.animationClip;
	}
	
	public void setLinkAnimation(final AssetAccessor<? extends DynamicAnimation> fromAnimation, Pose startPose, boolean isOnSameLayer, float transitionTimeModifier, LivingEntityPatch<?> entitypatch, LinkAnimation dest) {
		if (!entitypatch.isLogicalClient()) {
			startPose = TiedUpRigRegistry.EMPTY_ANIMATION.getPoseByTime(entitypatch, 0.0F, 1.0F);
		}
		
		dest.resetNextStartTime();
		
		float playTime = this.getPlaySpeed(entitypatch, dest);
		PlaybackSpeedModifier playSpeedModifier = this.getRealAnimation().get().getProperty(StaticAnimationProperty.PLAY_SPEED_MODIFIER).orElse(null);
		
		if (playSpeedModifier != null) {
			playTime = playSpeedModifier.modify(dest, entitypatch, playTime, 0.0F, playTime);
		}
		
		playTime = Math.abs(playTime);
		playTime *= TiedUpRigConstants.A_TICK;
		
		float linkTime = transitionTimeModifier > 0.0F ? transitionTimeModifier + this.transitionTime : this.transitionTime;
		float totalTime = playTime * (int)Math.ceil(linkTime / playTime);
		float nextStartTime = Math.max(0.0F, -transitionTimeModifier);
		nextStartTime += totalTime - linkTime;
		
		dest.setNextStartTime(nextStartTime);
		dest.getAnimationClip().reset();
		dest.setTotalTime(totalTime);
		dest.setConnectedAnimations(fromAnimation, this.getAccessor());
		
		Map<String, JointTransform> data1 = startPose.getJointTransformData();
		Map<String, JointTransform> data2 = this.getPoseByTime(entitypatch, nextStartTime, 0.0F).getJointTransformData();
		Set<String> joint1 = new HashSet<> (isOnSameLayer ? data1.keySet() : Set.of());
		Set<String> joint2 = new HashSet<> (data2.keySet());
		
		if (entitypatch.isLogicalClient()) {
			JointMaskEntry entry = fromAnimation.get().getJointMaskEntry(entitypatch, false).orElse(null);
			JointMaskEntry entry2 = this.getJointMaskEntry(entitypatch, true).orElse(null);
			
			if (entry != null) {
				joint1.removeIf((jointName) -> entry.isMasked(fromAnimation.get().getProperty(ClientAnimationProperties.LAYER_TYPE).orElse(Layer.LayerType.BASE_LAYER) == Layer.LayerType.BASE_LAYER ?
						entitypatch.getClientAnimator().currentMotion() : entitypatch.getClientAnimator().currentCompositeMotion(), jointName));
			}
			
			if (entry2 != null) {
				joint2.removeIf((jointName) -> entry2.isMasked(this.getProperty(ClientAnimationProperties.LAYER_TYPE).orElse(Layer.LayerType.BASE_LAYER) == Layer.LayerType.BASE_LAYER ?
						entitypatch.getCurrentLivingMotion() : entitypatch.currentCompositeMotion, jointName));
			}
		}
		
		joint1.addAll(joint2);
		
		if (linkTime != totalTime) {
			Map<String, JointTransform> firstPose = this.getPoseByTime(entitypatch, 0.0F, 0.0F).getJointTransformData();
			
			for (String jointName : joint1) {
				Keyframe[] keyframes = new Keyframe[3];
				keyframes[0] = new Keyframe(0.0F, data1.get(jointName));
				keyframes[1] = new Keyframe(linkTime, firstPose.get(jointName));
				keyframes[2] = new Keyframe(totalTime, data2.get(jointName));
				TransformSheet sheet = new TransformSheet(keyframes);
				dest.getAnimationClip().addJointTransform(jointName, sheet);
			}
		} else {
			for (String jointName : joint1) {
				Keyframe[] keyframes = new Keyframe[2];
				keyframes[0] = new Keyframe(0.0F, data1.get(jointName));
				keyframes[1] = new Keyframe(totalTime, data2.get(jointName));
				TransformSheet sheet = new TransformSheet(keyframes);
				dest.getAnimationClip().addJointTransform(jointName, sheet);
			}
		}
	}
	
	@Override
	public void begin(LivingEntityPatch<?> entitypatch) {
		// Load if null
		this.getAnimationClip();
		
		// Please fix this implementation when minecraft supports any mixinable method that returns noPhysics variable 
		this.getProperty(StaticAnimationProperty.NO_PHYSICS).ifPresent(val -> {
			if (val) {
				entitypatch.getAnimator().getVariables().put(HAD_NO_PHYSICS, this.getAccessor(), entitypatch.getOriginal().noPhysics);
				entitypatch.getOriginal().noPhysics = true;
			}
		});
		
		if (entitypatch.isLogicalClient()) {
			this.getProperty(ClientAnimationProperties.TRAIL_EFFECT).ifPresent(trailInfos -> {
				int idx = 0;
				
				for (TrailInfo trailInfo : trailInfos) {
					double eid = Double.longBitsToDouble((long)entitypatch.getOriginal().getId());
					double animid = Double.longBitsToDouble((long)this.getId());
					double jointId = Double.longBitsToDouble((long)this.armature.get().searchJointByName(trailInfo.joint()).getId());
					double index = Double.longBitsToDouble((long)idx++);
					
					// RIG : RenderItemBase (combat weapon item render) strippé —
					// TiedUp n'a pas d'items "actifs" porteurs de trails comme
					// les weapons EF. Le trailInfo reste tel quel de la définition.
					
					if (!trailInfo.playable()) {
						continue;
					}
					
					entitypatch.getOriginal().level().addParticle(trailInfo.particle(), eid, 0, animid, jointId, index, 0);
				}
			});
		}
		
		this.getProperty(StaticAnimationProperty.ON_BEGIN_EVENTS).ifPresent(events -> {
			for (SimpleEvent<?> event : events) {
				event.execute(entitypatch, this.getAccessor(), 0.0F, 0.0F);
			}
		});
		
		// RIG : PlayerEventListener (combat animation events) strippé.
		// Les ON_BEGIN_EVENTS SimpleEvent continuent de fonctionner ci-dessus.
	}

	@Override
	public void end(LivingEntityPatch<?> entitypatch, AssetAccessor<? extends DynamicAnimation> nextAnimation, boolean isEnd) {
		// RIG : ANIMATION_END_EVENT fire strippé (PlayerEventListener combat).
		
		this.getProperty(StaticAnimationProperty.ON_END_EVENTS).ifPresent((events) -> {
			for (SimpleEvent<?> event : events) {
				event.executeWithNewParams(entitypatch, this.getAccessor(), this.getTotalTime(), this.getTotalTime(), event.getParameters() == null ? AnimationParameters.of(isEnd) : AnimationParameters.addParameter(event.getParameters(), isEnd));
			}
		});
		
		this.getProperty(StaticAnimationProperty.NO_PHYSICS).ifPresent((val) -> {
			if (val) {
				entitypatch.getOriginal().noPhysics = entitypatch.getAnimator().getVariables().getOrDefault(HAD_NO_PHYSICS, this.getAccessor());
			}
		});
		
		entitypatch.getAnimator().getVariables().removeAll(this.getAccessor());
	}
	
	@Override
	public void tick(LivingEntityPatch<?> entitypatch) {
		this.getProperty(StaticAnimationProperty.NO_PHYSICS).ifPresent((val) -> {
			if (val) {
				entitypatch.getOriginal().noPhysics = true;
			}
		});
		
		this.getProperty(StaticAnimationProperty.TICK_EVENTS).ifPresent((events) -> {
			entitypatch.getAnimator().getPlayer(this.getAccessor()).ifPresent(player -> {
				for (AnimationEvent<?, ?> event : events) {
					float prevElapsed = player.getPrevElapsedTime();
					float elapsed = player.getElapsedTime();
					
					event.execute(entitypatch, this.getAccessor(), prevElapsed, elapsed);
				}
			});
		});
	}
	
	@Override
	public EntityState getState(LivingEntityPatch<?> entitypatch, float time) {
		return new EntityState(this.getStatesMap(entitypatch, time));
	}
	
	@Override
	public TypeFlexibleHashMap<StateFactor<?>> getStatesMap(LivingEntityPatch<?> entitypatch, float time) {
		return this.stateSpectrum.getStateMap(entitypatch, time);
	}
	
	@Override
	public <T> T getState(StateFactor<T> stateFactor, LivingEntityPatch<?> entitypatch, float time) {
		return this.stateSpectrum.getSingleState(stateFactor, entitypatch, time);
	}
	
	@Override
	public Optional<JointMaskEntry> getJointMaskEntry(LivingEntityPatch<?> entitypatch, boolean useCurrentMotion) {
		return this.getProperty(ClientAnimationProperties.JOINT_MASK);
	}
	
	@Override
	public void modifyPose(DynamicAnimation animation, Pose pose, LivingEntityPatch<?> entitypatch, float time, float partialTicks) {
		entitypatch.poseTick(animation, pose, time, partialTicks);
		
		this.getProperty(StaticAnimationProperty.POSE_MODIFIER).ifPresent((poseModifier) -> {
			poseModifier.modify(animation, pose, entitypatch, time, partialTicks);
		}); 
	}
	
	@Override
	public boolean isStaticAnimation() {
		return true;
	}
	
	@Override
	public boolean doesHeadRotFollowEntityHead() {
		return !this.getProperty(StaticAnimationProperty.FIXED_HEAD_ROTATION).orElse(false);
	}
	
	@Override
	public int getId() {
		return this.accessor.id();
	}
	
	@Override
	public boolean equals(Object obj) {
		if (obj instanceof StaticAnimation staticAnimation) {
			if (this.accessor != null && staticAnimation.accessor != null) {
				return this.getId() == staticAnimation.getId();
			}
		}
		
		return super.equals(obj);
	}
	
	public ResourceLocation getLocation() {
		return this.resourceLocation;
	}
	
	@Override
	public ResourceLocation getRegistryName() {
		return this.accessor.registryName();
	}
	
	public AssetAccessor<? extends Armature> getArmature() {
		return this.armature;
	}
	
	@Override
	public float getPlaySpeed(LivingEntityPatch<?> entitypatch, DynamicAnimation animation) {
		return 1.0F;
	}
	
	@Override
	public TransformSheet getCoord() {
		return this.getProperty(ActionAnimationProperty.COORD).orElse(super.getCoord());
	}
	
	@Override
	public String toString() {
		String classPath = this.getClass().toString();
		return classPath.substring(classPath.lastIndexOf(".") + 1) + " " + this.getLocation();
	}
	
	/**
	 * Internal use only
	 */
	@Deprecated
	public StaticAnimation addPropertyUnsafe(AnimationProperty<?> propertyType, Object value) {
		this.properties.put(propertyType, value);
		this.getSubAnimations().forEach((subAnimation) -> subAnimation.get().addPropertyUnsafe(propertyType, value));
		return this;
	}
	
	@SuppressWarnings("unchecked")
	public <A extends StaticAnimation, V> A addProperty(StaticAnimationProperty<V> propertyType, V value) {
		this.properties.put(propertyType, value);
		this.getSubAnimations().forEach((subAnimation) -> subAnimation.get().addProperty(propertyType, value));
		return (A)this;
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public <V> Optional<V> getProperty(AnimationProperty<V> propertyType) {
		return (Optional<V>) Optional.ofNullable(this.properties.get(propertyType));
	}
	
	@OnlyIn(Dist.CLIENT)
	public Layer.Priority getPriority() {
		return this.getProperty(ClientAnimationProperties.PRIORITY).orElse(Layer.Priority.LOWEST);
	}
	
	@OnlyIn(Dist.CLIENT)
	public Layer.LayerType getLayerType() {
		return this.getProperty(ClientAnimationProperties.LAYER_TYPE).orElse(LayerType.BASE_LAYER);
	}
	
	public Object getModifiedLinkState(StateFactor<?> factor, Object val, LivingEntityPatch<?> entitypatch, float elapsedTime) {
		return val;
	}
	
	public List<AssetAccessor<? extends StaticAnimation>> getSubAnimations() {
		return List.of();
	}
	
	@Override
	public AnimationAccessor<? extends StaticAnimation> getRealAnimation() {
		return this.getAccessor();
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public <A extends DynamicAnimation> AnimationAccessor<A> getAccessor() {
		return (AnimationAccessor<A>)this.accessor;
	}
	
	public void setAccessor(AnimationAccessor<? extends StaticAnimation> accessor) {
		this.accessor = accessor;
	}
	
	@OnlyIn(Dist.CLIENT)
	public void renderDebugging(PoseStack poseStack, MultiBufferSource buffer, LivingEntityPatch<?> entitypatch, float playTime, float partialTicks) {
		// RIG : debug render des targets IK (RenderingTool.drawQuad) strippé.
		// Pas d'IK en TiedUp. Reactivable Phase 2+ avec un helper drawQuad
		// simple si on veut debug les joints.
	}
	
	@Override
	public InverseKinematicsObject createSimulationData(InverseKinematicsProvider provider, InverseKinematicsSimulatable simOwner, InverseKinematicsSimulator.InverseKinematicsBuilder simBuilder) {
		return new InverseKinematicsObject(simBuilder);
	}
}
