
#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "ScriptedAnimation.generated.h"

class UEnhancedInputLocalPlayerSubsystem;
class UAnimSequenceBase;


USTRUCT(BlueprintType)
struct INTEGRATION_API FlxWorldClocks
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
	double WorldTime = 0.0;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    double RealTime = 0.0;
};

USTRUCT(BlueprintType)
struct INTEGRATION_API FlxScriptedAnimation
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
	UAnimSequenceBase *Sequence = nullptr;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    bool ContinueWhenPaused = false;

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	int64 AnimationStepID = 0;

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	double FadeInDuration = 0.0;

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	double FadeOutDuration = 0.0;
	
    // StartTime and EndTime.
    // 
    // StartTime is the world time when the animation was actually started.
    // That is immutable once the animation is created.  However, EndTime is
    // mutable: initially, it is set to StartTime + Animation.Length.  But
    // script requests that the animation be truncated, the EndTime
    // may be reduced to implement the truncation.
    //

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
    double StartTime = 0.0;

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
    double EndTime = 0.0;

    // Calculate the progress of a fade.
    //
    inline static double CalculateFade(double Offset, double Fade)
    {
        if (Offset < 0.0)
        {
            return 0.0;
        }
        if (Fade > 0.001)
        {
            return FMath::Min(Offset / Fade, 1.0);
        }
        return 1.0;
    }

    // Given an FGameTime, extract the correct clock to use for this animation.
    //
    inline double ChooseCorrectClock(const FlxWorldClocks &WorldClocks) const
    {
        return ContinueWhenPaused ? WorldClocks.RealTime : WorldClocks.WorldTime;
    }

    // Calculate Elapsed Time (unclamped)
    //
    // If current time is before the animation's start time, then
    // elapsed time will be negative.
    //
    inline double UnclampedElapsedTime(double CurrentTime) const
    {
        return CurrentTime - StartTime;
    }

    // Calculate Time Left (unclamped)
    // 
    // If current time is after the animation's end time, then
    // time left will be negative.
    //
    inline double UnclampedTimeLeft(double CurrentTime) const
    {
        return EndTime - CurrentTime;
    }

    // Calculate Elapsed Time (clamped)
    //
    // If current time is before the animation's start time, then
    // elapsed time will be zero.
    //
    inline double ClampedElapsedTime(double CurrentTime) const
    {
        return FMath::Max(0.0, CurrentTime - StartTime);
    }

    // Calculate Time Left (clamped)
    // 
    // If current time is after the animation's end time, then
    // time left will be zero.
    //
    inline double ClampedTimeLeft(double CurrentTime) const
    {
        return FMath::Max(0.0, EndTime - CurrentTime);
    }

    // Calculate the progress of the fadein.
    //
    inline double CalcFadeInAlpha(double CurrentTime) const
    {
        return CalculateFade(UnclampedElapsedTime(CurrentTime), FadeInDuration);
    }

    // Calculate the progress of the fadeout.
    //
    inline double CalcFadeOutAlpha(double CurrentTime) const
    {
        return CalculateFade(UnclampedTimeLeft(CurrentTime), FadeOutDuration);
    }

    // Calculate the combined alpha of the fade-in-out.
    //
    inline double CalcFadeInOutAlpha(double CurrentTime) const
    {
        return FMath::Min(CalcFadeInAlpha(CurrentTime), CalcFadeOutAlpha(CurrentTime));
    }

    // Cause an animation to start fading right away.
    //
    // Sets up the animation to start fading immediately, by adjusting
    // the animation's EndTime.  If the FadeOut time of the animation
    // is longer than the allowed fade, that will be reduced.
    // If the animation was already in the process of fading in
    // or fading out, then that will be taken into account.
    //
    // If you want to chop an animation off abruptly, without any fade,
    // simply set AllowedFade to 0.0.
    //
    void InitiateFadeOut(double CurrentTime, double AllowedFade);
};


UCLASS(BlueprintType)
class INTEGRATION_API UlxScriptedAnimations : public UObject
{
	GENERATED_BODY()

private:
	UPROPERTY()
	int KeepCount = 3;

	UPROPERTY()
	TArray<FlxScriptedAnimation> Animations;

	// If the number of elements in the array exceeds the count, discard.
	//
	void Keep(int n);
	
public:
	// Allow read-only access anywhere.
	//
	const TArray<FlxScriptedAnimation> &GetAnimations() const { return Animations; }

	// Add a scripted animation.
	//
	// The step is inserted at the front of the array: the first animation
	// in the array is always the most recent.  All other steps are shifted
	// back.  If the array size exceeds the KeepCount, it is truncated.
	//
	// The AnimationStepID parameter can be used to associate a scripted
    // animation with a step in the animation queue.  Typically, you would
    // set the scripted animation ID equal to the animation queue step ID.
    // This makes it feasible to garbage collect the scripted animation
    // if the animation queue step disappears.
    //
    // If 'ContinueWhenPaused' is true, then the animation keeps playing
    // even when the game is paused.  Otherwise, it freezes when the game
    // is paused.
	//
	UFUNCTION(BlueprintCallable, Category = "Luprex|Scripted Animations", meta=(WorldContext = "WorldContextObject"))
	void AddAnimation(
		UObject *WorldContextObject, UAnimSequenceBase* Sequence, double FadeInTime = 0.2, double FadeOutTime = 0.2,
        int64 AnimationStepID=0, bool ContinueWhenPaused=false);
    
	// Calculate time left for a given animation.
	//
    // If there are multiple animations playing with the given ID,
    // returns the MAX time left.
    // 
	UFUNCTION(BlueprintCallable, Category = "Luprex|Scripted Animations", meta=(WorldContext = "WorldContextObject"))
    double CalculateTimeLeft(UObject *WorldContextObject, int64 AnimationID=0);

	// Fade all animations whose IDs are not in the 'Keep' list.
	//
	// Sometimes, predictive reexecution causes an animation step to
	// vanish from the animation queue.  When that happens, it is
	// desirable to terminate any scripted animations that were launched
	// by that animation step.  You don't want to cut them off abruptly,
	// you want to fade them out.
    //
    // Whenever luprex calls 'Animation Queue Changed' in a blueprint,
    // it also automatically runs this garbage collection routine on the
    // scripted animations.
    //
    // Scripted animations with nonpositive IDs are immune to garbage
    // collection.  So if you want an animation step to be not affected,
    // give it an ID of zero or a negative number.
	//
	void FadeGarbage(const UObject *WorldContextObject, TArray<int64> KeepIDs);
};

UCLASS()
class INTEGRATION_API UlxScriptedAnimationLibrary : public UBlueprintFunctionLibrary
{
	GENERATED_BODY()

public:
	// Get all the major 'World Clocks' in a single struct.
	// 
    UFUNCTION(BlueprintCallable, Category = "Utilities|Time", meta=(WorldContext = "WorldContextObject"))
    static FlxWorldClocks GetAllWorldClocks(const UObject *WorldContextObject);

    // Get the data to drive Sequence Evaluators and Multi Blend
	//
	// To apply scripted animations in an Anim Graph, you will need
	// three sequence evaluators and a multi-blend node.  This
	// function outputs the input parameters for all of those nodes.
	//
    UFUNCTION(BlueprintPure, Category = "Luprex|Scripted Animations", meta=(BlueprintThreadSafe))
	static void ScriptedAnimationEvaluatorData(const UlxScriptedAnimations *Animations,
        const FlxWorldClocks &WorldClocks,
		UAnimSequenceBase *&Sequence0, float &ExplicitTime0,
		UAnimSequenceBase *&Sequence1, float &ExplicitTime1,
		UAnimSequenceBase *&Sequence2, float &ExplicitTime2,
		float &BaseAlpha, float &Sequence0Alpha, float &Sequence1Alpha, float &Sequence2Alpha);

};

