////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//                                             
//      _____ _            _____                   
//     |_   _|_|___ _ _   |_   _|_ _ _ ___ ___ ___ 
//       | | | |   | | |    | | | | | | -_| -_|   |
//       |_| |_|_|_|_  |    |_| |_____|___|___|_|_|
//                 |___|
// A Complete and Easy to use Tweens library in One File
//
// Basic use:
// using FronkonGames.TinyTween;
// GameObject gameObject = new();
// gameObject.TweenMove(new Vector3(10.0f, 0.0f, 0.0f), 1.0f, Ease.Bounce);
//
// Advanced use:
// using FronkonGames.TinyTween;
// GameObject clockHand = new();
// TweenQuaternion.Create()
//                .Origin(Quaternion.Euler(-30.0f, 0.0f, 0.0f))
//                .Destination(Quaternion.Euler(30.0f, 0.0f, 0.0f))
//                .Duration(1.0f)
//                .Loop(TweenLoop.YoYo)
//                .EasingIn(Ease.Back)
//                .EasingOut(Ease.Elastic)
//                .Owner(clockHand)
//                .Condition(tween => tween.ExecutionCount < 10)
//                .OnUpdate(tween => clockHand.transform.rotation = tween.Value)
//                .OnEnd(() => Debug.Log("It's show time!"))
//                .Start();
//
// Copyright (c) 2022 Martin Bustos @FronkonGames <fronkongames@gmail.com>
// 
// MIT License
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of
// the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
using System;
using System.Collections.Generic;
using UnityEngine;

namespace FronkonGames.TinyTween
{
  /// <summary> State of a Tween operation. </summary>
  public enum TweenState
  {
    Running, Paused, Finished
  }

  /// <summary> Execution modes of a Tween operation. </summary>
  public enum TweenLoop
  {
    // Just once. Start over from the beginning. Back and forth loop.  
    Once, Loop, YoYo,
  }

  /// <summary> Interface of a tween. </summary>
  public interface ITween
  {
    /// <summary> Tween status. </summary>
    TweenState State { get; }
    
    /// <summary> Update the Tween operation. </summary>
    void Update();
  }

  /// <summary> Generic interface of a tween. </summary>
  public interface ITween<T> : ITween where T : struct
  {
    /// <summary> Current value. </summary>
    T Value { get; }

    /// <summary> Tween operation progress (0, 1). </summary>
    float Progress { get; }

    /// <summary> Executions counter. </summary>
    int ExecutionCount { get; }
    
    /// <summary> Tween status. </summary>
    TweenState State { get; }
    
    /// <summary> Execute a tween operation. </summary>
    Tween<T> Start();

    /// <summary> Pause the tween. </summary>
    void Pause();

    /// <summary> Continue the tween. </summary>
    void Resume();

    /// <summary> Finish the Tween operation. </summary>
    /// <param name="moveToEnd">Move the value at the end or leave it as this.</param>
    void Stop(bool moveToEnd = true);

    /// <summary> Sets tween value at origin and time to 0. </summary>
    void Reset();
    
    /// <summary> Update the Tween operation. </summary>
    void Update();
  }

  /// <summary> Tween operation. If it is created manually, Update() must be called. </summary>
  public abstract class Tween<T> : ITween<T> where T : struct
  {
    /// <inheritdoc/>
    public TweenState State { get; private set; } = TweenState.Paused;

    /// <inheritdoc/>
    public T Value { get; private set; }

    /// <inheritdoc/>
    public float Progress { get; private set; }

    /// <inheritdoc/>
    public int ExecutionCount { get; private set; }

    /// <summary> Time that the operation takes. </summary>
    public float Time { get; private set; }
    
    /// <summary> Does the Tween depend on another object? </summary>
    private bool IsOwned { get; set; }
    
    private object owner = null;
    
    private T origin, destination;
    private Ease easeIn = Ease.None, easeOut = Ease.None;
    private TweenLoop loop = TweenLoop.Once;

    private float duration = 1.0f;
    private float currentTime;
    private bool clamp;
    private int residueCount = -1;

    private Action<Tween<T>> updateFunction;
    private Action<Tween<T>> endFunction;
    private Func<Tween<T>, bool> condition;

    private readonly Func<Tween<T>, T, T, float, bool, T> interpolationFunction; // Tween, start, end, progress, clamp.
    
    /// <summary> Constructor. </summary>
    /// <param name="lerpFunc">Interpolation function.</param>
    protected Tween(Func<Tween<T>, T, T, float, bool, T> interpolationFunction) => this.interpolationFunction = interpolationFunction;

    /// <summary> Initial value. Use it only to create a new Tween. </summary>
    /// <returns>This.</returns>
    public Tween<T> Origin(T start) { origin = start; return this; }

    /// <summary> Final value. Use it only to create a new Tween. </summary>
    /// <returns>This.</returns>
    public Tween<T> Destination(T end) { destination = end; return this; }

    /// <summary> Time to execute the operation, must be greater than 0. Use it only to create a new Tween. </summary>
    /// <returns>This.</returns>
    public Tween<T> Duration(float duration) { this.duration = Mathf.Max(duration, 0.0f); return this; }

    /// <summary> Execution mode. Use it only to create a new Tween. </summary>
    /// <returns>This.</returns>
    public Tween<T> Loop(TweenLoop loop) { this.loop = loop; return this; }
    
    /// <summary> Easing function. Overwrite the In and Out functions. Use it only to create a new Tween. </summary>
    /// <param name="ease">Easing function.</param>
    /// <returns>This.</returns>
    public Tween<T> Easing(Ease ease) { easeIn = easeOut = ease; return this; }
    
    /// <summary> Easing In function. Use it only to create a new Tween. </summary>
    /// <param name="ease">Easing function.</param>
    /// <returns>This.</returns>
    public Tween<T> EasingIn(Ease ease) { easeIn = ease; return this; }
    
    /// <summary> Easing Out function. Use it only to create a new Tween. </summary>
    /// <param name="ease">Easing function.</param>
    /// <returns>This.</returns>
    public Tween<T> EasingOut(Ease ease) { easeOut = ease; return this; }
    
    /// <summary> Update callback. Use it to apply the Tween values. </summary>
    /// <param name="updateCallback">Callback.</param>
    /// <returns>This.</returns>
    public Tween<T> OnUpdate(Action<Tween<T>> updateCallback) { updateFunction = updateCallback; return this; }

    /// <summary> Executed at the end of the operation (optional). </summary>
    /// <param name="endCallback">Callback.</param>
    /// <returns>This.</returns>
    public Tween<T> OnEnd(Action<Tween<T>> endCallback) { endFunction = endCallback; return this; }

    /// <summary> Condition of progress, stops if the operation is not true (optional). </summary>
    /// <param name="condition">Condition function.</param>
    /// <returns>This.</returns>
    public Tween<T> Condition(Func<Tween<T>, bool> condition) { this.condition = condition; return this; }

    /// <summary> Limits the values of the interpolation to the range [0, 1]. </summary>
    /// <param name="clamp">Clamp.</param>
    /// <returns>This.</returns>
    public Tween<T> Clamp(bool clamp) { this.clamp = clamp; return this; }
    
    /// <summary>
    /// I set an object as the 'owner' of the Tween. If the object is destroyed, the Tween ends and is destroyed.
    /// </summary>
    /// <param name="owner">Owner</param>
    /// <returns>This.</returns>
    public Tween<T> Owner(object owner) { IsOwned = owner != null; this.owner = owner; return this; }

    /// <inheritdoc/>
    public Tween<T> Start()
    {
      Debug.Assert(duration > 0.0f, "[FronkonGames.TinyTween] The duration of the tween should be greater than zero.");
      Debug.Assert(easeIn != Ease.None && easeOut != Ease.None, "[FronkonGames.TinyTween] You must set some kind of Ease.");

      State = TweenState.Running;
      UpdateValue();

      return this;
    }

    /// <inheritdoc/>
    public void Pause() => State = TweenState.Paused;

    /// <inheritdoc/>
    public void Resume() => State = TweenState.Running;

    /// <inheritdoc/>
    public void Stop(bool moveToEnd = true)
    {
      if (State != TweenState.Finished)
      {
        State = TweenState.Finished;
        if (moveToEnd == true)
        {
          currentTime = duration;

          UpdateValue();
        }

        endFunction?.Invoke(this);
      }
    }

    /// <inheritdoc/>
    public void Reset()
    {
      currentTime = 0.0f;
      Value = origin;
    }
    
    /// <inheritdoc/>
    public void Update()
    {
      if (IsOwned == true && owner.Equals(null) || condition != null && condition(this) == false)
        Stop(false);
      else
      {
        currentTime += UnityEngine.Time.deltaTime;
        if (currentTime >= duration)
        {
          residueCount--;
          ExecutionCount++;

          switch (loop)
          {
            case TweenLoop.Once: Stop(); break;

            case TweenLoop.Loop:
              if (residueCount == 0)
                Stop();

              Value = origin;
              currentTime = Progress = 0.0f;
              break;

            case TweenLoop.YoYo:
              if (residueCount == 0)
                Stop();

              (destination, origin) = (origin, destination);
              currentTime = Progress = 0.0f;
              break;

            default: throw new ArgumentOutOfRangeException();
          }
        }
        else
          UpdateValue();
      }
    }

    private void UpdateValue()
    {
      float t = currentTime / duration;
      Progress = EasingFunctions.Calculate(t > 0.5f ? easeOut : easeIn, easeIn != Ease.None, easeOut != Ease.None, t);

      Value = interpolationFunction(this, origin, destination, Progress, clamp);

      updateFunction?.Invoke(this);
    }
  }

  /// <summary>
  /// Tweens manager, update tweens and delete those that have already ended.
  /// It is not necessary if you are in charge of maintaining your Tweens ;)
  /// </summary>
  public sealed class TinyTween : MonoBehaviour
  {
    public static TinyTween Instance => LazyInstance.Value;

    private static readonly Lazy<TinyTween> LazyInstance = new(CreateSingleton);
    
    private readonly List<ITween> tweens = new();

    /// <summary> Add an existing tween. </summary>
    /// <param name="tween">New tween</param>
    /// <returns>Tween.</returns>
    public ITween Add(ITween tween) { tweens.Add(tween); return tween; }

    private static TinyTween CreateSingleton()
    {
      GameObject ownerObject = new("TinyTween");
      TinyTween instance = ownerObject.AddComponent<TinyTween>();
      DontDestroyOnLoad(ownerObject);

      return instance;
    }

    private void Update()
    {
      int count = tweens.Count;
      for (int i = count - 1; i >= 0; --i)
      {
        ITween tween = tweens[i];

        if (tween.State == TweenState.Running)
          tween.Update();

        if (tween.State == TweenState.Finished && i < count)
          tweens.RemoveAt(i);
      }      
    }

    private void OnDisable() => tweens.Clear();
  }
  
  /// <summary> Tween float. </summary>
  public sealed class TweenFloat : Tween<float>
  {
    /// <summary> Create a Tween float and add it to the TinyTween manager. </summary>
    /// <returns>Tween.</returns>
    public static Tween<float> Create() => TinyTween.Instance.Add(new TweenFloat()) as Tween<float>;
    
    private static float Lerp(ITween<float> t, float start, float end, float progress, bool clamp) =>
        clamp == true ? Mathf.Lerp(start, end, progress) : Mathf.LerpUnclamped(start, end, progress);

    private TweenFloat() : base(Lerp) { }
  }

  /// <summary> Tween Vector2. </summary>
  public sealed class TweenVector2 : Tween<Vector2>
  {
    /// <summary> Create a Tween Vector2 and add it to the TinyTween manager. </summary>
    /// <returns>Tween.</returns>
    public static Tween<Vector2> Create() => TinyTween.Instance.Add(new TweenVector2()) as Tween<Vector2>;

    private static Vector2 Lerp(ITween<Vector2> t, Vector2 start, Vector2 end, float progress, bool clamp) =>
      clamp == true ? Vector2.Lerp(start, end, progress) : Vector2.LerpUnclamped(start, end, progress); 

    private TweenVector2() : base(Lerp) { }
  }

  /// <summary> Tween Vector3. </summary>
  public sealed class TweenVector3 : Tween<Vector3>
  {
    /// <summary> Create a Tween Vector3 and add it to the TinyTween manager. </summary>
    /// <returns>Tween.</returns>
    public static Tween<Vector3> Create() => TinyTween.Instance.Add(new TweenVector3()) as Tween<Vector3>;
    
    private static Vector3 Lerp(ITween<Vector3> t, Vector3 start, Vector3 end, float progress, bool clamp) =>
      clamp == true ? Vector3.Lerp(start, end, progress) : Vector3.LerpUnclamped(start, end, progress);

    private TweenVector3() : base(Lerp) { }
  }

  /// <summary> Tween Vector4. </summary>
  public sealed class TweenVector4 : Tween<Vector4>
  {
    /// <summary> Create a Tween Vector4 and add it to the TinyTween manager. </summary>
    /// <returns>Tween.</returns>
    public static Tween<Vector4> Create() => TinyTween.Instance.Add(new TweenVector4()) as Tween<Vector4>;
    
    private static Vector4 Lerp(ITween<Vector4> t, Vector4 start, Vector4 end, float progress, bool clamp) =>
      clamp == true ? Vector4.Lerp(start, end, progress) : Vector4.LerpUnclamped(start, end, progress);

    private TweenVector4() : base(Lerp) { }
  }

  /// <summary> Tween Quaternion. </summary>
  public sealed class TweenQuaternion : Tween<Quaternion>
  {
    /// <summary> Create a Tween Quaternion and add it to the TinyTween manager. </summary>
    /// <returns>Tween.</returns>
    public static Tween<Quaternion> Create() => TinyTween.Instance.Add(new TweenQuaternion()) as Tween<Quaternion>;

    private static Quaternion Lerp(ITween<Quaternion> t, Quaternion start, Quaternion end, float progress, bool clamp) =>
      clamp == true ? Quaternion.Lerp(start, end, progress) : Quaternion.LerpUnclamped(start, end, progress);   

    private TweenQuaternion() : base(Lerp) { }
  }

  /// <summary> Tween Color. </summary>
  public sealed class TweenColor : Tween<Color>
  {
    /// <summary> Create a Tween Color and add it to the TinyTween manager. </summary>
    /// <returns>Tween.</returns>
    public static Tween<Color> Create() => TinyTween.Instance.Add(new TweenColor()) as Tween<Color>;

    private static Color Lerp(ITween<Color> t, Color start, Color end, float progress, bool clamp) =>
      clamp == true ? Color.Lerp(start, end, progress) : Color.LerpUnclamped(start, end, progress);

    private TweenColor() : base(Lerp) { }
  }

  /// <summary> Extensions to make it easy to use TinyTween. </summary>
  public static class TweenExtensions
  {
    /// <summary> Create and execute a tween. </summary>
    /// <param name="self">Self.</param>
    /// <param name="origin">Start value.</param>
    /// <param name="destination">End value.</param>
    /// <param name="duration">Time in seconds.</param>
    /// <param name="ease">Easing.</param>
    /// <returns>Tween.</returns>
    public static Tween<float> Tween(this float self, float origin, float destination, float duration, Ease ease) =>
      TweenFloat.Create()
        .Origin(origin)
        .Destination(destination)
        .Duration(duration)
        .OnUpdate(tween => self = tween.Value)
        .Owner(self)
        .Easing(ease)
        .Start();

    /// <summary> Create and execute a tween, using as initial value the current value. </summary>
    /// <param name="self">Self.</param>
    /// <param name="destination">End value.</param>
    /// <param name="duration">Time in seconds.</param>
    /// <param name="ease">Easing.</param>
    /// <returns>Tween.</returns>
    public static Tween<float> Tween(this float self, float destination, float duration, Ease ease) => Tween(self, self, destination, duration, ease);

    /// <summary> Create and execute a tween. </summary>
    /// <param name="self">Self.</param>
    /// <param name="origin">Start value.</param>
    /// <param name="destination">End value.</param>
    /// <param name="duration">Time in seconds.</param>
    /// <param name="ease">Easing.</param>
    /// <returns>Tween.</returns>
    public static Tween<Vector3> Tween(this Vector3 self, Vector3 origin, Vector3 destination, float duration, Ease ease) =>
      TweenVector3.Create()
        .Origin(origin)
        .Destination(destination)
        .Duration(duration)
        .OnUpdate(tween => self = tween.Value)
        .Owner(self)
        .Easing(ease)
        .Start();
    
    /// <summary> Create and execute a tween, using as initial value the current value. </summary>
    /// <param name="self">Self.</param>
    /// <param name="destination">End value.</param>
    /// <param name="duration">Time in seconds.</param>
    /// <param name="ease">Easing.</param>
    /// <returns>Tween.</returns>
    public static Tween<Vector3> Tween(this Vector3 self, Vector3 destination, float duration, Ease ease) => Tween(self, self, destination, duration, ease);

    /// <summary> Create and execute a tween. </summary>
    /// <param name="self">Self.</param>
    /// <param name="origin">Start value.</param>
    /// <param name="destination">End value.</param>
    /// <param name="duration">Time in seconds.</param>
    /// <param name="ease">Easing.</param>
    /// <returns>Tween.</returns>
    public static Tween<Quaternion> Tween(this Quaternion self, Quaternion origin, Quaternion destination, float duration, Ease ease) =>
      TweenQuaternion.Create()
        .Origin(origin)
        .Destination(destination)
        .Duration(duration)
        .OnUpdate(tween => self = tween.Value)
        .Owner(self)
        .Easing(ease)
        .Start();
    
    /// <summary> Create and execute a tween, using as initial value the current value. </summary>
    /// <param name="self">Self.</param>
    /// <param name="destination">End value.</param>
    /// <param name="duration">Time in seconds.</param>
    /// <param name="ease">Easing.</param>
    /// <returns>Tween.</returns>
    public static Tween<Quaternion> Tween(this Quaternion self, Quaternion destination, float duration, Ease ease) => Tween(self, self, destination, duration, ease);
    
    /// <summary> Create and execute a tween. </summary>
    /// <param name="self">Self.</param>
    /// <param name="origin">Start value.</param>
    /// <param name="destination">End value.</param>
    /// <param name="duration">Time in seconds.</param>
    /// <param name="ease">Easing.</param>
    /// <returns>Tween.</returns>
    public static Tween<Color> Tween(this Color self, Color origin, Color destination, float duration, Ease ease) =>
      FronkonGames.TinyTween.TweenColor.Create()
        .Origin(origin)
        .Destination(destination)
        .Duration(duration)
        .OnUpdate(tween => self = tween.Value)
        .Owner(self)
        .Easing(ease)
        .Start();
    
    /// <summary> Create and execute a tween, using as initial value the current value. </summary>
    /// <param name="self">Self.</param>
    /// <param name="destination">End value.</param>
    /// <param name="duration">Time in seconds.</param>
    /// <param name="ease">Easing.</param>
    /// <returns>Tween.</returns>
    public static Tween<Color> Tween(this Color self, Color destination, float duration, Ease ease) => Tween(self, self, destination, duration, ease);

    /// <summary> Moves a Transform. </summary>
    /// <param name="self">Self.</param>
    /// <param name="origin">Start position.</param>
    /// <param name="destination">End position.</param>
    /// <param name="duration">Time in seconds.</param>
    /// <param name="ease">Easing.</param>
    /// <returns>Tween.</returns>
    public static Tween<Vector3> TweenMove(this Transform self, Vector3 origin, Vector3 destination, float duration, Ease ease) =>
      TweenVector3.Create()
        .Origin(origin)
        .Destination(destination)
        .Duration(duration)
        .OnUpdate(tween => self.position = tween.Value)
        .Owner(self)
        .Easing(ease)
        .Start();

    /// <summary> Moves a Transform, using its current position as origin. </summary>
    /// <param name="self">Self.</param>
    /// <param name="destination">End position.</param>
    /// <param name="duration">Time in seconds.</param>
    /// <param name="ease">Easing.</param>
    /// <returns>Tween.</returns>
    public static Tween<Vector3> TweenMove(this Transform self, Vector3 destination, float duration, Ease ease) => TweenMove(self, self.position, destination, duration, ease);

    /// <summary> Scale a Transform. </summary>
    /// <param name="self">Self.</param>
    /// <param name="origin">Start scale.</param>
    /// <param name="destination">End scale.</param>
    /// <param name="duration">Time in seconds.</param>
    /// <param name="ease">Easing.</param>
    /// <returns>Tween.</returns>
    public static Tween<Vector3> TweenScale(this Transform self, Vector3 origin, Vector3 destination, float duration, Ease ease) =>
      TweenVector3.Create()
        .Origin(origin)
        .Destination(destination)
        .Duration(duration)
        .OnUpdate(tween => self.localScale = tween.Value)
        .Owner(self)
        .Easing(ease)
        .Start();

    /// <summary> Scale a Transform, using its current scale as origin. </summary>
    /// <param name="self">Self.</param>
    /// <param name="destination">End scale.</param>
    /// <param name="duration">Time in seconds.</param>
    /// <param name="ease">Easing.</param>
    /// <returns>Tween.</returns>
    public static Tween<Vector3> TweenScale(this Transform self, Vector3 destination, float duration, Ease ease) => TweenScale(self, self.localScale, destination, duration, ease);

    /// <summary> Rotate a Transform. </summary>
    /// <param name="self">Self.</param>
    /// <param name="origin">Start rotation.</param>
    /// <param name="destination">End rotation.</param>
    /// <param name="duration">Time in seconds.</param>
    /// <param name="ease">Easing.</param>
    /// <returns>Tween.</returns>
    public static Tween<Quaternion> TweenRotation(this Transform self, Quaternion origin, Quaternion destination, float duration, Ease ease) =>
      TweenQuaternion.Create()
        .Origin(origin)
        .Destination(destination)
        .Duration(duration)
        .OnUpdate(tween => self.rotation = tween.Value)
        .Owner(self)
        .Easing(ease)
        .Start();

    /// <summary> Rotate a Transform, using its current rotation as origin. </summary>
    /// <param name="self">Self.</param>
    /// <param name="destination">End rotation.</param>
    /// <param name="duration">Time in seconds.</param>
    /// <param name="ease">Easing.</param>
    /// <returns>Tween.</returns>
    public static Tween<Quaternion> TweenRotation(this Transform self, Quaternion destination, float duration, Ease ease) => TweenRotation(self, self.rotation, destination, duration, ease);
    
    /// <summary> Moves a GameObject. </summary>
    /// <param name="self">Self.</param>
    /// <param name="origin">Start position.</param>
    /// <param name="destination">End position.</param>
    /// <param name="duration">Time in seconds.</param>
    /// <param name="ease">Easing.</param>
    /// <returns>Tween.</returns>
    public static Tween<Vector3> TweenMove(this GameObject self, Vector3 origin, Vector3 destination, float duration, Ease ease) => TweenMove(self.transform, origin, destination, duration, ease);
    
    /// <summary> Moves a GameObject, using its current position as origin. </summary>
    /// <param name="self">Self.</param>
    /// <param name="destination">End position.</param>
    /// <param name="duration">Time in seconds.</param>
    /// <param name="ease">Easing.</param>
    /// <returns>Tween.</returns>
    public static Tween<Vector3> TweenMove(this GameObject self, Vector3 destination, float duration, Ease ease) => TweenMove(self.transform, self.transform.position, destination, duration, ease);
    
    /// <summary> Scale a GameObject. </summary>
    /// <param name="self">Self.</param>
    /// <param name="origin">Start scale.</param>
    /// <param name="destination">End scale.</param>
    /// <param name="duration">Time in seconds.</param>
    /// <param name="ease">Easing.</param>
    /// <returns>Tween.</returns>
    public static Tween<Vector3> TweenScale(this GameObject self, Vector3 origin, Vector3 destination, float duration, Ease ease) => TweenScale(self.transform, origin, destination, duration, ease);

    /// <summary> Scale a GameObject, using its current scale as origin. </summary>
    /// <param name="self">Self.</param>
    /// <param name="destination">End scale.</param>
    /// <param name="duration">Time in seconds.</param>
    /// <param name="ease">Easing.</param>
    /// <returns>Tween.</returns>
    public static Tween<Vector3> TweenScale(this GameObject self, Vector3 destination, float duration, Ease ease) => TweenScale(self.transform, self.transform.localScale, destination, duration, ease);
    
    /// <summary> Rotate a GameObject. </summary>
    /// <param name="self">Self.</param>
    /// <param name="origin">Start rotation.</param>
    /// <param name="destination">End rotation.</param>
    /// <param name="duration">Time in seconds.</param>
    /// <param name="ease">Easing.</param>
    /// <returns>Tween.</returns>
    public static Tween<Quaternion> TweenRotation(this GameObject self, Quaternion origin, Quaternion destination, float duration, Ease ease) => TweenRotation(self.transform, origin, destination, duration, ease);

    /// <summary> Rotate a GameObject, using its current rotation as origin. </summary>
    /// <param name="self">Self.</param>
    /// <param name="destination">End rotation.</param>
    /// <param name="duration">Time in seconds.</param>
    /// <param name="ease">Easing.</param>
    /// <returns>Tween.</returns>
    public static Tween<Quaternion> TweenRotation(this GameObject self, Quaternion destination, float duration, Ease ease) => TweenRotation(self.transform, self.transform.rotation, destination, duration, ease);
    
    /// <summary> Change the color of a Material. </summary>
    /// <param name="self">Self.</param>
    /// <param name="origin">Start color.</param>
    /// <param name="destination">End color.</param>
    /// <param name="duration">Time in seconds.</param>
    /// <param name="ease">Easing.</param>
    /// <param name="name">The name of the color variable.</param>
    /// <returns>Tween.</returns>
    public static Tween<Color> TweenColor(this Material self, Color origin, Color destination, float duration, Ease ease, string name = "_Color") =>
      FronkonGames.TinyTween.TweenColor.Create()
        .Origin(origin)
        .Destination(destination)
        .Duration(duration)
        .OnUpdate(tween => self.SetColor(name, tween.Value))
        .Owner(self)
        .Easing(ease)
        .Start();
    
    /// <summary> Change the color of a Material, using its current color as origin. </summary>
    /// <param name="self">Self.</param>
    /// <param name="destination">End color.</param>
    /// <param name="duration">Time in seconds.</param>
    /// <param name="ease">Easing.</param>
    /// <param name="name">The name of the color variable.</param>
    /// <returns>Tween.</returns>
    public static Tween<Color> TweenColor(this Material self, Color destination, float duration, Ease ease, string name = "_Color") => TweenColor(self, self.GetColor(name), destination, duration, ease, name);
  }

  /// <summary> Types of Easing functions. See https://easings.net </summary>
  public enum Ease
  {
    None,
    Linear,
    Sine,
    Quad,
    Cubic,
    Quart,
    Quint,
    Expo,
    Circ,
    Back,
    Elastic,
    Bounce,
  }

  /// <summary> Easing functions. </summary>
  internal static class EasingFunctions
  {
    public static float Calculate(Ease ease, bool easingIn, bool easingOut, float t) => ease switch
    {
      Ease.Linear     => t,
      Ease.Sine       => easingIn && easingOut ? SineInOut(t)    : easingIn ? SineIn(t)    : SineOut(t),
      Ease.Quad       => easingIn && easingOut ? QuadInOut(t)    : easingIn ? QuadIn(t)    : QuadOut(t),
      Ease.Cubic      => easingIn && easingOut ? CubicInOut(t)   : easingIn ? CubicIn(t)   : CubicOut(t),
      Ease.Quart      => easingIn && easingOut ? QuartInOut(t)   : easingIn ? QuartIn(t)   : QuartOut(t),
      Ease.Quint      => easingIn && easingOut ? QuintInOut(t)   : easingIn ? QuintIn(t)   : QuintOut(t),
      Ease.Expo       => easingIn && easingOut ? ExpoInOut(t)    : easingIn ? ExpoIn(t)    : ExpoOut(t),
      Ease.Circ       => easingIn && easingOut ? CircInOut(t)    : easingIn ? CircIn(t)    : CircOut(t),
      Ease.Back       => easingIn && easingOut ? BackInOut(t)    : easingIn ? BackIn(t)    : BackOut(t),
      Ease.Elastic    => easingIn && easingOut ? ElasticInOut(t) : easingIn ? ElasticIn(t) : ElasticOut(t),
      Ease.Bounce     => easingIn && easingOut ? BounceInOut(t)  : easingIn ? BounceIn(t)  : BounceOut(t),
      _ => t
    };

    private static float SineIn(float t)    => 1.0f - Mathf.Cos(t * Mathf.PI / 2.0f);
    private static float SineOut(float t)   => Mathf.Sin(t * Mathf.PI / 2.0f);
    private static float SineInOut(float t) => 0.5f * (1.0f - Mathf.Cos(Mathf.PI * t));
    
    private static float QuadIn(float t)    => t * t;
    private static float QuadOut(float t)   => 1.0f - (1.0f - t) * (1.0f - t);
    private static float QuadInOut(float t) => t < 0.5f ? 2.0f * t * t : 1.0f - Mathf.Pow(-2.0f * t + 2.0f, 2.0f) / 2.0f;

    private static float CubicIn(float t) => t * t * t;
    private static float CubicOut(float t) => --t * t * t + 1.0f;
    private static float CubicInOut(float t) => t < 0.5f ? 4.0f * t * t * t : 1.0f - Mathf.Pow(-2.0f * t + 2.0f, 3.0f) / 2.0f;
    
    private static float QuartIn(float t) => t * t * t * t;
    private static float QuartOut(float t) => 1.0f - (--t * t * t * t);
    private static float QuartInOut(float t) => (t *= 2.0f) < 1.0f ? 0.5f * t * t * t * t : -0.5f * ((t -= 2.0f) * t * t * t - 2.0f);

    private static float QuintIn(float t) => t * t * t * t * t;
    private static float QuintOut(float t) => --t * t * t * t * t + 1.0f;
    private static float QuintInOut(float t) => t < 0.5f ? 16.0f * t * t * t * t * t : 1.0f - Mathf.Pow(-2.0f * t + 2.0f, 5.0f) / 2.0f;

    private static float ExpoIn(float t) => t == 0.0f ? 0.0f : Mathf.Pow(2.0f, 10.0f * t - 10.0f);
    private static float ExpoOut(float t) => t == 1.0f ? 1.0f : 1.0f - Mathf.Pow(2.0f, -10.0f * t);
    private static float ExpoInOut(float t) => t == 0.0f ? 0.0f
                                                         : t == 1.0f ? 1.0f
                                                                     : t < 0.5f ? Mathf.Pow(2.0f, 20.0f * t - 10.0f) / 2.0f
                                                                                : (2.0f - Mathf.Pow(2.0f, -20.0f * t + 10.0f)) / 2.0f;

    private static float CircIn(float t) => 1.0f - Mathf.Sqrt(1.0f - Mathf.Pow(t, 2.0f));
    private static float CircOut(float t) => Mathf.Sqrt(1.0f - Mathf.Pow(t - 1.0f, 2.0f));
    private static float CircInOut(float t) => t < 0.5f ? (1.0f - Mathf.Sqrt(1.0f - Mathf.Pow(2.0f * t, 2.0f))) / 2.0f
                                                        : (Mathf.Sqrt(1.0f - Mathf.Pow(-2.0f * t + 2.0f, 2.0f)) + 1.0f) / 2.0f;

    private static float BackIn(float t) => C3 * t * t * t - C1 * t * t;
    private static float BackOut(float t) => 1.0f + C3 * Mathf.Pow(t - 1.0f, 3.0f) + C1 * Mathf.Pow(t - 1.0f, 2.0f);
    private static float BackInOut(float t) => t < 0.5f ? Mathf.Pow(2.0f * t, 2.0f) * ((C2 + 1.0f) * 2.0f * t - C2) / 2.0f
                                                        : (Mathf.Pow(2.0f * t - 2.0f, 2.0f) * ((C2 + 1.0f) * (t * 2.0f - 2.0f) + C2) + 2.0f) / 2.0f;

    private static float ElasticIn(float t) => -Mathf.Pow(2.0f, 10.0f * t - 10.0f) * Mathf.Sin((t * 10.0f - 10.75f) * C4);
    private static float ElasticOut(float t) => Mathf.Pow(2.0f, -10.0f * t) * Mathf.Sin((t * 10.0f - 0.75f) * C4) + 1.0f;      
    private static float ElasticInOut(float t) => t < 0.5f ? -(Mathf.Pow(2.0f, 20.0f * t - 10.0f) * Mathf.Sin((20.0f * t - 11.125f) * C5)) / 2f
                                                           : Mathf.Pow(2.0f, -20.0f * t + 10.0f) * Mathf.Sin((20.0f * t - 11.125f) * C5) / 2.0f + 1.0f;

    private static float BounceIn(float t) => 1.0f - BounceOut(1.0f - t);
    private static float BounceOut(float t)
    {
      if (t < 1.0f / 2.75f)
        return 7.5625f * t * t;
      
      if (t < 2.0f / 2.75f)
        return 7.5625f * (t -= 1.5f / 2.75f) * t + 0.75f;
      
      if (t < 2.5f / 2.75f)
        return 7.5625f * (t -= 2.25f / 2.75f) * t + 0.9375f;
      
      return 7.5625f * (t -= 2.625f / 2.75f) * t + 0.984375f;
    }
    private static float BounceInOut(float t) => t < 0.5f ? BounceIn(t * 2.0f) * 0.5f : BounceOut(t * 2.0f - 1.0f) * 0.5f + 0.5f;

    private const float C1 = 1.70158f;
    private const float C2 = C1 * 1.525f;
    private const float C3 = C1 + 1.0f;
    private const float C4 = 2.0f * Mathf.PI / 3.0f;
    private const float C5 = 2.0f * Mathf.PI / 4.5f;
  }
}