// (CVN) - Candy Valley Network GmbH

#pragma once

#include <CoreMinimal.h>

// EXTERNAL INCLUDES
#include <Components/ActorComponent.h>

// INTERNAL INCLUDES
#include "LovenseTypes.h"

#include "LovenseToyAnimationControlComponent.generated.h"


// Example of how a FLovenseGetAnimationData event function could be implemented. This is dummy code to get the general idea across.
#if false
	class Class {
		void LovenseGetAnimationData(TArray<class UAnimSequence*>& outAnimations, TArray<float>& outTimes);
		void CalculatePlayRate(float deltaTime, FRandomStream& random, float& playRate);
		void Tick(float deltaTime);

		TArray<class USkeletalMeshComponent> skeletalMeshComponents;
		TArray<class UAnimSequence*> animations;
		float currentAnimationTime;
		// It's important to use FRandomStream instead of the FMath functions as it is deterministic, allowing us to "look into the future".
		FRandomStream randomStream;
		float currentPlayRate;
		float currentTime;
		float currentPlayRateTarget;
	};

	void Class::LovenseGetAnimationData(TArray<class UAnimSequence*>& outAnimations, TArray<float>& outTimes) {
		outAnimations.Empty(this->skeletalMeshComponents.Num());
		outTimes.Empty(50);

		for (int32 i = 0; i < this->skeletalMeshComponents.Num(); ++i) {
			// Need one animation for every skeletal mesh component.
			outAnimations.Add(this->animations[i]);
		}

		// Filling outTimes is optional. If it's empty, the component will use a fixed time step of 0.1 seconds.

		// Cache any play rate values as we want to "look into the future" and not affect the current play rate.
		FRandomStream random = this->randomStream;
		float playRate = this->currentPlayRate;

		int32 toyDelay = FLovenseManager::Get()->GetToyDelay();
		float toyDelaySeconds = static_cast<float>(toyDelay) / 1000.0f;
		// Start at the current animation time + the toy delay to account for the communication delay.
		float animationTime = this->currentAnimationTime + toyDelaySeconds;
		float time = this->currentTime;
		float playRateTarget = this->currentPlayRateTarget;

		// Iterate animationTime until we have 51 time values as we can only have 50 pattern values and the first time value will be swallowed.
		while (outTimes.Num() < 51) {
			// Add current animation time to times array.
			outTimes.Add(animationTime);
			// Step the animation time forwards with the cached play rate.
			animationTime += 0.1f * playRate;
			// Tick the cached play rate with the cached random stream.
			this->CalculatePlayRate(0.1f, random, time, playRateTarget, playRate);
		}
	}

	void Class::CalculatePlayRate(float deltaTime, FRandomStream& random, float& time, float& playRateTarget, float& playRate) {
		// This part is important; we need to make sure to not get a value from the random stream faster than the pattern interval.
		// Otherwise we would get a different result as we'd get fewer values from the random stream when looking into the future
		// compared to the actual play rate update, which runs every tick.
		// Alternatively, you could update the actual play rate every >0.1 seconds instead of every tick.
		time += deltaTime;
		if (time > 0.1f) {
			// This can be as simple or as complex as you want.
			playRateTarget = random.FRand();
		}

		playRate = FMath::FInterpTo(playRate, playRateTarget, deltaTime, 1.0f);
	}

	void Class::Tick(float deltaTime) {
		// Actually update the play rate.
		this->CalculatePlayRate(deltaTime, this->randomStream, this->currentTime, this->currentPlayRateTarget, this->currentPlayRate);
	}
#endif

/**
 * @brief Blueprint exposed event delegate used by ULovenseToyAnimationControlComponent to retrieve animation data from external code that uses this component.
 * \n Called every 4 seconds when the internal timer loops.
 * @param outAnimations (TArray<UAnimSequence*>&) The animations to be evaluated.
 * \n Needs one animation for each skeletal mesh component passed to ULovenseToyAnimationControlComponent::Initialize().
 * \n All animations need to have the same sequence length.
 * @param outTimes (TArray<float>&) Optional custom time values. If empty will use a fixed time step of 0.1 seconds.
 * \n If filled will iterate over these values and evaluate the animations at these time values. Useful if you need to take e.g. dynamic play rate variations into account.
 * \n The first time value will be swallowed.
 * \n Maximum number of time values is 51 (effectively 50, as first value will be swallowed).
 */
DECLARE_DYNAMIC_DELEGATE_TwoParams(FLovenseGetAnimationData, TArray<class UAnimSequence*>&, outAnimations, TArray<float>&, outTimes);


/**
 * @brief This component calculates Lovense command speeds based on the average bone movement of N bones on N skeletal mesh components and broadcasts commands to all connected toys.
 * \n The commands that are sent are ELovenseCommand::Pattern for Lovense Remote
 * and ELovenseCommand::Vibrate, ELovenseCommand::RotateClockwise, ELovenseCommand::RotateAntiClockwise, ELovenseCommand::AirAuto for Lovense Connect.
 * \n For ELovenseCommand::Pattern, a pattern will be generated every 4 seconds based on animation data that gets passed in via getAnimationDataDelegate.
 * This mode is called "Pattern Generation".
 * \n For the Lovense Connect commands, we average the physics linear velocities of all bones in real time. This mode is called "Realtime Evaluation".
 * @note You will need to manually call OnGamePaused() / OnGameUnpaused() on this component to stop toys during game pause as there are no global events for that in unreal.
 * \n I recommend creating a child class from this to handle that.
 *
 * ~~~
 * This diagram depicts which methods should be called on the ULovenseToyAnimationControlComponent and when.
 * 
 *                 +-----------+                +---------------------------------------+
 *                 | External  |                | ULovenseToyAnimationControlComponent  |
 *                 +-----------+                +---------------------------------------+
 *                       |                                          |
 *                       | Initialize()                             |
 *                       |----------------------------------------->|
 *                       |                                          |
 *                       | StartBehaviour()                         |
 *                       |----------------------------------------->|
 *                       |                                          |
 *                       |                                          |
 *                       |                                          |
 *                       | Animation Loop                           |
 *                       |---------------                           |
 *                       |              |                           |
 *                       |<--------------                           |
 *                       |                                          |
 *                       |                                          |
 *                       |                                          |
 *                       | StartTransition()                        |
 *                       |----------------------------------------->|
 *                       |                                          |
 *                       | Animation Transition                     |
 *                       |---------------------                     |
 *                       |                    |                     |
 *                       |<--------------------                     |
 *                       |                                          |
 *                       | StartBehaviour()                         |
 *                       |----------------------------------------->|
 *                       |                                          |
 *                       |                                          |
 *   ------------------\ |                                          |
 *   | Game was paused |-|                                          |
 *   |-----------------| | OnGamePaused()                           |
 *                       |----------------------------------------->|
 * --------------------\ |                                          |
 * | Game was unpaused |-|                                          |
 * |-------------------| | OnGameUnpaused()                         |
 *                       |----------------------------------------->|
 *                       |                                          |
 *                       |                                          |
 *          -----------\ |                                          |
 *          | Finished |-|                                          |
 *          |----------| | SetEnabled(false)                        |
 *                       |----------------------------------------->|
 *                       |                                          |
 * ~~~
 */
UCLASS(ClassGroup = "Lovense", Meta = (BlueprintSpawnableComponent))
class LOVENSEINTEGRATION_API ULovenseToyAnimationControlComponent : public UActorComponent {
	GENERATED_BODY()

public:
	/**
	 * @brief Initializes the component with required data. Can be called multiple times.
	 * @note Will also disable the component, so you will need to manually call StartBehaviour() (for first initialization) or SetEnabled(true).
	 * @param inSkeletalMeshComponents The skeletal meshes that will be evaluated. All of these components have to use the same skeleton or have all bones defined in boneNames.
	 * @param inBoneNames The names of the bones of the skeletal meshes that will be evaluated.
	 * \n This component calculates an average velocity across these bones of all skeletal meshes, which is then converted to a toy command speed.
	 * @param inGetAnimationDataDelegate Event delegate called once every 4 seconds to retrieve animation data. See FLovenseCalculateAnimationTime for more information.
	 */
	UFUNCTION(BlueprintCallable, Category = "Lovense")
	virtual void Initialize(
		const TArray<USkeletalMeshComponent*>& inSkeletalMeshComponents,
		const TArray<FName>& inBoneNames,
		const FLovenseGetAnimationData& inGetAnimationDataDelegate
	);

	/**
	 * @brief Resets running values and calls SetEnabled(true).
	 * \n Should be called when first enabling this component and after each animation transition (See StartTransition()).
	 */
	UFUNCTION(BlueprintCallable, Category = "Lovense")
	virtual void StartBehaviour();

	/**
	 * @brief Sets max toy speed to maxToySpeedTargetDuringTransition and flips rotation direction.
	 * \n Should be called before blending to a new animation, though it's not necessary.
	 * \n This will prevent the toy from freaking out due to high bone velocities during an animation blend.
	 * \n The Rotation direction change only works for Lovense Connect, as rotation direction can't be controlled with Lovense Remote.
	 * \n Call StartBehaviour() once transition has finished to reset.
	 */
	UFUNCTION(BlueprintCallable, Category = "Lovense")
	virtual void StartTransition();

	/**
	 * @brief Enable/Disable the component. Component will not run and no commands will be sent while the component is disabled.
	 * @param value If false, realtime evaluation will be stopped, pattern generation timer will be stopped and all commands cleared.
	 * \n If true, realtime evaluation will be (re)started, will immediately generate and send a pattern command and (re)start the generate pattern timer.
	 */
	UFUNCTION(BlueprintCallable, Category = "Lovense")
	virtual void SetEnabled(bool value);

	/** @return Whether the component is currently enabled. Component will not run and no commands will be sent while the component is disabled. */
	UFUNCTION(BlueprintPure, Category = "Lovense")
	FORCEINLINE bool IsEnabled() { return this->bIsEnabled; }

	/**
	 * @brief Disables the component when the game has been paused to stop the toys.
	 * @note You will need to manually call this as there are no global events for that in unreal.
	 */
	UFUNCTION(BlueprintCallable, Category = "Lovense")
	virtual void OnGamePaused();

	/**
	 * @brief Enables the component when the game has been unpaused.
	 * @note You will need to manually call this as there are no global events for that in unreal.
	 */
	UFUNCTION(BlueprintCallable, Category = "Lovense")
	virtual void OnGameUnpaused();

public:
	ULovenseToyAnimationControlComponent();

	// UActorComponent Interface
	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
	virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
	// End UActorComponent Interface

	/**
	 * @brief Get the speed (per second) at which maxRecordedVelocity will decay at.
	 * \n Only relevant for realtime evaluation.
	 */
	FORCEINLINE float GetMaxRecordedVelocityDecaySpeed() { return this->maxRecordedVelocityDecaySpeed; }
	/**
	 * @brief Set the speed (per second) at which maxRecordedVelocity will decay at.
	 * \n Only relevant for realtime evaluation.
	 */
	FORCEINLINE void SetMaxRecordedVelocityDecaySpeed(float newDecaySpeed) { this->maxRecordedVelocityDecaySpeed = newDecaySpeed; }
	/**
	 * @brief Naming is a bit confusing, this is the minimum value that maxRecordedVelocity can decay to.
	 * \n Only relevant for realtime evaluation.
	 */
	FORCEINLINE float GetMaxRecordedVelocityDecayMinimum() { return this->maxRecordedVelocityDecayMinimum; }
	/**
	 * @brief Naming is a bit confusing, this is the minimum value that maxRecordedVelocity can decay to.
	 * \n Only relevant for realtime evaluation.
	 */
	FORCEINLINE void SetMaxRecordedVelocityDecayMinimum(float newDecayMinimum) { this->maxRecordedVelocityDecayMinimum = newDecayMinimum; }
	/**
	 * @brief Get interpolation speed at which currentMaxToySpeed will be interpolated towards maxToySpeedTarget.
	 * \n Only relevant for realtime evaluation.
	 */
	FORCEINLINE float GetMaxToySpeedInterpolationSpeed() { return this->maxToySpeedInterpolationSpeed; }
	/**
	 * @brief Set interpolation speed at which currentMaxToySpeed will be interpolated towards maxToySpeedTarget.
	 * \n Only relevant for realtime evaluation.
	 */
	FORCEINLINE void SetMaxToySpeedInterpolationSpeed(float newInterpolationSpeed) { this->maxToySpeedInterpolationSpeed = newInterpolationSpeed; }
	/**
	 * @brief Get maximum toy command speed that maxToySpeedTarget will be set to during transitions (See StartTransition()).
	 * \n Value is between 1-20.
	 * \n Only relevant for realtime evaluation.
	 */
	FORCEINLINE float GetMaxToySpeedTargetDuringTransition() { return this->maxToySpeedTargetDuringTransition; }
	/**
	 * @brief Set maximum toy command speed that maxToySpeedTarget will be set to during transitions (See StartTransition()).
	 * \n Value should be between 1-20.
	 * \n Only relevant for realtime evaluation.
	 */
	FORCEINLINE void SetMaxToySpeedTargetDuringTransition(float newSpeedTargetDuringTransition) { this->maxToySpeedTargetDuringTransition = newSpeedTargetDuringTransition; }
	/**
	 * @brief Get input value range for the average velocity to toy command speed conversion.
	 * \n E.g. if X = 5cm/s and Y = 15cm/s, toy command speed will be 1 if average velocity is <=5cm/s and 20 if average velocity is >= 15cm/s.
	 * \n Values are >=0 and X<=Y.
	 * \n Only relevant for pattern generation.
	 */
	FORCEINLINE FVector2D GetPatternToySpeedInputRange() { return this->patternToySpeedInputRange; }
	/**
	 * @brief Set input value range for the average velocity to toy command speed conversion.
	 * \n E.g. if X = 5cm/s and Y = 15cm/s, toy command speed will be 1 if average velocity is <=5cm/s and 20 if average velocity is >= 15cm/s.
	 * \n Values should be >=0 and X<=Y.
	 * \n Only relevant for pattern generation.
	 */
	FORCEINLINE void SetPatternToySpeedInputRange(FVector2D newInputRange) { this->patternToySpeedInputRange = newInputRange; }
	/**
	 * @brief Get the skeletal meshes that will be evaluated. All of these components have to use the same skeleton or have all bones defined in boneNames.
	 * \n Gets passed in via Initialize().
	 */
	FORCEINLINE const TArray<USkeletalMeshComponent*>& GetSkeletalMeshComponents() { return this->skeletalMeshComponents; }
	/**
	 * @brief Get the names of the bones of the skeletal meshes that will be evaluated.
	 * \n This component calculates an average velocity across these bones of all skeletal meshes, which is then converted to a toy command speed.
	 * \n Gets passed in via Initialize().
	 */
	FORCEINLINE const TArray<FName>& GetBoneNames() { return this->boneNames; }
	/**
	 * @brief Get the current maximum velocity that has been calculated. Used to remap the current velocity to a toy command speed value.
	 * \n This will slowly decay if the current velocity is smaller. Decay speed is defined by maxRecordedVelocityDecaySpeed.
	 * \n Only relevant for realtime evaluation.
	 */
	FORCEINLINE float GeMaxRecordedVelocity() { return this->maxRecordedVelocity; }
	/**
	 * @brief Get the current maximum toy command speed.
	 * \n Is interpolated towards maxToySpeedTarget with maxToySpeedInterpolationSpeed.
	 * \n Only relevant for realtime evaluation.
	 */
	FORCEINLINE float GeCurrentMaxToySpeed() { return this->currentMaxToySpeed; }
	/**
	 * @brief Get the target value that currentMaxToySpeed gets interpolated to.
	 * \n Is usually 20, but will be dropped to maxToySpeedTargetDuringTransition during transitions (see StartTransition()).
	 * \n Only relevant for realtime evaluation.
	 */
	FORCEINLINE float GeMaxToySpeedTarget() { return this->maxToySpeedTarget; }
	/**
	 * @brief Whether the toys should rotate clockwise or anticlockwise.
	 * \n Gets toggled in StartTransition().
	 * \n Only relevant for realtime evaluation.
	 */
	FORCEINLINE bool GeRotateClockwise() { return this->bRotateClockwise; }

protected:
	/**
	 * @brief Called every 4 seconds when the generate pattern timer loops.
	 * \n Generates a pattern based on animation data passed in via getAnimationDataDelegate and sends a pattern command with the generated pattern.
	 */
	UFUNCTION()
	virtual void OnPatternGenerationTimerFinished();

protected:
	/**
	 * @brief Called every tick.
	 * \n Calculates current toy command speed based on the average physics linear velocity of all boneNames of all skeletalMeshComponents and sends Lovense Connect commands.
	 */
	virtual void RealtimeEvaluationUpdate(float deltaTime);
	/**
	 * @brief Called by OnPatternGenerationTimerFinished().
	 * \n Generates a pattern based on animation data passed in via getAnimationDataDelegate.
	 * @return Only returns false if input data is invalid.
	 */
	virtual bool GeneratePattern(const TArray<class UAnimSequence*>& animations, const TArray<float>& times, FLovensePattern& outPattern);

protected:
	/**
	 * @brief Maximum velocity the average velocity of all bones on all skeletal mesh components will be capped to.
	 * \n Should be as small as possible to prevent situations where it takes a long time for maxRecordedVelocity to decay.
	 * \n Only relevant for realtime evaluation.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lovense|Realtime Evaluation")
	double maxAverageVelocity;

	/**
	 * @brief The speed (per second) at which maxRecordedVelocity will decay at.
	 * \n Only relevant for realtime evaluation.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lovense|Realtime Evaluation")
	float maxRecordedVelocityDecaySpeed;

	/**
	 * @brief Naming is a bit confusing, this is the minimum value that maxRecordedVelocity can decay to.
	 * \n Only relevant for realtime evaluation.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lovense|Realtime Evaluation")
	float maxRecordedVelocityDecayMinimum;

	/**
	 * @brief Interpolation speed at which currentMaxToySpeed will be interpolated towards maxToySpeedTarget.
	 * \n Only relevant for realtime evaluation.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lovense|Realtime Evaluation")
	float maxToySpeedInterpolationSpeed;

	/**
	 * @brief Maximum toy command speed that maxToySpeedTarget will be set to during transitions (See StartTransition()).
	 * \n Value should be between 1-20.
	 * \n Only relevant for realtime evaluation.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lovense|Realtime Evaluation", Meta = (ClampMin = "1", ClampMax = "20", UIMin = "1", UIMax = "20"))
	float maxToySpeedTargetDuringTransition;

	/**
	 * @brief Input value range for the average velocity to toy command speed conversion.
	 * \n E.g. if X = 5cm/s and Y = 15cm/s, toy command speed will be 1 if average velocity is <=5cm/s and 20 if average velocity is >= 15cm/s.
	 * \n Values should be >=0 and X<=Y.
	 * \n Only relevant for pattern generation.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lovense|Pattern Generation")
	FVector2D patternToySpeedInputRange;

	/**
	 * @brief The skeletal meshes that will be evaluated. All of these components have to use the same skeleton or have all bones defined in boneNames.
	 * \n Gets passed in via Initialize().
	 */
	UPROPERTY(Transient, VisibleAnywhere, BlueprintReadOnly, Category = "Lovense")
	TArray<TObjectPtr<USkeletalMeshComponent>> skeletalMeshComponents;

	/**
	 * @brief The names of the bones of the skeletal meshes that will be evaluated.
	 * \n This component calculates an average velocity across these bones of all skeletal meshes, which is then converted to a toy command speed.
	 * \n Gets passed in via Initialize().
	 */
	UPROPERTY(Transient, VisibleAnywhere, BlueprintReadOnly, Category = "Lovense")
	TArray<FName> boneNames;

	/**
	 * @brief The current maximum velocity that has been calculated. Used to remap the current velocity to a toy command speed value.
	 * \n This will slowly decay if the current velocity is smaller. Decay speed is defined by maxRecordedVelocityDecaySpeed.
	 * \n Only relevant for realtime evaluation.
	 */
	UPROPERTY(Transient, VisibleAnywhere, BlueprintReadOnly, Category = "Lovense|Realtime Evaluation")
	double maxRecordedVelocity;

	/**
	 * @brief The current maximum toy command speed.
	 * \n Is interpolated towards maxToySpeedTarget with maxToySpeedInterpolationSpeed.
	 * \n Only relevant for realtime evaluation.
	 */
	UPROPERTY(Transient, VisibleAnywhere, BlueprintReadOnly, Category = "Lovense|Realtime Evaluation")
	float currentMaxToySpeed;

	/**
	 * @brief The target value that currentMaxToySpeed gets interpolated to.
	 * \n Is usually 20, but will be dropped to maxToySpeedTargetDuringTransition during transitions (see StartTransition()).
	 * \n Only relevant for realtime evaluation.
	 */
	UPROPERTY(Transient, VisibleAnywhere, BlueprintReadOnly, Category = "Lovense|Realtime Evaluation")
	float maxToySpeedTarget;

	/**
	 * @brief Whether the toys should rotate clockwise or anticlockwise.
	 * \n Gets toggled in StartTransition().
	 * \n Only relevant for realtime evaluation.
	 */
	UPROPERTY(Transient, VisibleAnywhere, BlueprintReadOnly, Category = "Lovense|Realtime Evaluation")
	bool bRotateClockwise;

protected:
	/**  @brief Whether the component is currently enabled. Component will not run and no commands will be sent while the component is disabled. */
	bool bIsEnabled;
	/**
	 * @brief Handle for the pattern generation timer.
	 * \n Timer is looping with an interval of 4 seconds.
	 * \n Will generate and send a pattern command on each iteration.
	 * \n Only relevant for pattern generation.
	 */
	FTimerHandle patternGenerationTimerHandle;
	/**
	 * @brief Executed every 4 seconds in OnPatternGenerationTimerFinished() when pattern generation timer loops
	 * to retrieve animation data from external code that uses this component.
	 * \n Gets passed in via Initialize().
	 * \n Only relevant for pattern generation.
	 */
	FLovenseGetAnimationData getAnimationDataDelegate;
};
