using System;
using Intrepid.Utilities;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;

namespace Intrepid.Core.Entities
{
    public class EntityModelAnimation : MonoBehaviour
    {
        [GroupInjections, SerializeField] private Animator animator;

        private PlayableGraph graph;
        private AnimationMixerPlayable mixer;
        private AnimationPlayableOutput output;

        private AnimationClipPlayable currentPlayable;
        private AnimationClipPlayable nextPlayable;

        private AnimationClip nextClip;

        private int currentInput;
        private int nextInput = 1;

        private float fadeDuration;
        private float fadeElapsed;

        private float currentSpeed = 1f;
        private float nextSpeed = 1f;

        public AnimationClip CurrentClip { get; private set; }

        public bool IsFading { get; private set; }

        public bool IsPaused { get; private set; }

        public bool IsValid => currentPlayable.IsValid();

        public float Time
        {
            get
            {
                if (!currentPlayable.IsValid())
                {
                    return 0f;
                }

                return (float)currentPlayable.GetTime();
            }
        }

        public float Length => CurrentClip?.length ?? 0f;

        public float NormalizedTime
        {
            get
            {
                if (Length <= 0f)
                {
                    return 0f;
                }

                return Time / Length;
            }
        }

        public bool IsDone
        {
            get
            {
                if (!currentPlayable.IsValid())
                {
                    return true;
                }

                return currentPlayable.IsDone();
            }
        }

        // Takes looping into consideration.
        public bool IsFinished
        {
            get
            {
                if (CurrentClip is null)
                {
                    return true;
                }

                if (CurrentClip.isLooping)
                {
                    return false;
                }

                return NormalizedTime >= 1f;
            }
        }

        private void Awake()
        {
            graph = PlayableGraph.Create($"{name}_AnimationGraph");
            graph.SetTimeUpdateMode(DirectorUpdateMode.GameTime);

            mixer = AnimationMixerPlayable.Create(graph, 2);

            output = AnimationPlayableOutput.Create(graph, "Animation", animator);
            output.SetSourcePlayable(mixer);

            mixer.SetInputWeight(0, 0f);
            mixer.SetInputWeight(1, 0f);

            graph.Play();
        }

        private void Update()
        {
            if (IsPaused)
            {
                return;
            }

            if (!IsFading)
            {
                return;
            }

            fadeElapsed += UnityEngine.Time.deltaTime;

            var t = Mathf.Clamp01(fadeElapsed / fadeDuration);

            mixer.SetInputWeight(currentInput, 1f - t);
            mixer.SetInputWeight(nextInput, t);

            if (t >= 1f)
            {
                CompleteFade();
            }
        }

        public bool IsCurrent(AnimationClip clip)
        {
            return clip.IsNotNull()
                   && CurrentClip == clip
                   && currentPlayable.IsValid();
        }

        public bool IsFadingTo(AnimationClip clip)
        {
            return clip.IsNotNull()
                   && IsFading
                   && nextClip == clip
                   && nextPlayable.IsValid();
        }

        public bool IsPlaying(AnimationClip clip)
        {
            return IsCurrent(clip) || IsFadingTo(clip);
        }

        public void Play(AnimationClip clip, float speed = 1f)
        {
            if (clip is null)
            {
                return;
            }

            IsPaused = false;

            ClearInput(0);
            ClearInput(1);

            currentInput = 0;
            nextInput = 1;

            CurrentClip = clip;
            nextClip = null;

            currentSpeed = speed;
            nextSpeed = 1f;

            currentPlayable = AnimationClipPlayable.Create(graph, clip);
            currentPlayable.SetTime(0);
            currentPlayable.SetSpeed(currentSpeed);

            graph.Connect(currentPlayable, 0, mixer, currentInput);

            mixer.SetInputWeight(currentInput, 1f);
            mixer.SetInputWeight(nextInput, 0f);

            IsFading = false;
            fadeElapsed = 0f;
        }

        public void CrossFade(AnimationClip clip, float fadeDuration, float speed = 1f)
        {
            if (clip is null)
            {
                return;
            }

            if (!currentPlayable.IsValid() || CurrentClip is null || fadeDuration <= 0f)
            {
                Play(clip, speed);
                return;
            }

            if (CurrentClip == clip && !IsFading)
            {
                return;
            }

            IsPaused = false;

            if (currentPlayable.IsValid())
            {
                currentPlayable.SetSpeed(currentSpeed);
            }

            if (nextPlayable.IsValid())
            {
                nextPlayable.SetSpeed(nextSpeed);
            }

            ClearInput(nextInput);

            nextClip = clip;
            nextSpeed = speed;

            nextPlayable = AnimationClipPlayable.Create(graph, clip);
            nextPlayable.SetTime(0);
            nextPlayable.SetSpeed(nextSpeed);

            graph.Connect(nextPlayable, 0, mixer, nextInput);

            mixer.SetInputWeight(nextInput, 0f);

            this.fadeDuration = Mathf.Max(0.0001f, fadeDuration);
            fadeElapsed = 0f;

            IsFading = true;
        }

        public void Pause()
        {
            if (IsPaused)
            {
                return;
            }

            IsPaused = true;

            if (currentPlayable.IsValid())
            {
                currentPlayable.SetSpeed(0f);
            }

            if (nextPlayable.IsValid())
            {
                nextPlayable.SetSpeed(0f);
            }
        }

        public void Unpause()
        {
            if (!IsPaused)
            {
                return;
            }

            IsPaused = false;

            if (currentPlayable.IsValid())
            {
                currentPlayable.SetSpeed(currentSpeed);
            }

            if (nextPlayable.IsValid())
            {
                nextPlayable.SetSpeed(nextSpeed);
            }
        }

        public void Stop()
        {
            ClearInput(0);
            ClearInput(1);

            currentPlayable = default;
            nextPlayable = default;

            CurrentClip = null;
            nextClip = null;

            currentInput = 0;
            nextInput = 1;

            currentSpeed = 1f;
            nextSpeed = 1f;

            fadeDuration = 0f;
            fadeElapsed = 0f;

            IsFading = false;
            IsPaused = false;
        }

        private void CompleteFade()
        {
            ClearInput(currentInput);

            currentPlayable = nextPlayable;
            CurrentClip = nextClip;
            currentSpeed = nextSpeed;

            nextPlayable = default;
            nextClip = null;
            nextSpeed = 1f;

            mixer.SetInputWeight(currentInput, 0f);
            mixer.SetInputWeight(nextInput, 1f);

            (currentInput, nextInput) = (nextInput, currentInput);

            fadeElapsed = 0f;
            IsFading = false;
        }

        private void ClearInput(int input)
        {
            if (!mixer.IsValid())
            {
                return;
            }

            var playable = mixer.GetInput(input);

            if (playable.IsValid())
            {
                graph.Disconnect(mixer, input);
                playable.Destroy();
            }

            mixer.SetInputWeight(input, 0f);
        }

        private void OnDestroy()
        {
            if (graph.IsValid())
            {
                graph.Destroy();
            }
        }
    }
}