#if PPV2_EXISTS
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using UnityEditor.SceneManagement;
using UnityEngine.Categorization;
using UnityEditor.Rendering.Converter;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.SceneManagement;
using BIRPRendering = UnityEngine.Rendering.PostProcessing;
using Object = UnityEngine.Object;
using URPRendering = UnityEngine.Rendering.Universal;

namespace UnityEditor.Rendering.Universal
{
    [URPHelpURL("features/rp-converter")]
    [Serializable]
    [PipelineConverter("Built-in", "Universal Render Pipeline (Universal Renderer)")]
    [ElementInfo(Name = "Post-Processing Stack v2",
                 Order = int.MaxValue,
                 Description = "This converter creates Universal Render Pipeline (URP) assets and corresponding Renderer assets, configuring their settings to match the equivalent settings from the Built-in Render Pipeline.")]

    internal class PPv2Converter : AssetsConverter
    {
        public override bool isEnabled => s_PostProcessTypesToSearch?.Count() > 0;
        public override string isDisabledMessage => "Missing types to search";

        private IEnumerable<PostProcessEffectSettingsConverter> effectConverters = null;

        private List<Object> postConversionDestroyables = null;

        protected override List<(string query, string description)> contextSearchQueriesAndIds
            => s_PostProcessTypesToSearch;

        static List<(string, string)> s_PostProcessTypesToSearch = new()
        {
            ("p: t:PostProcessVolume", "Contains Post Process Volume Component"),
            ("p: t:PostProcessLayer", "Contains Post Process Layer Component"),
            ("p: t:PostProcessProfile", "Contains Instance of Post Process Profile")
        };
        public override void BeforeConvert()
        {
            // Converters should already be set to null on domain reload,
            // but we're doing it here just in case anything somehow lingers.
            effectConverters = null;
            postConversionDestroyables = new List<Object>();
        }

        protected override Status ConvertObject(UnityEngine.Object obj, StringBuilder message)
        {
            BIRPRendering.PostProcessVolume[] oldVolumes = null;
            BIRPRendering.PostProcessLayer[] oldLayers = null;

            // TODO: Upcoming changes to GlobalObjectIdentifierToObjectSlow will allow
            //       this to be inverted, and the else to be deleted.
#if false
            if (obj is GameObject go)
            {
                oldVolumes = go.GetComponents<BIRPRendering.PostProcessVolume>();
                oldLayers = go.GetComponents<BIRPRendering.PostProcessLayer>();
            }
            else if (obj is MonoBehaviour mb)
            {
                oldVolumes = mb.GetComponents<BIRPRendering.PostProcessVolume>();
                oldLayers = mb.GetComponents<BIRPRendering.PostProcessLayer>();
            }
#else
            if (obj is GameObject go)
            {
                oldVolumes = go.GetComponentsInChildren<BIRPRendering.PostProcessVolume>();
                oldLayers = go.GetComponentsInChildren<BIRPRendering.PostProcessLayer>();
            }
            else if (obj is MonoBehaviour mb)
            {
                oldVolumes = mb.GetComponentsInChildren<BIRPRendering.PostProcessVolume>();
                oldLayers = mb.GetComponentsInChildren<BIRPRendering.PostProcessLayer>();
            }
#endif

            // Note: even if nothing needs to be converted, that should still count as success,
            //       though it shouldn't ever actually occur.
            var succeeded = true;

            if (effectConverters == null ||
                effectConverters.Count() == 0 ||
                effectConverters.Any(converter => converter == null))
            {
                effectConverters = GetAllBIRPConverters();
            }

            if (oldVolumes != null)
            {
                foreach (var oldVolume in oldVolumes)
                {
                    ConvertVolume(oldVolume, ref succeeded, message);
                }
            }

            if (oldLayers != null)
            {
                foreach (var oldLayer in oldLayers)
                {
                    ConvertLayer(oldLayer, ref succeeded, message);
                }
            }

            if (obj is BIRPRendering.PostProcessProfile oldProfile)
            {
                ConvertProfile(oldProfile, ref succeeded, message);
            }

            if (!succeeded)
            {
                return Status.Error;
            }
            else
            {
                var currentScene = SceneManager.GetActiveScene();
                EditorSceneManager.SaveScene(currentScene);
                return Status.Success;
            }
        }

        public override void AfterConvert()
        {
            for (var i = 0; i < postConversionDestroyables.Count; i++)
            {
                Object.DestroyImmediate(postConversionDestroyables[i], allowDestroyingAssets: true);
            }

            postConversionDestroyables.Clear();
        }

#region Conversion_Entry_Points

        private void ConvertVolume(BIRPRendering.PostProcessVolume oldVolume, ref bool succeeded,
            StringBuilder errorString)
        {
            if (!succeeded)
            {
                return;
            }

            if (!oldVolume)
            {
                // TODO: unless there's good way to tell the if the object is just missing because it was already
                //       converted as part of an earlier conversion object, then these two lines should be commented
                //       out or removed.  It should still return though.
                // succeeded = false;
                // errorString.AppendLine("PPv2 PostProcessVolume failed to be converted because the original asset reference was lost during conversion.");
                return;
            }

            if (PrefabUtility.IsPartOfPrefabInstance(oldVolume) &&
                !PrefabUtility.IsAddedComponentOverride(oldVolume))
            {
                // This is a property override on an instance of the component,
                // so override the component instance with the modifications.
                succeeded = ConvertVolumeInstance(oldVolume, errorString);
            }
            else
            {
                // The entire component is unique, so just convert it
                succeeded = ConvertVolumeComponent(oldVolume, errorString);
            }
        }

        private void ConvertLayer(BIRPRendering.PostProcessLayer oldLayer, ref bool succeeded,
            StringBuilder errorString)
        {
            if (!succeeded)
            {
                return;
            }

            if (!oldLayer)
            {
                // TODO: unless there's good way to tell the if the object is just missing because it was already
                //       converted as part of an earlier conversion object, then these two lines should be commented
                //       out or removed.  It should still return though.
                // succeeded = false;
                // errorString.AppendLine("PPv2 PostProcessLayer failed to be converted because the original asset reference was lost during conversion.");
                return;
            }

            if (PrefabUtility.IsPartOfPrefabInstance(oldLayer) &&
                !PrefabUtility.IsAddedComponentOverride(oldLayer))
            {
                // This is a property override on an instance of the component,
                // so override the component instance with the modifications.
                succeeded = ConvertLayerInstance(oldLayer, errorString);
            }
            else
            {
                // The entire component is unique, so just convert it
                succeeded = ConvertLayerComponent(oldLayer, errorString);
            }
        }

        private void ConvertProfile(BIRPRendering.PostProcessProfile oldProfile, ref bool succeeded,
            StringBuilder errorString)
        {
            if (!succeeded)
            {
                return;
            }

            if (!oldProfile)
            {
                errorString.AppendLine(
                    "PPv2 PostProcessProfile failed to be converted because the original asset reference was lost during conversion.");
                return;
            }

            ConvertVolumeProfileAsset(oldProfile, errorString, ref succeeded);

            // TODO:
            // - Perhaps old Profiles should only be deleted if they actually no longer have references,
            // just in case some some Volume conversions are skipped and still need references for future conversion.
            // - Alternatively, leave deletion of Profiles entirely to the user. (I think this is preferred)
        }

#endregion Conversion_Entry_Points

        private bool ConvertVolumeComponent(BIRPRendering.PostProcessVolume oldVolume, StringBuilder errorString)
        {
            // Don't convert if it appears to already have been converted.
            if (oldVolume.GetComponent<Volume>()) return true;

            var gameObject = oldVolume.gameObject;
            var newVolume = gameObject.AddComponent<Volume>();

            newVolume.priority = oldVolume.priority;
            newVolume.weight = oldVolume.weight;
            newVolume.blendDistance = oldVolume.blendDistance;
            newVolume.isGlobal = oldVolume.isGlobal;
            newVolume.enabled = oldVolume.enabled;

            var success = true;
            newVolume.sharedProfile = ConvertVolumeProfileAsset(oldVolume.sharedProfile, errorString, ref success);

            if (PrefabUtility.IsPartOfPrefabAsset(oldVolume))
            {
                postConversionDestroyables.Add(oldVolume);
            }
            else
            {
                Object.DestroyImmediate(oldVolume, allowDestroyingAssets: true);
            }

            EditorUtility.SetDirty(gameObject);

            return success;
        }

        private bool ConvertVolumeInstance(BIRPRendering.PostProcessVolume oldVolume, StringBuilder errorString)
        {
            // First get a reference to the local instance of the converted component
            // which may require immediately converting it at its origin location first.
            var newVolumeInstance = oldVolume.GetComponent<Volume>();
            if (!newVolumeInstance)
            {
                var oldVolumeOrigin = PrefabUtility.GetCorrespondingObjectFromSource(oldVolume);

                if (!ConvertVolumeComponent(oldVolumeOrigin, errorString))
                {
                    return false;
                }

                PrefabUtility.SavePrefabAsset(oldVolumeOrigin.gameObject);

                newVolumeInstance = oldVolume.GetComponent<Volume>();

                if (!newVolumeInstance)
                {
                    errorString.AppendLine(
                        "PPv2 PostProcessVolume failed to be converted because the instance object did not inherit the converted Prefab source.");
                    return false;
                }
            }

            bool success = true;
            var oldModifications = PrefabUtility.GetPropertyModifications(oldVolume);
            foreach (var oldModification in oldModifications)
            {
                if (oldModification.target is BIRPRendering.PostProcessVolume)
                {
                    if (oldModification.propertyPath.EndsWith("priority", StringComparison.InvariantCultureIgnoreCase))
                        newVolumeInstance.priority = oldVolume.priority;
                    else if (oldModification.propertyPath.EndsWith("weight",
                        StringComparison.InvariantCultureIgnoreCase))
                        newVolumeInstance.weight = oldVolume.weight;
                    else if (oldModification.propertyPath.EndsWith("blendDistance",
                        StringComparison.InvariantCultureIgnoreCase))
                        newVolumeInstance.blendDistance = oldVolume.blendDistance;
                    else if (oldModification.propertyPath.EndsWith("isGlobal",
                        StringComparison.InvariantCultureIgnoreCase))
                        newVolumeInstance.isGlobal = oldVolume.isGlobal;
                    else if (oldModification.propertyPath.EndsWith("enabled",
                        StringComparison.InvariantCultureIgnoreCase))
                        newVolumeInstance.enabled = oldVolume.enabled;
                    else if (oldModification.propertyPath.EndsWith("sharedProfile",
                        StringComparison.InvariantCultureIgnoreCase))
                        newVolumeInstance.sharedProfile =
                            ConvertVolumeProfileAsset(oldVolume.sharedProfile, errorString, ref success);

                    EditorUtility.SetDirty(newVolumeInstance);
                }
            }

            return success;
        }

        private bool ConvertLayerComponent(BIRPRendering.PostProcessLayer oldLayer, StringBuilder errorString)
        {
            var siblingCamera = oldLayer.GetComponent<Camera>().GetUniversalAdditionalCameraData();

            // PostProcessLayer requires a sibling Camera component, but
            // we check it here just in case something weird went happened.
            if (!siblingCamera)
            {
                errorString.AppendLine(
                    "PPv2 PostProcessLayer failed to be converted because the instance object was missing a required sibling Camera component.");
                return false;
            }

            // The presence of a PostProcessLayer implies the Camera should render post-processes
            siblingCamera.renderPostProcessing = true;

            siblingCamera.volumeLayerMask = oldLayer.volumeLayer;
            siblingCamera.volumeTrigger = oldLayer.volumeTrigger;
            siblingCamera.stopNaN = oldLayer.stopNaNPropagation;

            siblingCamera.antialiasingQuality =
                (URPRendering.AntialiasingQuality)oldLayer.subpixelMorphologicalAntialiasing.quality;

            switch (oldLayer.antialiasingMode)
            {
                case BIRPRendering.PostProcessLayer.Antialiasing.None:
                    siblingCamera.antialiasing = URPRendering.AntialiasingMode.None;
                    break;
                case BIRPRendering.PostProcessLayer.Antialiasing.FastApproximateAntialiasing:
                    siblingCamera.antialiasing = URPRendering.AntialiasingMode.FastApproximateAntialiasing;
                    break;
                case BIRPRendering.PostProcessLayer.Antialiasing.SubpixelMorphologicalAntialiasing:
                    siblingCamera.antialiasing = URPRendering.AntialiasingMode.SubpixelMorphologicalAntiAliasing;
                    break;
                default:
                    // Default to the the most performant mode, since "None" is an explicit option.
                    siblingCamera.antialiasing = URPRendering.AntialiasingMode.FastApproximateAntialiasing;
                    break;
            }

            if (PrefabUtility.IsPartOfPrefabAsset(oldLayer))
            {
                postConversionDestroyables.Add(oldLayer);
            }
            else
            {
                Object.DestroyImmediate(oldLayer, allowDestroyingAssets: true);
            }

            EditorUtility.SetDirty(siblingCamera.gameObject);

            return true;
        }

        private bool ConvertLayerInstance(BIRPRendering.PostProcessLayer oldLayer, StringBuilder errorString)
        {
            // First get a reference to the local instance of the camera (which is required by PostProcessingLayer)
            var siblingCamera = oldLayer.GetComponent<Camera>().GetUniversalAdditionalCameraData();
            if (!siblingCamera)
            {
                errorString.AppendLine(
                    "PPv2 PostProcessLayer failed to be converted because the instance object was missing a required sibling Camera component.");
                return false;
            }

            var oldModifications = PrefabUtility.GetPropertyModifications(oldLayer);
            foreach (var oldModification in oldModifications)
            {
                if (oldModification.target is BIRPRendering.PostProcessLayer)
                {
                    if (oldModification.propertyPath.EndsWith("volumeLayer",
                        StringComparison.InvariantCultureIgnoreCase))
                        siblingCamera.volumeLayerMask = oldLayer.volumeLayer;
                    else if (oldModification.propertyPath.EndsWith("volumeTrigger",
                        StringComparison.InvariantCultureIgnoreCase))
                        siblingCamera.volumeTrigger = oldLayer.volumeTrigger;
                    else if (oldModification.propertyPath.EndsWith("stopNaNPropagation",
                        StringComparison.InvariantCultureIgnoreCase))
                        siblingCamera.stopNaN = oldLayer.stopNaNPropagation;
                    else if (oldModification.propertyPath.EndsWith("quality",
                        StringComparison.InvariantCultureIgnoreCase))
                        siblingCamera.antialiasingQuality =
                            (URPRendering.AntialiasingQuality)oldLayer.subpixelMorphologicalAntialiasing.quality;
                    else if (oldModification.propertyPath.EndsWith("antialiasingMode",
                        StringComparison.InvariantCultureIgnoreCase))
                    {
                        switch (oldLayer.antialiasingMode)
                        {
                            case BIRPRendering.PostProcessLayer.Antialiasing.None:
                                siblingCamera.antialiasing = URPRendering.AntialiasingMode.None;
                                break;
                            case BIRPRendering.PostProcessLayer.Antialiasing.FastApproximateAntialiasing:
                                siblingCamera.antialiasing = URPRendering.AntialiasingMode.FastApproximateAntialiasing;
                                break;
                            case BIRPRendering.PostProcessLayer.Antialiasing.SubpixelMorphologicalAntialiasing:
                                siblingCamera.antialiasing =
                                    URPRendering.AntialiasingMode.SubpixelMorphologicalAntiAliasing;
                                break;
                            default:
                                // Default to the the most performant mode, since "None" is an explicit option.
                                siblingCamera.antialiasing = URPRendering.AntialiasingMode.FastApproximateAntialiasing;
                                break;
                        }
                    }

                    EditorUtility.SetDirty(siblingCamera);
                }
            }

            return true;
        }

        private VolumeProfile ConvertVolumeProfileAsset(BIRPRendering.PostProcessProfile oldProfile,
            StringBuilder errorString, ref bool success)
        {
            // Don't convert if it appears to already have been converted.
            if (!oldProfile) return null;

            var oldPath = AssetDatabase.GetAssetPath(oldProfile);
            var oldDirectory = Path.GetDirectoryName(oldPath);
            var oldName = Path.GetFileNameWithoutExtension(oldPath);
            var newPath = Path.Combine(oldDirectory, $"{oldName}(URP).asset");
            if (File.Exists(newPath))
            {
                return AssetDatabase.LoadAssetAtPath<VolumeProfile>(newPath);
            }

            var newProfile = ScriptableObject.CreateInstance<VolumeProfile>();
            try
            {
                AssetDatabase.CreateAsset(newProfile, newPath);
            }
            catch (Exception e)
            {
                errorString.AppendLine($"PPv2 PostProcessLayer failed to be converted with exception:\n{e}");
                success = false;

                if (!newProfile) return null;
            }

            foreach (var oldSettings in oldProfile.settings)
            {
                foreach (var effectConverter in effectConverters)
                {
                    effectConverter.AddConvertedProfileSettingsToProfile(oldSettings, newProfile);
                }
            }

            EditorUtility.SetDirty(newProfile);

            return newProfile;
        }

        public IEnumerable<PostProcessEffectSettingsConverter> GetAllBIRPConverters()
        {
            var baseType = typeof(PostProcessEffectSettingsConverter);

            var assembly = Assembly.GetAssembly(baseType);
            var derivedTypes = assembly
                .GetTypes()
                .Where(t =>
                    t.BaseType != null &&
                    t.BaseType == baseType)
                .Select(t => ScriptableObject.CreateInstance(t) as PostProcessEffectSettingsConverter);

            return derivedTypes;
        }
    }
}
#endif