using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine.Assertions;

namespace UnityEngine.Rendering
{
    /// <summary>
    /// An Asset which holds a set of settings to use with a <see cref="Volume"/>.
    /// </summary>
    [PipelineHelpURL("UniversalRenderPipelineAsset","Volume-Profile")]
    [PipelineHelpURL("HDRenderPipelineAsset","create-a-volume-profile")]
    [Icon("Packages/com.unity.render-pipelines.core/Editor/Icons/Processed/VolumeProfile Icon.asset")]
    public sealed class VolumeProfile : ScriptableObject
    {
        /// <summary>
        /// A list of every setting that this Volume Profile stores.
        /// </summary>
        public List<VolumeComponent> components = new List<VolumeComponent>();

        /// <summary>
        /// **Note**: For Internal Use Only<br/>
        /// A dirty check used to redraw the profile inspector when something has changed. This is
        /// currently only used in the editor.
        /// </summary>
        [Obsolete("This field was only public for editor access. #from(6000.0)")]
        public bool isDirty
        {
            get => dirtyState != DirtyState.None;
            set
            {
                if (value)
                    dirtyState |= DirtyState.Other;
                else
                    dirtyState &= ~DirtyState.Other;
            }
        }

        [Flags] internal enum DirtyState
        {
            None = 0,
            DirtyByComponentChange = 1,
            DirtyByProfileReset = 2,
            Other = 4
        }

        internal DirtyState dirtyState;

        void OnEnable()
        {
            // Make sure every setting is valid. If a profile holds a script that doesn't exist
            // anymore, nuke it to keep the volume clean. Note that if you delete a script that is
            // currently in use in a volume you'll still get a one-time error in the console, it's
            // harmless and happens because Unity does a redraw of the editor (and thus the current
            // frame) before the recompilation step.
            components.RemoveAll(x => x == null);
        }

        // The lifetime of ScriptableObjects is different from MonoBehaviours. When the last reference to a
        // VolumeProfile goes out of scope (e.g. when a scene containing Volume components is unloaded), Unity will call
        // OnDisable() on the VolumeProfile. We need to release the internal resources in this case to avoid leaks.
        internal void OnDisable()
        {
            if (components == null)
               return;

            for (int i = 0; i < components.Count; i++)
            {
                if (components[i] != null)
                    components[i].Release();
            }
        }

        /// <summary>
        /// Resets the dirty state of the Volume Profile. Unity uses this to force-refresh and redraw the
        /// Volume Profile editor when you modify the Asset via script instead of the Inspector.
        /// </summary>
        public void Reset()
            => dirtyState |= DirtyState.DirtyByProfileReset;

        /// <summary>
        /// Adds a <see cref="VolumeComponent"/> to this Volume Profile.
        /// </summary>
        /// <remarks>
        /// You can only have a single component of the same type per Volume Profile.
        /// </remarks>
        /// <typeparam name="T">A type of <see cref="VolumeComponent"/>.</typeparam>
        /// <param name="overrides">Specifies whether Unity should automatically override all the settings when
        /// you add a <see cref="VolumeComponent"/> to the Volume Profile.</param>
        /// <returns>The instance for the given type that you added to the Volume Profile</returns>
        /// <seealso cref="Add"/>
        public T Add<T>(bool overrides = false)
            where T : VolumeComponent
        {
            return (T)Add(typeof(T), overrides);
        }

        /// <summary>
        /// Adds a <see cref="VolumeComponent"/> to this Volume Profile.
        /// </summary>
        /// <remarks>
        /// You can only have a single component of the same type per Volume Profile.
        /// </remarks>
        /// <param name="type">A type that inherits from <see cref="VolumeComponent"/>.</param>
        /// <param name="overrides">Specifies whether Unity should automatically override all the settings when
        /// you add a <see cref="VolumeComponent"/> to the Volume Profile.</param>
        /// <returns>The instance created for the given type that has been added to the profile</returns>
        /// <seealso cref="Add{T}"/>
        public VolumeComponent Add(Type type, bool overrides = false)
        {
            if (Has(type))
                throw new InvalidOperationException("Component already exists in the volume");

            var component = (VolumeComponent)CreateInstance(type);
#if UNITY_EDITOR
            component.hideFlags = HideFlags.HideInInspector | HideFlags.HideInHierarchy;
            component.name = type.Name;
#endif
            component.SetAllOverridesTo(overrides);
            components.Add(component);
            dirtyState |= DirtyState.DirtyByComponentChange;
            return component;
        }

        /// <summary>
        /// Removes a <see cref="VolumeComponent"/> from this Volume Profile.
        /// </summary>
        /// <remarks>
        /// This method does nothing if the type does not exist in the Volume Profile.
        /// </remarks>
        /// <typeparam name="T">A type of <see cref="VolumeComponent"/>.</typeparam>
        /// <seealso cref="Remove"/>
        public void Remove<T>()
            where T : VolumeComponent
        {
            Remove(typeof(T));
        }

        /// <summary>
        /// Removes a <see cref="VolumeComponent"/> from this Volume Profile.
        /// </summary>
        /// <remarks>
        /// This method does nothing if the type does not exist in the Volume Profile.
        /// </remarks>
        /// <param name="type">A type that inherits from <see cref="VolumeComponent"/>.</param>
        /// <seealso cref="Remove{T}"/>
        public void Remove(Type type)
        {
            int toRemove = -1;

            for (int i = 0; i < components.Count; i++)
            {
                if (components[i].GetType() == type)
                {
                    toRemove = i;
                    break;
                }
            }

            if (toRemove >= 0)
            {
                components.RemoveAt(toRemove);
                dirtyState |= DirtyState.DirtyByComponentChange;
            }
        }

        /// <summary>
        /// Checks if this Volume Profile contains the <see cref="VolumeComponent"/> you pass in.
        /// </summary>
        /// <typeparam name="T">A type of <see cref="VolumeComponent"/>.</typeparam>
        /// <returns><c>true</c> if the <see cref="VolumeComponent"/> exists in the Volume Profile,
        /// <c>false</c> otherwise.</returns>
        /// <seealso cref="Has"/>
        /// <seealso cref="HasSubclassOf"/>
        public bool Has<T>()
            where T : VolumeComponent
        {
            return Has(typeof(T));
        }

        /// <summary>
        /// Checks if this Volume Profile contains the <see cref="VolumeComponent"/> you pass in.
        /// </summary>
        /// <param name="type">A type that inherits from <see cref="VolumeComponent"/>.</param>
        /// <returns><c>true</c> if the <see cref="VolumeComponent"/> exists in the Volume Profile,
        /// <c>false</c> otherwise.</returns>
        /// <seealso cref="Has{T}"/>
        /// <seealso cref="HasSubclassOf"/>
        public bool Has(Type type)
        {
            foreach (var component in components)
            {
                if (component.GetType() == type)
                    return true;
            }

            return false;
        }

        /// <summary>
        /// Checks if this Volume Profile contains the <see cref="VolumeComponent"/>, which is a subclass of <paramref name="type"/>,
        /// that you pass in.
        /// </summary>
        /// <param name="type">A type that inherits from <see cref="VolumeComponent"/>.</param>
        /// <returns><c>true</c> if the <see cref="VolumeComponent"/> exists in the Volume Profile,
        /// <c>false</c> otherwise.</returns>
        /// <seealso cref="Has"/>
        /// <seealso cref="Has{T}"/>
        public bool HasSubclassOf(Type type)
        {
            foreach (var component in components)
            {
                if (component.GetType().IsSubclassOf(type))
                    return true;
            }

            return false;
        }

        /// <summary>
        /// Gets the <see cref="VolumeComponent"/> of the specified type, if it exists.
        /// </summary>
        /// <typeparam name="T">A type of <see cref="VolumeComponent"/>.</typeparam>
        /// <param name="component">The output argument that contains the <see cref="VolumeComponent"/>
        /// or <c>null</c>.</param>
        /// <returns><c>true</c> if the <see cref="VolumeComponent"/> is in the Volume Profile,
        /// <c>false</c> otherwise.</returns>
        /// <seealso cref="TryGet{T}(Type, out T)"/>
        /// <seealso cref="TryGetSubclassOf{T}"/>
        /// <seealso cref="TryGetAllSubclassOf{T}"/>
        public bool TryGet<T>(out T component)
            where T : VolumeComponent
        {
            return TryGet(typeof(T), out component);
        }

        /// <summary>
        /// Gets the <see cref="VolumeComponent"/> of the specified type, if it exists.
        /// </summary>
        /// <typeparam name="T">A type of <see cref="VolumeComponent"/></typeparam>
        /// <param name="type">A type that inherits from <see cref="VolumeComponent"/>.</param>
        /// <param name="component">The output argument that contains the <see cref="VolumeComponent"/>
        /// or <c>null</c>.</param>
        /// <returns><c>true</c> if the <see cref="VolumeComponent"/> is in the Volume Profile,
        /// <c>false</c> otherwise.</returns>
        /// <seealso cref="TryGet{T}(out T)"/>
        /// <seealso cref="TryGetSubclassOf{T}"/>
        /// <seealso cref="TryGetAllSubclassOf{T}"/>
        public bool TryGet<T>(Type type, out T component)
            where T : VolumeComponent
        {
            component = null;

            foreach (var comp in components)
            {
                if (comp.GetType() == type)
                {
                    component = (T)comp;
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Gets the <see cref="VolumeComponent"/>, which is a subclass of <paramref name="type"/>, if
        /// it exists.
        /// </summary>
        /// <typeparam name="T">A type of <see cref="VolumeComponent"/>.</typeparam>
        /// <param name="type">A type that inherits from <see cref="VolumeComponent"/>.</param>
        /// <param name="component">The output argument that contains the <see cref="VolumeComponent"/>
        /// or <c>null</c>.</param>
        /// <returns><c>true</c> if the <see cref="VolumeComponent"/> is in the Volume Profile,
        /// <c>false</c> otherwise.</returns>
        /// <seealso cref="TryGet{T}(Type, out T)"/>
        /// <seealso cref="TryGet{T}(out T)"/>
        /// <seealso cref="TryGetAllSubclassOf{T}"/>
        public bool TryGetSubclassOf<T>(Type type, out T component)
            where T : VolumeComponent
        {
            component = null;

            foreach (var comp in components)
            {
                if (comp.GetType().IsSubclassOf(type))
                {
                    component = (T)comp;
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Gets all the <see cref="VolumeComponent"/> that are subclasses of the specified type,
        /// if there are any.
        /// </summary>
        /// <typeparam name="T">A type of <see cref="VolumeComponent"/>.</typeparam>
        /// <param name="type">A type that inherits from <see cref="VolumeComponent"/>.</param>
        /// <param name="result">The output list that contains all the <see cref="VolumeComponent"/>
        /// if any. Note that Unity does not clear this list.</param>
        /// <returns><c>true</c> if any <see cref="VolumeComponent"/> have been found in the profile,
        /// <c>false</c> otherwise.</returns>
        /// <seealso cref="TryGet{T}(Type, out T)"/>
        /// <seealso cref="TryGet{T}(out T)"/>
        /// <seealso cref="TryGetSubclassOf{T}"/>
        public bool TryGetAllSubclassOf<T>(Type type, List<T> result)
            where T : VolumeComponent
        {
            Assert.IsNotNull(components);
            int count = result.Count;

            foreach (var comp in components)
            {
                if (comp.GetType().IsSubclassOf(type))
                    result.Add((T)comp);
            }

            return count != result.Count;
        }

        internal int GetComponentListHashCode()
        {
            unchecked
            {
                var hashCode = HashFNV1A32.Create();

                for (int i = 0; i < components.Count; i++)
                    hashCode.Append(components[i].GetType().GetHashCode());

                return hashCode.value;
            }
        }

        /// <summary>
        /// Removes any components that were destroyed externally from the iternal list of components
        /// </summary>
        internal void Sanitize()
        {
            for (int i = components.Count - 1; i >= 0; i--)
                if (components[i] == null)
                    components.RemoveAt(i);
        }

#if UNITY_EDITOR
        void OnValidate()
        {
            // Delay the callback because when undoing the deletion of a VolumeComponent from a profile,
            // it's possible VolumeComponent.OnEnable() has not yet been called, resulting in a crash when trying to
            // update the default state.
            EditorApplication.delayCall += () =>
            {
                if (VolumeManager.instance.isInitialized)
                    VolumeManager.instance.OnVolumeProfileChanged(this);
            };
        }
#endif
    }
}