/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * Licensed under the Oculus SDK License Agreement (the "License");
 * you may not use the Oculus SDK except in compliance with the License,
 * which is provided at the time of installation or download, or which
 * otherwise accompanies this software in either electronic or hard copy form.
 *
 * You may obtain a copy of the License at
 *
 * https://developer.oculus.com/licenses/oculussdk/
 *
 * Unless required by applicable law or agreed to in writing, the Oculus SDK
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using System;
using UnityEngine;

namespace Oculus.Interaction.Input
{
    public interface IDataSource
    {
        int CurrentDataVersion { get; }
        void MarkInputDataRequiresUpdate();
        event Action InputDataAvailable;
    }

    public interface IDataSource<TData> : IDataSource
    {
        TData GetData();
    }

    public abstract class DataSource<TData> : MonoBehaviour, IDataSource<TData>
        where TData : class, ICopyFrom<TData>, new()
    {
        public bool Started => _started;
        protected bool _started = false;
        private bool _requiresUpdate = true;

        [Flags]
        public enum UpdateModeFlags
        {
            Manual = 0,
            UnityUpdate = 1 << 0,
            UnityFixedUpdate = 1 << 1,
            UnityLateUpdate = 1 << 2,
            AfterPreviousStep = 1 << 3
        }

        [Header("Update")]
        [SerializeField]
        private UpdateModeFlags _updateMode;
        public UpdateModeFlags UpdateMode => _updateMode;

        [SerializeField, Interface(typeof(IDataSource))]
        [Optional(OptionalAttribute.Flag.DontHide)]
        private UnityEngine.Object _updateAfter;

        private IDataSource UpdateAfter;
        private int _currentDataVersion;

        protected bool UpdateModeAfterPrevious => (_updateMode & UpdateModeFlags.AfterPreviousStep) != 0;

        // Notifies that new data is available for query via GetData() method.
        // Do not use this event if you are reading data from a `Oculus.Interaction.Input.Hand` object,
        // instead, use the `Updated` event on that class.
        public event Action InputDataAvailable = delegate { };

        public virtual int CurrentDataVersion => _currentDataVersion;

        #region Unity Lifecycle
        protected virtual void Start()
        {
            this.BeginStart(ref _started);
            if (_updateAfter != null)
            {
                UpdateAfter = _updateAfter as IDataSource;
                this.AssertField(UpdateAfter, nameof(UpdateAfter));
            }
            this.EndStart(ref _started);
        }

        protected virtual void OnEnable()
        {
            if (_started)
            {
                if (UpdateModeAfterPrevious && UpdateAfter != null)
                {
                    UpdateAfter.InputDataAvailable += MarkInputDataRequiresUpdate;
                }
            }
        }

        protected virtual void OnDisable()
        {
            if (_started)
            {
                if (UpdateAfter != null)
                {
                    UpdateAfter.InputDataAvailable -= MarkInputDataRequiresUpdate;
                }
            }
        }

        protected virtual void Update()
        {
            if ((_updateMode & UpdateModeFlags.UnityUpdate) != 0)
            {
                MarkInputDataRequiresUpdate();
            }
        }

        protected virtual void FixedUpdate()
        {
            if ((_updateMode & UpdateModeFlags.UnityFixedUpdate) != 0)
            {
                MarkInputDataRequiresUpdate();
            }
        }

        protected virtual void LateUpdate()
        {
            if ((_updateMode & UpdateModeFlags.UnityLateUpdate) != 0)
            {
                MarkInputDataRequiresUpdate();
            }
        }
        #endregion

        protected void ResetUpdateAfter(IDataSource updateAfter, UpdateModeFlags updateMode)
        {
            bool wasActive = isActiveAndEnabled;
            if (isActiveAndEnabled) { OnDisable(); }

            _updateMode = updateMode;
            UpdateAfter = updateAfter;
            _requiresUpdate = true;
            _currentDataVersion += 1;

            if (wasActive) { OnEnable(); }
        }

        public TData GetData()
        {
            if (RequiresUpdate())
            {
                UpdateData();
                _requiresUpdate = false;
            }

            return DataAsset;
        }

        protected bool RequiresUpdate()
        {
            return _requiresUpdate;
        }

        /// <summary>
        /// Marks the DataAsset stored as outdated, which means it will be
        /// re-processed JIT during the next call to GetData.
        /// </summary>
        public virtual void MarkInputDataRequiresUpdate()
        {
            _requiresUpdate = true;
            _currentDataVersion += 1;
            InputDataAvailable();
        }

        protected abstract void UpdateData();

        /// <summary>
        /// Returns the current DataAsset, without performing any updates.
        /// </summary>
        /// <returns>
        /// Null if no call to GetData has been made since this data source was initialized.
        /// </returns>
        protected abstract TData DataAsset { get; }

        #region Inject
        public void InjectAllDataSource(UpdateModeFlags updateMode, IDataSource updateAfter)
        {
            InjectUpdateMode(updateMode);
            InjectUpdateAfter(updateAfter);
        }

        public void InjectUpdateMode(UpdateModeFlags updateMode)
        {
            _updateMode = updateMode;
        }

        public void InjectUpdateAfter(IDataSource updateAfter)
        {
            _updateAfter = updateAfter as UnityEngine.Object;
            UpdateAfter = updateAfter;
        }
        #endregion
    }
}
