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

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

namespace Animancer
{
    /// <summary>[Pro-Only]
    /// A base wrapper which allows access to the value of properties that are controlled by animations.
    /// </summary>
    /// <remarks>
    /// Documentation: <see href="https://kybernetik.com.au/animancer/docs/manual/ik#animated-properties">Animated Properties</see>
    /// </remarks>
    /// <example><see href="https://kybernetik.com.au/animancer/docs/examples/jobs">Animation Jobs</see></example>
    /// https://kybernetik.com.au/animancer/api/Animancer/AnimatedProperty_2
    /// 
    public abstract class AnimatedProperty<TJob, TValue> : AnimancerJob<TJob>, IDisposable
        where TJob : struct, IAnimationJob
        where TValue : struct
    {
        /************************************************************************************************************************/

        /// <summary>The properties wrapped by this object.</summary>
        protected NativeArray<PropertyStreamHandle> _Properties;

        /// <summary>The value of each of the <see cref="_Properties"/> from the most recent update.</summary>
        protected NativeArray<TValue> _Values;

        /************************************************************************************************************************/
        #region Initialisation
        /************************************************************************************************************************/

        /// <summary>
        /// Allocates room for a specified number of properties to be filled by
        /// <see cref="InitializeProperty(int, Transform, Type, string)"/>.
        /// </summary>
        public AnimatedProperty(IAnimancerComponent animancer, int propertyCount,
            NativeArrayOptions options = NativeArrayOptions.ClearMemory)
        {
            _Properties = new NativeArray<PropertyStreamHandle>(propertyCount, Allocator.Persistent, options);
            _Values = new NativeArray<TValue>(propertyCount, Allocator.Persistent);
            CreateJob();

            var playable = animancer.Playable;
            CreatePlayable(playable);
            playable.Disposables.Add(this);
        }

        /// <summary>Initializes a single property.</summary>
        public AnimatedProperty(IAnimancerComponent animancer, string propertyName)
            : this(animancer, 1, NativeArrayOptions.UninitializedMemory)
        {
            var animator = animancer.Animator;
            _Properties[0] = animator.BindStreamProperty(animator.transform, typeof(Animator), propertyName);
        }

        /// <summary>Initializes a group of properties.</summary>
        public AnimatedProperty(IAnimancerComponent animancer, params string[] propertyNames)
            : this(animancer, propertyNames.Length, NativeArrayOptions.UninitializedMemory)
        {
            var count = propertyNames.Length;

            var animator = animancer.Animator;
            var transform = animator.transform;
            for (int i = 0; i < count; i++)
                InitializeProperty(animator, i, transform, typeof(Animator), propertyNames[i]);
        }

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

        /// <summary>Initializes a property on the target <see cref="Animator"/>.</summary>
        public void InitializeProperty(Animator animator, int index, string name)
            => InitializeProperty(animator, index, animator.transform, typeof(Animator), name);

        /// <summary>Initializes the specified `index` to read a property with the specified `name`.</summary>
        public void InitializeProperty(Animator animator, int index, Transform transform, Type type, string name)
            => _Properties[index] = animator.BindStreamProperty(transform, type, name);

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

        /// <summary>Creates and assigns the <see cref="AnimancerJob._Job"/>.</summary>
        protected abstract void CreateJob();

        /************************************************************************************************************************/
        #endregion
        /************************************************************************************************************************/
        #region Accessors
        /************************************************************************************************************************/

        /// <summary>Returns the value of the first property.</summary>
        public TValue Value => this[0];

        /// <summary>Returns the value of the first property.</summary>
        public static implicit operator TValue(AnimatedProperty<TJob, TValue> properties) => properties[0];

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

        /// <summary>Returns the value of the property at the specified `index`.</summary>
        /// <remarks>This method is identical to <see cref="this[int]"/>.</remarks>
        public TValue GetValue(int index) => _Values[index];

        /// <summary>Returns the value of the property at the specified `index`.</summary>
        /// <remarks>This indexer is identical to <see cref="GetValue(int)"/>.</remarks>
        public TValue this[int index] => _Values[index];

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

        /// <summary>Resizes the `values` if necessary and copies the value of each property into it.</summary>
        public void GetValues(ref TValue[] values)
        {
            AnimancerUtilities.SetLength(ref values, _Values.Length);
            _Values.CopyTo(values);
        }

        /// <summary>Returns a new array containing the values of all properties.</summary>
        /// <remarks>Use <see cref="GetValues(ref TValue[])"/> to avoid allocating a new array every call.</remarks>
        public TValue[] GetValues()
        {
            var values = new TValue[_Values.Length];
            _Values.CopyTo(values);
            return values;
        }

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

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

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

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

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