using UnityEngine;
using UnityEngine.Rendering;

#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.SceneManagement;
#endif

#if !UNITY_2019_1_OR_NEWER

public struct ScriptableRenderContext { }

public static class RenderPipelineManager
{
    public static event Action<ScriptableRenderContext, Camera> endCameraRendering;
}

#endif

namespace Popcron
{
    [ExecuteInEditMode]
    [AddComponentMenu("")]
    public class GizmosInstance : MonoBehaviour
    {
        private const int DefaultQueueSize = 4096;

        private static GizmosInstance instance;
        private static bool hotReloaded = true;
        private static Material defaultMaterial;
        private static Plane[] cameraPlanes = new Plane[6];

        private Color color = Color.white;
        private Matrix4x4 matrix;
        private Material overrideMaterial;
        private int queueIndex = 0;
        private int lastFrame;
        private Element[] queue = new Element[DefaultQueueSize];

        /// <summary>
        /// The material being used to render
        /// </summary>
        public static Material Material
        {
            get
            {
                GizmosInstance inst = GetOrCreate();
                if (inst.overrideMaterial)
                {
                    return inst.overrideMaterial;
                }

                return DefaultMaterial;
            }
            set
            {
                GizmosInstance inst = GetOrCreate();
                inst.overrideMaterial = value;
            }
        }

        /// <summary>
        /// The matrix to use
        /// </summary>
        public static Matrix4x4 Matrix
        {
            get
            {
                GizmosInstance inst = GetOrCreate();
                if (!inst.matrix.isIdentity)
                {
                    return inst.matrix;
                }

                return Matrix4x4.identity;
            }
            set
            {
                GizmosInstance inst = GetOrCreate();
                inst.matrix = value;
            }
        }

        /// <summary>
        /// The matrix to use
        /// </summary>
        public static Color Color
        {
            get
            {
                GizmosInstance inst = GetOrCreate();
                return inst.color;
            }
            set
            {
                GizmosInstance inst = GetOrCreate();
                inst.color = value;
            }
        }

        /// <summary>
        /// The default line renderer material
        /// </summary>
        public static Material DefaultMaterial
        {
            get
            {
                if (!defaultMaterial)
                {
                    // Unity has a built-in shader that is useful for drawing
                    // simple colored things.
                    Shader shader = Shader.Find("Hidden/Internal-Colored");
                    defaultMaterial = new Material(shader)
                    {
                        hideFlags = HideFlags.HideAndDontSave
                    };

                    // Turn on alpha blending
                    defaultMaterial.SetInt("_SrcBlend", (int)BlendMode.SrcAlpha);
                    defaultMaterial.SetInt("_DstBlend", (int)BlendMode.OneMinusSrcAlpha);
                    defaultMaterial.SetInt("_Cull", (int)CullMode.Off);
                    defaultMaterial.SetInt("_ZWrite", 0);
                }

                return defaultMaterial;
            }
        }

        internal static GizmosInstance GetOrCreate()
        {
            if (hotReloaded || !instance)
            {
                bool markDirty = false;
                GizmosInstance[] gizmosInstances = FindObjectsOfType<GizmosInstance>();
                for (int i = 0; i < gizmosInstances.Length; i++)
                {
                    instance = gizmosInstances[i];

                    //destroy any extra gizmo instances
                    if (i > 0)
                    {
                        if (Application.isPlaying)
                        {
                            Destroy(gizmosInstances[i]);
                        }
                        else
                        {
                            DestroyImmediate(gizmosInstances[i]);
                            markDirty = true;
                        }
                    }
                }

                //none were found, create a new one
                if (!instance)
                {
                    instance = new GameObject(typeof(GizmosInstance).FullName).AddComponent<GizmosInstance>();
                    instance.gameObject.hideFlags = HideFlags.HideInHierarchy | HideFlags.HideInInspector;

                    markDirty = true;
                }

#if UNITY_EDITOR
                //mark scene as dirty
                if (markDirty && !Application.isPlaying)
                {
                    Scene scene = SceneManager.GetActiveScene();
                    EditorSceneManager.MarkSceneDirty(scene);
                }
#endif

                hotReloaded = false;
            }

            return instance;
        }

        private float CurrentTime
        {
            get
            {
                float time = 0f;
                if (Application.isPlaying)
                {
                    time = Time.time;
                }
                else
                {
#if UNITY_EDITOR
                    time = (float)EditorApplication.timeSinceStartup;
#endif
                }

                return time;
            }
        }

        /// <summary>
        /// Submits an array of points to draw into the queue.
        /// </summary>
        internal static void Submit(Vector3[] points, Color? color2, bool dashed)
        {
            GizmosInstance inst = GetOrCreate();

            //if new frame, reset index
            if (inst.lastFrame != Time.frameCount)
            {
                inst.lastFrame = Time.frameCount;
                inst.queueIndex = 0;
            }

            //multiply points by matrix
            for (int i = 0; i < points.Length; i++)
            {
                points[i] = inst.matrix.MultiplyPoint3x4(points[i]);
            }

            //excedeed the length, so make it even bigger
            if (inst.queueIndex >= inst.queue.Length)
            {
                Element[] bigger = new Element[inst.queue.Length + DefaultQueueSize];
                for (int i = inst.queue.Length; i < bigger.Length; i++)
                {
                    bigger[i] = new Element();
                }

                Array.Copy(inst.queue, 0, bigger, 0, inst.queue.Length);
                inst.queue = bigger;
            }

            inst.queue[inst.queueIndex].color = inst.color;
            inst.queue[inst.queueIndex].points = points;
            inst.queue[inst.queueIndex].dashed = dashed;
            inst.queue[inst.queueIndex].matrix = inst.matrix;

            inst.queueIndex++;
        }

        private void OnEnable()
        {
            //populate queue with empty elements
            queue = new Element[DefaultQueueSize];
            for (int i = 0; i < DefaultQueueSize; i++)
            {
                queue[i] = new Element();
            }

            if (GraphicsSettings.renderPipelineAsset == null)
            {
                Camera.onPostRender += OnRendered;
            }
            else
            {
                RenderPipelineManager.endCameraRendering += OnRendered;
            }
        }

        private void OnDisable()
        {
            if (GraphicsSettings.renderPipelineAsset == null)
            {
                Camera.onPostRender -= OnRendered;
            }
            else
            {
                RenderPipelineManager.endCameraRendering -= OnRendered;
            }
        }

        private void OnRendered(ScriptableRenderContext context, Camera camera) => OnRendered(camera);

        private bool ShouldRenderCamera(Camera camera)
        {
            if (!camera)
            {
                return false;
            }

            //allow the scene and main camera always
            bool isSceneCamera = false;
#if UNITY_EDITOR
            SceneView sceneView = SceneView.currentDrawingSceneView;
            if (sceneView == null)
            {
                sceneView = SceneView.lastActiveSceneView;
            }

            if (sceneView != null && sceneView.camera == camera)
            {
                isSceneCamera = true;
            }
#endif
            if (isSceneCamera || camera.CompareTag("MainCamera"))
            {
                return true;
            }

            if (camera.name.StartsWith("Mirror "))
            {
                return true;
            }

            //it passed through the filter
            if (Gizmos.CameraFilter?.Invoke(camera) == true)
            {
                return true;
            }

            return false;
        }

        private bool IsVisibleByCamera(Element points, Camera camera)
        {
            if (!camera)
            {
                return false;
            }

            //essentially check if at least 1 point is visible by the camera
            for (int i = 0; i < points.points.Length; i++)
            {
                Vector3 vp = camera.WorldToViewportPoint(points.points[i], camera.stereoActiveEye);
                if (vp.x >= 0 && vp.x <= 1 && vp.y >= 0 && vp.y <= 1)
                {
                    return true;
                }
            }

            return false;
        }

        //private void Update()
        //{
        //    //always render something
        //    Gizmos.Line(default, default);
        //}

        private void OnRendered(Camera camera)
        {
            Material.SetPass(Gizmos.Pass);

            //shouldnt be rendering
            if (!Gizmos.Enabled)
            {
                queueIndex = 0;
            }

            //check if this camera is ok to render with
            if (!ShouldRenderCamera(camera))
            {
                GL.PushMatrix();
                GL.Begin(GL.LINES);

                //bla bla bla

                GL.End();
                GL.PopMatrix();
                return;
            }

            Vector3 offset = Gizmos.Offset;

            GL.PushMatrix();
            GL.MultMatrix(Matrix4x4.identity);
            GL.Begin(GL.LINES);

            bool alt = CurrentTime % 1 > 0.5f;
            float dashGap = Mathf.Clamp(Gizmos.DashGap, 0.01f, 32f);
            bool frustumCull = Gizmos.FrustumCulling;
            List<Vector3> points = new List<Vector3>();

            //draw le elements
            for (int e = 0; e < queueIndex; e++)
            {
                //just in case
                if (queue.Length <= e)
                {
                    break;
                }

                Element element = queue[e];

                //dont render this thingy if its not inside the frustum
                if (frustumCull)
                {
                    if (!IsVisibleByCamera(element, camera))
                    {
                        continue;
                    }
                }

                points.Clear();
                if (element.dashed)
                {
                    //subdivide
                    for (int i = 0; i < element.points.Length - 1; i++)
                    {
                        Vector3 pointA = element.points[i];
                        Vector3 pointB = element.points[i + 1];
                        Vector3 direction = pointB - pointA;
                        if (direction.sqrMagnitude > dashGap * dashGap * 2f)
                        {
                            float magnitude = direction.magnitude;
                            int amount = Mathf.RoundToInt(magnitude / dashGap);
                            direction /= magnitude;

                            for (int p = 0; p < amount - 1; p++)
                            {
                                if (p % 2 == (alt ? 1 : 0))
                                {
                                    float startLerp = p / (amount - 1f);
                                    float endLerp = (p + 1) / (amount - 1f);
                                    Vector3 start = Vector3.Lerp(pointA, pointB, startLerp);
                                    Vector3 end = Vector3.Lerp(pointA, pointB, endLerp);
                                    points.Add(start);
                                    points.Add(end);
                                }
                            }
                        }
                        else
                        {
                            points.Add(pointA);
                            points.Add(pointB);
                        }
                    }
                }
                else
                {
                    points.AddRange(element.points);
                }

                GL.Color(element.color);
                for (int i = 0; i < points.Count; i++)
                {
                    GL.Vertex(points[i] + offset);
                }
            }

            GL.End();
            GL.PopMatrix();
        }
    }
}
