// Animancer // https://kybernetik.com.au/animancer // Copyright 2022 Kybernetik //

#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value.

using System;
using Unity.Collections;
using UnityEngine;
using UnityEngine.Animations;

namespace Animancer.Examples.Jobs
{
    /// <summary>
    /// A wrapper that manages an Animation Job (the <see cref="Job"/> struct nested inside this class) which rotates a
    /// set of bones to allow the character to dynamically lean over independantly of their animations.
    /// </summary>
    /// 
    /// <remarks>
    /// The axis around which the bones are rotated can be set to achieve several different effects:
    /// <list type="number">
    /// <item>The right axis allows bending forwards and backwards.</item>
    /// <item>The up axis allows turning to either side.</item>
    /// <item>The forward axis allows leaning to either side.</item>
    /// </list>
    /// <see cref="https://github.com/KybernetikGames/animancer/issues/48#issuecomment-632336377">
    /// This script is based on an implementation by ted-hou on GitHub.</see>
    /// </remarks>
    /// 
    /// <example><see href="https://kybernetik.com.au/animancer/docs/examples/jobs/lean">Lean</see></example>
    /// 
    /// https://kybernetik.com.au/animancer/api/Animancer.Examples.Jobs/SimpleLean
    /// 
    public sealed class SimpleLean : AnimancerJob<SimpleLean.Job>, IDisposable
    {
        /************************************************************************************************************************/
        #region Initialisation
        /************************************************************************************************************************/

        public SimpleLean(AnimancerPlayable animancer, Vector3 axis, NativeArray<TransformStreamHandle> leanBones)
        {
            var animator = animancer.Component.Animator;

            _Job = new Job
            {
                root = animator.BindStreamTransform(animator.transform),
                bones = leanBones,
                axis = axis,
                angle = AnimancerUtilities.CreateNativeReference<float>(),
            };

            CreatePlayable(animancer);

            animancer.Disposables.Add(this);
        }

        /************************************************************************************************************************/

        public SimpleLean(AnimancerPlayable animancer)
            : this(animancer, Vector3.forward, GetDefaultHumanoidLeanBones(animancer.Component.Animator))
        { }

        /************************************************************************************************************************/

        public static NativeArray<TransformStreamHandle> GetDefaultHumanoidLeanBones(Animator animator)
        {
            var leanBones = new NativeArray<TransformStreamHandle>(2, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
            leanBones[0] = animator.BindStreamTransform(animator.GetBoneTransform(HumanBodyBones.Spine));
            leanBones[1] = animator.BindStreamTransform(animator.GetBoneTransform(HumanBodyBones.Chest));
            return leanBones;
        }

        /************************************************************************************************************************/
        #endregion
        /************************************************************************************************************************/
        #region Control
        /************************************************************************************************************************/
        // The Axis probably won't change often so the setter can just get the job data and change it.
        /************************************************************************************************************************/

        public Vector3 Axis
        {
            get => _Job.axis;
            set
            {
                if (_Job.axis == value)
                    return;

                _Job.axis = value;
                _Playable.SetJobData(_Job);
            }
        }

        /************************************************************************************************************************/
        // But since the Angle could change all the time, we can exploit the fact that arrays are actualy references to avoid
        // copying the entire struct out of the job playable then back in every time.
        /************************************************************************************************************************/

        public float Angle
        {
            get => _Job.angle[0];
            set => _Job.angle[0] = value;
        }

        /************************************************************************************************************************/
        #endregion
        /************************************************************************************************************************/
        #region Clean Up
        /************************************************************************************************************************/

        void IDisposable.Dispose() => Dispose();

        /// <summary>Cleans up the <see cref="NativeArray{T}"/>s.</summary>
        /// <remarks>Called by <see cref="AnimancerPlayable.OnPlayableDestroy"/>.</remarks>
        private void Dispose()
        {
            if (_Job.angle.IsCreated)
                _Job.angle.Dispose();

            if (_Job.bones.IsCreated)
                _Job.bones.Dispose();
        }

        /// <summary>Destroys the <see cref="_Playable"/> and restores the graph connection it was intercepting.</summary>
        public override void Destroy()
        {
            Dispose();
            base.Destroy();
        }

        /************************************************************************************************************************/
        #endregion
        /************************************************************************************************************************/
        #region Job
        /************************************************************************************************************************/

        /// <summary>An <see cref="IAnimationJob"/> that applies a lean effect to an <see cref="AnimationStream"/>.</summary>
        /// <example><see href="https://kybernetik.com.au/animancer/docs/examples/jobs/lean">Lean</see></example>
        public struct Job : IAnimationJob
        {
            /************************************************************************************************************************/

            public TransformStreamHandle root;
            public NativeArray<TransformStreamHandle> bones;
            public Vector3 axis;
            public NativeArray<float> angle;

            /************************************************************************************************************************/

            public void ProcessRootMotion(AnimationStream stream) { }

            /************************************************************************************************************************/

            public void ProcessAnimation(AnimationStream stream)
            {
                var angle = this.angle[0] / bones.Length;
                var worldAxis = root.GetRotation(stream) * axis;
                var offset = Quaternion.AngleAxis(angle, worldAxis);

                for (int i = bones.Length - 1; i >= 0; i--)
                {
                    var bone = bones[i];
                    bone.SetRotation(stream, offset * bone.GetRotation(stream));
                }
            }

            /************************************************************************************************************************/
        }

        /************************************************************************************************************************/
        #endregion
        /************************************************************************************************************************/
    }
}