﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Serialization;

namespace EPOOutline
{
    public enum DilateQuality
    {
        Base,
        High,
        Ultra
    }

    public enum RenderingMode
    {
        LDR,
        HDR
    }

    public enum OutlineRenderingStrategy
    {
        Default,
        PerObject
    }

    public enum RenderStage
    {
        BeforeTransparents,
        AfterTransparents
    }

    public enum BufferSizeMode
    {
        WidthControllsHeight,
        HeightControlsWidth,
        Scaled,
        Native
    }

    [ExecuteAlways]
    [RequireComponent(typeof(Camera))]
    public class Outliner : MonoBehaviour
    {
#if UNITY_EDITOR
        private static GameObject lastSelectedOutliner;

        private static List<Outliner> outliners = new List<Outliner>();
#endif

        private static List<Outlinable> temporaryOutlinables = new List<Outlinable>(); 

        private OutlineParameters parameters = new OutlineParameters();

#if UNITY_EDITOR
        private OutlineParameters editorPreviewParameters = new OutlineParameters();
#endif

        private Camera targetCamera;

        [SerializeField]
        private RenderStage stage = RenderStage.AfterTransparents;

        [SerializeField]
        private OutlineRenderingStrategy renderingStrategy = OutlineRenderingStrategy.Default;

        [SerializeField]
        private RenderingMode renderingMode;

        [SerializeField]
        private long outlineLayerMask = -1;

        [SerializeField]
        private BufferSizeMode primaryBufferSizeMode;

        [SerializeField]
        [Range(0.15f, 1.0f)]
        private float primaryRendererScale = 0.75f;

        [SerializeField]
        private int primarySizeReference = 800;

        [SerializeField]
        [Range(0.0f, 2.0f)]
        private float blurShift = 1.0f;

        [SerializeField]
        [Range(0.0f, 2.0f)]
        private float dilateShift = 1.0f;

        [SerializeField]
        [FormerlySerializedAs("dilateIterrations")]
        private int dilateIterations = 1;

        [SerializeField]
        private DilateQuality dilateQuality;

        [SerializeField]
        [FormerlySerializedAs("blurIterrations")]
        private int blurIterations = 1;

        [SerializeField]
        private BlurType blurType = BlurType.Box;

        [Obsolete]
        public float InfoRendererScale
        {
            get
            {
                throw new NotImplementedException();
            }

            set
            {
                throw new NotImplementedException();
            }
        }
        
        public int PrimarySizeReference
        {
            get
            {
                return primarySizeReference;
            }

            set
            {
                primarySizeReference = value;
            }
        }

        public BufferSizeMode PrimaryBufferSizeMode
        {
            get
            {
                return primaryBufferSizeMode;
            }

            set
            {
                primaryBufferSizeMode = value;
            }
        }

        private CameraEvent Event
        {
            get
            {
                return stage == RenderStage.BeforeTransparents ? CameraEvent.AfterForwardOpaque : CameraEvent.BeforeImageEffects;
            }
        }

        public OutlineRenderingStrategy RenderingStrategy
        {
            get
            {
                return renderingStrategy;
            }

            set
            {
                renderingStrategy = value;
            }
        }

        public RenderStage RenderStage
        {
            get
            {
                return stage;
            }

            set
            {
                stage = value;
            }
        }

        public DilateQuality DilateQuality
        {
            get
            {
                return dilateQuality;
            }

            set
            {
                dilateQuality = value;
            }
        }

        private RenderingMode RenderingMode
        {
            get
            {
                return renderingMode;
            }

            set
            {
                renderingMode = value;
            }
        }

        public float BlurShift
        {
            get
            {
                return blurShift;
            }

            set
            {
                blurShift = Mathf.Clamp(value, 0, 2.0f);
            }
        }

        public float DilateShift
        {
            get
            {
                return dilateShift;
            }

            set
            {
                dilateShift = Mathf.Clamp(value, 0, 2.0f);
            }
        }

        public long OutlineLayerMask
        {
            get
            {
                return outlineLayerMask;
            }

            set
            {
                outlineLayerMask = value;
            }
        }

        public float PrimaryRendererScale
        {
            get
            {
                return primaryRendererScale;
            }

            set
            {
                primaryRendererScale = Mathf.Clamp01(value);
            }
        }

        [Obsolete("Fixed incorrect spelling. Use BlurIterations instead")]
        public int BlurIterrations
        {
            get
            {
                return BlurIterations;
            }

            set
            {
                BlurIterations = value;
            }
        }

        public int BlurIterations
        {
            get
            {
                return blurIterations;
            }

            set
            {
                blurIterations = value > 0 ? value : 0;
            }
        }

        public BlurType BlurType
        {
            get
            {
                return blurType;
            }

            set
            {
                blurType = value;
            }
        }

        [Obsolete("Fixed incorrect spelling. Use DilateIterations instead")]
        public int DilateIterration
        {
            get
            {
                return DilateIterations;
            }

            set
            {
                DilateIterations = value;
            }
        }

        public int DilateIterations
        {
            get
            {
                return dilateIterations;
            }

            set
            {
                dilateIterations = value > 0 ? value : 0;
            }
        }

        private void OnValidate()
        {
            if (blurIterations < 0)
                blurIterations = 0;

            if (dilateIterations < 0)
                dilateIterations = 0;
        }

        private void OnEnable()
        {
            if (targetCamera == null)
                targetCamera = GetComponent<Camera>();

            targetCamera.forceIntoRenderTexture = targetCamera.stereoTargetEye == StereoTargetEyeMask.None || !UnityEngine.XR.XRSettings.enabled;

#if UNITY_EDITOR
            outliners.Add(this);
#endif

            parameters.CheckInitialization();
            parameters.Buffer.name = "Outline";

#if UNITY_EDITOR
            editorPreviewParameters.CheckInitialization();

            editorPreviewParameters.Buffer.name = "Editor outline";
#endif
        }

        private void OnDestroy()
        {
#if UNITY_EDITOR
            GameObject.DestroyImmediate(editorPreviewParameters.BlitMesh);
            if (editorPreviewParameters.Buffer != null)
                editorPreviewParameters.Buffer.Dispose();
#endif

            GameObject.DestroyImmediate(parameters.BlitMesh);

            if (parameters.Buffer != null)
                parameters.Buffer.Dispose();
        }

        private void OnDisable()
        {
            if (targetCamera != null)
                UpdateBuffer(targetCamera, parameters.Buffer, true);

#if UNITY_EDITOR
            RemoveFromAllSceneViews();

            outliners.Remove(this);
#endif

#if UNITY_EDITOR
            foreach (var view in UnityEditor.SceneView.sceneViews)
            {
                var viewToUpdate = (UnityEditor.SceneView)view;

                viewToUpdate.camera.RemoveCommandBuffer(CameraEvent.BeforeImageEffects, editorPreviewParameters.Buffer);
                viewToUpdate.camera.RemoveCommandBuffer(CameraEvent.AfterForwardOpaque, editorPreviewParameters.Buffer);
            }
#endif
        }

        private void UpdateBuffer(Camera targetCamera, CommandBuffer buffer, bool removeOnly)
        {
            targetCamera.RemoveCommandBuffer(CameraEvent.BeforeImageEffects, buffer);
            targetCamera.RemoveCommandBuffer(CameraEvent.AfterForwardOpaque, buffer);
            if (removeOnly)
                return;

            targetCamera.AddCommandBuffer(Event, buffer);
        }

        private void OnPreRender()
        {
            if (GraphicsSettings.renderPipelineAsset != null)
                return;

            parameters.OutlinablesToRender.Clear();
            SetupOutline(targetCamera, parameters, false);
        }

        private void SetupOutline(Camera cameraToUse, OutlineParameters parametersToUse, bool isEditor)
        {
            UpdateBuffer(cameraToUse, parametersToUse.Buffer, false);
            UpdateParameters(parametersToUse, cameraToUse, isEditor);

            parametersToUse.Buffer.Clear();
            if (renderingStrategy == OutlineRenderingStrategy.Default)
            {
                OutlineEffect.SetupOutline(parametersToUse);
                parametersToUse.BlitMesh = null;
                parametersToUse.MeshPool.ReleaseAllMeshes();
            }
            else
            {
                temporaryOutlinables.Clear();
                temporaryOutlinables.AddRange(parametersToUse.OutlinablesToRender);

                parametersToUse.OutlinablesToRender.Clear();
                parametersToUse.OutlinablesToRender.Add(null);

                foreach (var outlinable in temporaryOutlinables)
                {
                    parametersToUse.OutlinablesToRender[0] = outlinable;
                    OutlineEffect.SetupOutline(parametersToUse);
                    parametersToUse.BlitMesh = null;
                }

                parametersToUse.MeshPool.ReleaseAllMeshes();
            }
        }

#if UNITY_EDITOR
        private void RemoveFromAllSceneViews()
        {
            foreach (var view in UnityEditor.SceneView.sceneViews)
            {
                var viewToUpdate = (UnityEditor.SceneView)view;
                var eventTransferer = viewToUpdate.camera.GetComponent<OnPreRenderEventTransferer>();
                if (eventTransferer != null)
                    eventTransferer.OnPreRenderEvent -= UpdateEditorCamera;

                viewToUpdate.camera.RemoveCommandBuffer(CameraEvent.BeforeImageEffects, editorPreviewParameters.Buffer);
                viewToUpdate.camera.RemoveCommandBuffer(CameraEvent.AfterForwardOpaque, editorPreviewParameters.Buffer);
            }
        }

        private void LateUpdate()
        {
            if (lastSelectedOutliner == null && outliners.Count > 0)
                lastSelectedOutliner = outliners[0].gameObject;

            var isSelected = Array.Find(UnityEditor.Selection.gameObjects, x => x == gameObject) ?? lastSelectedOutliner != null;
            if (isSelected)
                lastSelectedOutliner = gameObject;

            foreach (var view in UnityEditor.SceneView.sceneViews)
            {
                var viewToUpdate = (UnityEditor.SceneView)view;
                var eventTransferer = viewToUpdate.camera.GetComponent<OnPreRenderEventTransferer>();
                if (eventTransferer != null)
                    eventTransferer.OnPreRenderEvent -= UpdateEditorCamera;

                UpdateBuffer(viewToUpdate.camera, editorPreviewParameters.Buffer, true);
            }

            if (!isSelected)
                return;

            foreach (var view in UnityEditor.SceneView.sceneViews)
            {
                var viewToUpdate = (UnityEditor.SceneView)view;
                if (!viewToUpdate.sceneViewState.showImageEffects)
                    continue;

                var eventTransferer = viewToUpdate.camera.GetComponent<OnPreRenderEventTransferer>();
                if (eventTransferer == null)
                    eventTransferer = viewToUpdate.camera.gameObject.AddComponent<OnPreRenderEventTransferer>();

                eventTransferer.OnPreRenderEvent += UpdateEditorCamera;
            }
        }

        private void UpdateEditorCamera(Camera camera)
        {
            SetupOutline(camera, editorPreviewParameters, true);
        }
#endif

        public void UpdateSharedParameters(OutlineParameters parameters, Camera camera, bool editorCamera)
        {
            parameters.DilateQuality = DilateQuality;
            parameters.Camera = camera;
            parameters.IsEditorCamera = editorCamera;
            parameters.PrimaryBufferScale = primaryRendererScale;

            parameters.PrimaryBufferSizeMode = primaryBufferSizeMode;
            parameters.PrimaryBufferSizeReference = primarySizeReference;

            parameters.BlurIterations = blurIterations;
            parameters.BlurType = blurType;
            parameters.DilateIterations = dilateIterations;
            parameters.BlurShift = blurShift;
            parameters.DilateShift = dilateShift;
            parameters.UseHDR = camera.allowHDR && (RenderingMode == RenderingMode.HDR);
            parameters.EyeMask = camera.stereoTargetEye;

            parameters.OutlineLayerMask = outlineLayerMask;

            parameters.Prepare();
        }

        private void UpdateParameters(OutlineParameters parameters, Camera camera, bool editorCamera)
        {
            parameters.DepthTarget = RenderTargetUtility.ComposeTarget(parameters, BuiltinRenderTextureType.CameraTarget);

            var targetTexture = camera.targetTexture == null ? camera.activeTexture : camera.targetTexture;

            if (UnityEngine.XR.XRSettings.enabled
                && !parameters.IsEditorCamera
                && parameters.EyeMask != StereoTargetEyeMask.None)
            {
                var descriptor = UnityEngine.XR.XRSettings.eyeTextureDesc;
                parameters.TargetWidth = descriptor.width;
                parameters.TargetHeight = descriptor.height;
            }
            else
            {
                parameters.TargetWidth = targetTexture != null ? targetTexture.width : camera.scaledPixelWidth;
                parameters.TargetHeight = targetTexture != null ? targetTexture.height : camera.scaledPixelHeight;
            }

            parameters.Antialiasing = editorCamera ? (targetTexture == null ? 1 : targetTexture.antiAliasing) : CameraUtility.GetMSAA(targetCamera);

            parameters.Target = RenderTargetUtility.ComposeTarget(parameters, BuiltinRenderTextureType.CameraTarget);

            parameters.Camera = camera;

            Outlinable.GetAllActiveOutlinables(parameters.Camera, parameters.OutlinablesToRender);
            RendererFilteringUtility.Filter(parameters.Camera, parameters);
            UpdateSharedParameters(parameters, camera, editorCamera);
        }
    }
}