using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;

namespace UnityEngine.Rendering
{
    /// <summary>
    /// Debug Display Settings Volume
    /// </summary>
    public class DebugDisplaySettingsVolume : IDebugDisplaySettingsData
    {
        /// <summary>Current volume debug settings.</summary>
        public IVolumeDebugSettings volumeDebugSettings { get; }

        /// <summary>
        /// Constructor with the settings
        /// </summary>
        /// <param name="volumeDebugSettings">The volume debug settings object used for configuration.</param>
        public DebugDisplaySettingsVolume(IVolumeDebugSettings volumeDebugSettings)
        {
            this.volumeDebugSettings = volumeDebugSettings;
        }

        internal int volumeComponentEnumIndex;

        internal Dictionary<string, VolumeComponent> debugState = new Dictionary<string, VolumeComponent>();

        static class Styles
        {
            public static readonly GUIContent none = new GUIContent("None");
            public static readonly GUIContent editorCamera = new GUIContent("Editor Camera");
        }

        static class Strings
        {
            public static readonly string none = "None";
            public static readonly string camera = "Camera";
            public static readonly string parameter = "Parameter";
            public static readonly string component = "Component";
            public static readonly string debugViewNotSupported = "N/A";
            public static readonly string parameterNotOverrided = "-";
            public static readonly string volumeInfo = "Volume Info";
            public static readonly string gameObject = "GameObject";
            public static readonly string resultValue = "Result";
            public static readonly string resultValueTooltip = "The interpolated result value of the parameter. This value is used to render the camera.";
            public static readonly string globalDefaultValue = "Graphics Settings";
            public static readonly string globalDefaultValueTooltip = "Default value for this parameter, defined by the Default Volume Profile in Global Settings.";
            public static readonly string qualityLevelValue = "Quality Settings";
            public static readonly string qualityLevelValueTooltip = "Override value for this parameter, defined by the Volume Profile in the current SRP Asset.";
            public static readonly string global = "Global";
            public static readonly string local = "Local";
            public static readonly string volumeProfile = "Volume Profile";
        }

        const string k_PanelTitle = "Volume";

#if UNITY_EDITOR
        internal static void OpenInRenderingDebugger()
        {
            EditorApplication.ExecuteMenuItem("Window/Analysis/Rendering Debugger");
            var idx = DebugManager.instance.FindPanelIndex(k_PanelTitle);
            if (idx != -1)
                DebugManager.instance.RequestEditorWindowPanelIndex(idx);
        }
#endif

        internal static class WidgetFactory
        {
            public static DebugUI.EnumField CreateComponentSelector(SettingsPanel panel, Action<DebugUI.Field<int>, int> refresh)
            {
                int componentIndex = 0;
                var componentNames = new List<GUIContent>() { Styles.none };
                var componentValues = new List<int>() { componentIndex++ };

                var volumesAndTypes = VolumeManager.instance.GetVolumeComponentsForDisplay(GraphicsSettings.currentRenderPipelineAssetType);
                foreach (var type in volumesAndTypes)
                {
                    componentNames.Add(new GUIContent() { text = type.Item1 });
                    componentValues.Add(componentIndex++);
                }

                return new DebugUI.EnumField
                {
                    displayName = Strings.component,
                    getter = () => panel.data.volumeDebugSettings.selectedComponent,
                    setter = value => panel.data.volumeDebugSettings.selectedComponent = value,
                    enumNames = componentNames.ToArray(),
                    enumValues = componentValues.ToArray(),
                    getIndex = () => panel.data.volumeComponentEnumIndex,
                    setIndex = value => { panel.data.volumeComponentEnumIndex = value; },
                    onValueChanged = refresh
                };
            }

            public static DebugUI.ObjectPopupField CreateCameraSelector(SettingsPanel panel, Action<DebugUI.Field<Object>, Object> refresh)
            {
                return new DebugUI.ObjectPopupField
                {
                    displayName = Strings.camera,
                    getter = () => panel.data.volumeDebugSettings.selectedCamera,
                    setter = value =>
                    {
                        var c = panel.data.volumeDebugSettings.cameras.ToArray();
                        panel.data.volumeDebugSettings.selectedCameraIndex = Array.IndexOf(c, value as Camera);
                    },
                    getObjects = () => panel.data.volumeDebugSettings.cameras,
                    onValueChanged = refresh
                };
            }

            static DebugUI.Widget CreateVolumeParameterWidget(string name, bool isResultParameter, VolumeParameter param, Func<bool> isHiddenCallback = null)
            {
#if UNITY_EDITOR || DEVELOPMENT_BUILD
                if (param != null)
                {
                    var parameterType = param.GetType();
                    if (parameterType == typeof(ColorParameter))
                    {
                        var p = (ColorParameter)param;
                        return new DebugUI.ColorField()
                        {
                            displayName = name,
                            hdr = p.hdr,
                            showAlpha = p.showAlpha,
                            getter = () => p.value,
                            setter = value => p.value = value,
                            isHiddenCallback = isHiddenCallback
                        };
                    }

                    var typeInfo = parameterType.GetTypeInfo();
                    var genericArguments = typeInfo.BaseType.GenericTypeArguments;
                    if (genericArguments.Length > 0 && genericArguments[0].IsArray)
                    {
                        return new DebugUI.ObjectListField()
                        {
                            displayName = name,
                            getter = () => (Object[])parameterType.GetProperty("value").GetValue(param, null),
                            type = parameterType
                        };
                    }

                    return new DebugUI.Value()
                    {
                        displayName = name,
                        getter = () =>
                        {
                            var property = param.GetType().GetProperty("value");
                            if (property == null)
                                return "-";

                            if (isResultParameter || param.overrideState)
                            {
                                var value = property.GetValue(param);
                                var propertyType = property.PropertyType;
                                if (value == null || value.Equals(null))
                                    return Strings.none + $" ({propertyType.Name})";

                                var toString = propertyType.GetMethod("ToString", Type.EmptyTypes);
                                if ((toString == null) || (toString.DeclaringType == typeof(object)) || (toString.DeclaringType == typeof(UnityEngine.Object)))
                                {
                                    // Check if the parameter has a name
                                    var nameProp = property.PropertyType.GetProperty("name");
                                    if (nameProp == null)
                                        return Strings.debugViewNotSupported;

                                    var valueString = nameProp.GetValue(value);
                                    return valueString ?? Strings.none;
                                }

                                return value.ToString();
                            }

                            return Strings.parameterNotOverrided;
                        },
                        isHiddenCallback = isHiddenCallback
                    };
                }
    #endif
                return new DebugUI.Value();
            }

            static DebugUI.Value s_EmptyDebugUIValue = new DebugUI.Value { getter = () => string.Empty };

            struct VolumeParameterChain
            {
                public DebugUI.Widget.NameAndTooltip nameAndTooltip;
                public VolumeProfile volumeProfile;
                public VolumeComponent volumeComponent;
                public Volume volume;
            }

            static VolumeComponent GetSelectedVolumeComponent(VolumeProfile profile, Type selectedType)
            {
                if (profile != null)
                {
                    foreach (var component in profile.components)
                        if (component.GetType() == selectedType)
                            return component;
                }
                return null;
            }

            static List<VolumeParameterChain> GetResolutionChain(DebugDisplaySettingsVolume data)
            {
                List<VolumeParameterChain> chain = new List<VolumeParameterChain>();

                Type selectedType = data.volumeDebugSettings.selectedComponentType;
                if (selectedType == null)
                    return chain;

                var volumeManager = VolumeManager.instance;
                var stack = data.volumeDebugSettings.selectedCameraVolumeStack ?? volumeManager.stack;
                var stackComponent = stack.GetComponent(selectedType);
                if (stackComponent == null)
                    return chain;

                var result = new VolumeParameterChain()
                {
                    nameAndTooltip = new DebugUI.Widget.NameAndTooltip()
                    {
                        name = Strings.resultValue,
                        tooltip = Strings.resultValueTooltip,
                    },
                    volumeComponent = stackComponent,
                };

                chain.Add(result);

                // Add volume components that override default values
                var volumes = data.volumeDebugSettings.GetVolumes();
                foreach (var volume in volumes)
                {
                    var profile = volume.HasInstantiatedProfile() ? volume.profile : volume.sharedProfile;
                    var overrideComponent = GetSelectedVolumeComponent(profile, selectedType);
                    if (overrideComponent != null)
                    {
                        var overrideVolume = new VolumeParameterChain()
                        {
                            nameAndTooltip = new DebugUI.Widget.NameAndTooltip()
                            {
                                name = profile.name,
                                tooltip = profile.name,
                            },
                            volumeProfile = profile,
                            volumeComponent = overrideComponent,
                            volume = volume
                        };
                        chain.Add(overrideVolume);
                    }
                }

                // Add custom default profiles
                if (volumeManager.customDefaultProfiles != null)
                {
                    foreach (var customProfile in volumeManager.customDefaultProfiles)
                    {
                        var customProfileComponent = GetSelectedVolumeComponent(customProfile, selectedType);
                        if (customProfileComponent != null)
                        {
                            var overrideVolume = new VolumeParameterChain()
                            {
                                nameAndTooltip = new DebugUI.Widget.NameAndTooltip()
                                {
                                    name = customProfile.name,
                                    tooltip = customProfile.name,
                                },
                                volumeProfile = customProfile,
                                volumeComponent = customProfileComponent,
                            };
                            chain.Add(overrideVolume);
                        }
                    }
                }

                // Add Quality Settings
                if (volumeManager.globalDefaultProfile != null)
                {
                    var qualitySettingsComponent = GetSelectedVolumeComponent(volumeManager.qualityDefaultProfile, selectedType);
                    if (qualitySettingsComponent != null)
                    {
                        var overrideVolume = new VolumeParameterChain()
                        {
                            nameAndTooltip = new DebugUI.Widget.NameAndTooltip()
                            {
                                name = Strings.qualityLevelValue,
                                tooltip = Strings.qualityLevelValueTooltip,
                            },
                            volumeProfile = volumeManager.qualityDefaultProfile,
                            volumeComponent = qualitySettingsComponent,
                        };
                        chain.Add(overrideVolume);
                    }
                }

                // Add Graphics Settings
                if (volumeManager.globalDefaultProfile != null)
                {
                    var graphicsSettingsComponent = GetSelectedVolumeComponent(volumeManager.globalDefaultProfile, selectedType);
                    if (graphicsSettingsComponent != null)
                    {
                        var overrideVolume = new VolumeParameterChain()
                        {
                            nameAndTooltip = new DebugUI.Widget.NameAndTooltip()
                            {
                                name = Strings.globalDefaultValue,
                                tooltip = Strings.globalDefaultValueTooltip,
                            },
                            volumeProfile = volumeManager.globalDefaultProfile,
                            volumeComponent = graphicsSettingsComponent,
                        };
                        chain.Add(overrideVolume);
                    }
                }

                return chain;
            }

            public static DebugUI.Table CreateVolumeTable(DebugDisplaySettingsVolume data)
            {
                var table = new DebugUI.Table()
                {
                    displayName = Strings.parameter,
                    isReadOnly = true,
                    isHiddenCallback = () => data.volumeDebugSettings.selectedComponent == 0
                };

                var resolutionChain = GetResolutionChain(data);
                if (resolutionChain.Count == 0)
                    return table;

                GenerateTableRows(table, resolutionChain);
                GenerateTableColumns(table, data, resolutionChain);

                float timer = 0.0f, refreshRate = 0.2f;
                var volumes = data.volumeDebugSettings.GetVolumes();
                table.isHiddenCallback = () =>
                {
                    timer += Time.deltaTime;
                    if (timer >= refreshRate)
                    {
                        if (data.volumeDebugSettings.selectedCamera != null)
                        {
                            SetTableColumnVisibility(data, table);

                            var newVolumes = data.volumeDebugSettings.GetVolumes();
                            if (!volumes.SequenceEqual(newVolumes))
                            {
                                volumes = newVolumes;
                                DebugManager.instance.ReDrawOnScreenDebug();
                            }
                        }

                        timer = 0.0f;
                    }
                    return false;
                };

                return table;
            }

            private static void SetTableColumnVisibility(DebugDisplaySettingsVolume data, DebugUI.Table table)
            {
                var newResolutionChain = GetResolutionChain(data);
                for (int i = 1; i < newResolutionChain.Count; i++) // We always skip the interpolated stack that is in index 0
                {
                    bool visible = true;
                    if (newResolutionChain[i].volume != null)
                    {
                        visible = data.volumeDebugSettings.VolumeHasInfluence(newResolutionChain[i].volume);
                    }
                    else
                    {
                        visible = newResolutionChain[i].volumeComponent.active;

                        if (visible)
                        {
                            bool atLeastOneParameterIsOverriden = false;
                            foreach (var parameter in newResolutionChain[i].volumeComponent.parameterList)
                            {
                                if (parameter.overrideState == true)
                                {
                                    atLeastOneParameterIsOverriden = true;
                                    break;
                                }
                            }

                            visible &= atLeastOneParameterIsOverriden;
                        }
                    }

                    table.SetColumnVisibility(i, visible);
                }
            }

            private static void GenerateTableColumns(DebugUI.Table table, DebugDisplaySettingsVolume data, List<VolumeParameterChain> resolutionChain)
            {
                for (int i = 0; i < resolutionChain.Count; ++i)
                {
                    var chain = resolutionChain[i];
                    int iRowIndex = -1;

                    if (chain.volume != null)
                    {
                        ((DebugUI.Table.Row)table.children[++iRowIndex]).children.Add(new DebugUI.Value()
                        {
                            nameAndTooltip = chain.nameAndTooltip,
                            getter = () =>
                            {
                                var scope = chain.volume.isGlobal ? Strings.global : Strings.local;
                                var weight = data.volumeDebugSettings.GetVolumeWeight(chain.volume);
                                return scope + " (" + (weight * 100f) + "%)";
                            },
                            refreshRate = 0.2f
                        });
                        ((DebugUI.Table.Row)table.children[++iRowIndex]).children.Add(new DebugUI.ObjectField() { displayName = string.Empty, getter = () => chain.volume });
                    }
                    else
                    {
                        ((DebugUI.Table.Row)table.children[++iRowIndex]).children.Add(new DebugUI.Value()
                        {
                            nameAndTooltip = chain.nameAndTooltip,
                            getter = () => string.Empty
                        });
                        ((DebugUI.Table.Row)table.children[++iRowIndex]).children.Add(s_EmptyDebugUIValue);
                    }

                    ((DebugUI.Table.Row)table.children[++iRowIndex]).children.Add(chain.volumeProfile != null ? new DebugUI.ObjectField() { displayName = string.Empty, getter = () => chain.volumeProfile } :
                        s_EmptyDebugUIValue);

                    ((DebugUI.Table.Row)table.children[++iRowIndex]).children.Add(s_EmptyDebugUIValue);

                    bool isResultParameter = i == 0;
                    for (int j = 0; j < chain.volumeComponent.parameterList.Count; ++j)
                    {
                        var parameter = chain.volumeComponent.parameterList[j];
                        ((DebugUI.Table.Row)table.children[++iRowIndex]).children.Add(CreateVolumeParameterWidget(chain.nameAndTooltip.name, isResultParameter, parameter));
                    }
                }
            }

            private static void GenerateTableRows(DebugUI.Table table, List<VolumeParameterChain> resolutionChain)
            {
                // First row for volume info
                var volumeInfoRow = new DebugUI.Table.Row()
                {
                    displayName = Strings.volumeInfo,
                    opened = true, // Open by default for the in-game view
                };

                table.children.Add(volumeInfoRow);

                // Second row, links to volume gameobjects
                var gameObjectRow = new DebugUI.Table.Row()
                {
                    displayName = Strings.gameObject,
                };

                table.children.Add(gameObjectRow);

                // Third row, links to volume profile assets
                var volumeProfileRow = new DebugUI.Table.Row()
                {
                    displayName = Strings.volumeProfile,
                };
                table.children.Add(volumeProfileRow);

                var separatorRow = new DebugUI.Table.Row()
                {
                    displayName =  string.Empty ,
                };

                table.children.Add(separatorRow);

                var results = resolutionChain[0].volumeComponent;
                for (int i = 0; i < results.parameterList.Count; ++i)
                {
                    var parameter = results.parameterList[i];

#if UNITY_EDITOR
                    string displayName = UnityEditor.ObjectNames.NicifyVariableName(parameter.debugId); // In the editor, make the name more readable
#elif DEVELOPMENT_BUILD
                    string displayName = parameter.debugId; // In the development player, just the debug id
#else
                    string displayName = i.ToString(); // Everywhere else, just a dummy id ( TODO: The Volume panel code should be stripped completely in nom-development builds )
#endif

                    table.children.Add(new DebugUI.Table.Row()
                    {
                        displayName = displayName
                    });
                }
            }
        }

        [DisplayInfo(name = k_PanelTitle, order = int.MaxValue)]
        internal class SettingsPanel : DebugDisplaySettingsPanel<DebugDisplaySettingsVolume>
        {
            public SettingsPanel(DebugDisplaySettingsVolume data)
                : base(data)
            {
                AddWidget(WidgetFactory.CreateCameraSelector(this, (_, __) => Refresh()));
                AddWidget(WidgetFactory.CreateComponentSelector(this, (_, __) => Refresh()));
                m_VolumeTable = WidgetFactory.CreateVolumeTable(m_Data);
                AddWidget(m_VolumeTable);
            }

            DebugUI.Table m_VolumeTable = null;
            void Refresh()
            {
                var panel = DebugManager.instance.GetPanel(PanelName);
                if (panel == null)
                    return;

                bool needsRefresh = false;
                if (m_VolumeTable != null)
                {
                    needsRefresh = true;
                    panel.children.Remove(m_VolumeTable);
                }

                if (m_Data.volumeDebugSettings.selectedComponent > 0 && m_Data.volumeDebugSettings.selectedCamera != null)
                {
                    needsRefresh = true;
                    m_VolumeTable = WidgetFactory.CreateVolumeTable(m_Data);
                    AddWidget(m_VolumeTable);
                    panel.children.Add(m_VolumeTable);
                }

                if (needsRefresh)
                    DebugManager.instance.ReDrawOnScreenDebug();
            }
        }

        #region IDebugDisplaySettingsData
        /// <summary>
        /// Checks whether ANY of the debug settings are currently active.
        /// </summary>
        public bool AreAnySettingsActive => false; // Volume Debug Panel doesn't need to modify the renderer data, therefore this property returns false

        /// <inheritdoc/>
        public IDebugDisplaySettingsPanelDisposable CreatePanel()
        {
            return new SettingsPanel(this);
        }

        #endregion
    }
}