using System;
using System.Collections.Generic;

namespace UnityEngine.Rendering.Universal
{

    public class VolumetricFogPass : ScriptableRenderPass
    {
        static readonly int sp_volumetric_fog_texture = Shader.PropertyToID("_g_volumetric_fog_texture");
        static readonly int sp_noiseTexture = Shader.PropertyToID("_volumetric_fog_noiseTexture");
        static readonly int sp_densityTexture = Shader.PropertyToID("_volumetric_fog_densityTexture");
        static readonly int sp_lightingTexture = Shader.PropertyToID("_volumetric_fog_lightingTexture");
        static readonly int sp_scatteringCoefe = Shader.PropertyToID("_scatteringCoef");
        static readonly int sp_absorptionCoef = Shader.PropertyToID("_absorptionCoef");
        static readonly int sp_phaseAnisotropy = Shader.PropertyToID("_phaseAnisotropy");
        static readonly int sp_clipToWorld = Shader.PropertyToID("_clipToWorld");
        static readonly int sp_cameraWorldPos = Shader.PropertyToID("_cameraWorldPos");
        static readonly int sp_cameraWorldForwardDir = Shader.PropertyToID("_cameraWorldForwardDir");
        static readonly int sp_USE_VOLUMETRIC_FOG = Shader.PropertyToID("USE_VOLUMETRIC_FOG");

        static readonly int default_depth_id = Shader.PropertyToID("_CameraDepthAttachment");

        RenderTargetIdentifier m_Source;
        RenderTargetIdentifier m_SourceDepth;

        Texture2D m_disableTexture = null;
        RenderTexture m_densityTexture = null;

        RenderTexture m_lightingTexture = null;
        RenderTexture m_integratedTexture = null;

        Texture3D m_noise = null;
        Mesh m_quadMesh = null;

        Material m_material = null;





        const string m_ProfilerTag = "Volumetric Fog";

        public VolumetricFogPass(RenderPassEvent evt)
        {
            renderPassEvent = evt;
            //m_rthIntegrate.Init("_g_volumetric_fog_texture");

            CreateTexture();

            CreateQuadMesh();

            //m_material = new Material(Shader.Find("X2M/Evironment/VolumetricFog"));

        }

        void CreateTexture()
        {
            // the volume is XYZ : 64 * 32 * 16
            if (SystemInfo.SupportsTextureFormat(TextureFormat.Alpha8))
                m_noise = new Texture3D(16, 16, 16, TextureFormat.Alpha8, false);
            else
                m_noise = new Texture3D(16, 16, 16, TextureFormat.ARGB32, false);
            m_noise.name = "NoiseTexture";
            m_noise.wrapMode = TextureWrapMode.Repeat;
            m_noise.filterMode = FilterMode.Trilinear;

            Color[] colors = m_noise.GetPixels();
            for (int i = 0; i < colors.Length; i++)
            {
                float a = Random.value;
                colors[i] = new Color(a, a, a, a);

            }
            m_noise.SetPixels(colors);
            m_noise.Apply(false, true);

            m_disableTexture = new Texture2D(1, 1, TextureFormat.ARGB32, false, true);
            m_disableTexture.name = "VolumetricFogDisableTexture";
            m_disableTexture.SetPixel(0, 0, new Color(0, 0, 0, 1));
            m_disableTexture.Apply(false, true);

            // 3DWidth * 3DHeight
            const int rtWidth = 32 * 32;
            // 3DDepth
            const int rtHeight = 16;

            m_densityTexture = new RenderTexture(rtWidth, rtHeight, 0, RenderTextureFormat.ARGB32);
            m_densityTexture.name = "m_densityTexture";
            m_densityTexture.filterMode = FilterMode.Bilinear;
            m_densityTexture.wrapMode = TextureWrapMode.Clamp;
            m_densityTexture.autoGenerateMips = true;
            m_densityTexture.dimension = TextureDimension.Tex2D;
            m_densityTexture.anisoLevel = 0;
            m_densityTexture.Create();

            if (SystemInfo.SupportsRenderTextureFormat(RenderTextureFormat.ARGBHalf))
            {
                m_lightingTexture = new RenderTexture(rtWidth, rtHeight, 0, RenderTextureFormat.ARGBHalf);
            }
            else if (SystemInfo.SupportsRenderTextureFormat(RenderTextureFormat.ARGBFloat))
            {
                m_lightingTexture = new RenderTexture(rtWidth, rtHeight, 0, RenderTextureFormat.ARGBFloat);
            }
            else
            {
                m_lightingTexture = new RenderTexture(rtWidth, rtHeight, 0, RenderTextureFormat.ARGB32);
            }
            m_lightingTexture.name = "m_lightingTexture";
            m_lightingTexture.filterMode = FilterMode.Bilinear;
            m_lightingTexture.wrapMode = TextureWrapMode.Clamp;
            m_lightingTexture.autoGenerateMips = false;
            m_lightingTexture.dimension = TextureDimension.Tex2D;
            m_lightingTexture.anisoLevel = 0;
            m_lightingTexture.Create();

            m_integratedTexture = new RenderTexture(rtWidth, rtHeight, 0, RenderTextureFormat.ARGB32);
            m_integratedTexture.name = "m_integratedTexture";
            m_integratedTexture.filterMode = FilterMode.Bilinear;
            m_integratedTexture.wrapMode = TextureWrapMode.Clamp;
            m_integratedTexture.autoGenerateMips = false;
            m_integratedTexture.dimension = TextureDimension.Tex2D;
            m_integratedTexture.anisoLevel = 0;
            m_integratedTexture.Create();

        }

        void CreateQuadMesh()
        {
            m_quadMesh = new Mesh();

            m_quadMesh.name = "Quad";
            List<Vector3> vertex = new List<Vector3>();
            vertex.Add(new Vector3(1, 1, 0));
            vertex.Add(new Vector3(1, -1, 0));
            vertex.Add(new Vector3(-1, -1, 0));
            vertex.Add(new Vector3(-1, 1, 0));
            List<Vector2> uvs = new List<Vector2>();
            //uvs.Add(new Vector2(1, 1));
            //uvs.Add(new Vector2(1, 0));
            //uvs.Add(new Vector2(0, 0));
            //uvs.Add(new Vector2(0, 1));
            uvs.Add(new Vector2(1, 0));
            uvs.Add(new Vector2(1, 1));
            uvs.Add(new Vector2(0, 1));
            uvs.Add(new Vector2(0, 0));


            List<int> indics = new List<int>();
            indics.Add(0);
            indics.Add(1);
            indics.Add(2);

            indics.Add(0);
            indics.Add(2);
            indics.Add(3);

            m_quadMesh.SetVertices(vertex);
            m_quadMesh.SetUVs(0, uvs);
            m_quadMesh.SetIndices(indics.ToArray(), MeshTopology.Triangles, 0);
            m_quadMesh.UploadMeshData(true);

        }

        bool SetCmdInfo(ScriptableRenderContext context, CommandBuffer cb, Camera camera)
        {
            if (X2Engine.EnvironmentSystem.instance == null)
                return false;
            if (X2Engine.EnvironmentSystem.instance.profile == null)
                return false;

            var profile = X2Engine.EnvironmentSystem.instance.profile;
            if (profile.volumetricFogSetting.enable == false)
                return false;

            var densityList = profile.volumetricFogSetting.GetDensityList();
            if (densityList == null)
                return false;

            // NOTE: code was ported from: https://gamedev.stackexchange.com/questions/131978/shader-reconstructing-position-from-depth-in-vr-through-projection-matrix
            // More clerification of whats going on is needed!
            var p = GL.GetGPUProjectionMatrix(camera.projectionMatrix, false);// Unity flips its 'Y' vector depending on if its in VR, Editor view or game view etc... (facepalm)
            p[2, 3] = p[3, 2] = 0.0f;
            p[3, 3] = 1.0f;
            var clipToWorld = Matrix4x4.Inverse(p * camera.worldToCameraMatrix) * Matrix4x4.TRS(new Vector3(0, 0, -p[2, 2]), Quaternion.identity, Vector3.one);

            //// test only
            //Vector4 testPos = camera.transform.position + camera.transform.forward * 10 + camera.transform.right * 5;
            //testPos.w = 1;
            //float distanceToCamera = Vector3.Distance(new Vector3(testPos.x, testPos.y, testPos.z), camera.transform.position);
            //Vector4 testPosInV = camera.worldToCameraMatrix * testPos;
            //Vector4 testPosInVP = GL.GetGPUProjectionMatrix(camera.projectionMatrix, false) * testPosInV;
            //testPosInVP /= testPosInVP.w;
            //Vector4 testInvPosInVP = new Vector4(testPosInVP.x, testPosInVP.y, 0, 1.0f);
            //Vector4 testInvPosInZeroPlane = clipToWorld * testInvPosInVP;
            //Vector3 testDir = new Vector3(testInvPosInZeroPlane.x, testInvPosInZeroPlane.y, testInvPosInZeroPlane.z) - camera.transform.position;
            //Vector3 testInvPos2 = testDir * -testPosInV.z + camera.transform.position;
            //// test end

            cb.SetRenderTarget(m_densityTexture);
            cb.ClearRenderTarget(true, true, Color.clear);

            densityList.Sort((a, b) =>
            {

                float da = Vector3.SqrMagnitude(a.transform.position - camera.transform.position);
                float db = Vector3.SqrMagnitude(b.transform.position - camera.transform.position);
                return da >= db ? -1 : 1;

            });

            for (int i = 0; i < densityList.Count; i++)
            {
                Material material = densityList[i].GetMaterial(m_material);
                material.SetTexture(sp_noiseTexture, m_noise);
                material.SetVector(sp_cameraWorldPos, camera.transform.position);
                material.SetVector(sp_cameraWorldForwardDir, camera.transform.forward);
                material.SetMatrix(sp_clipToWorld, clipToWorld);
                densityList[i].SetMaterailProperty(material);
                cb.DrawMesh(m_quadMesh, Matrix4x4.identity, material, 0, 0);
            }

            // set materail property
            {
                Material material = m_material;
                material.SetFloat(sp_absorptionCoef, profile.volumetricFogSetting.absorptionCoef);
                material.SetFloat(sp_scatteringCoefe, profile.volumetricFogSetting.scatteringCoef);
                material.SetFloat(sp_phaseAnisotropy, profile.volumetricFogSetting.phaseAnisotropy);
                material.SetVector(sp_cameraWorldPos, camera.transform.position);
                material.SetVector(sp_cameraWorldForwardDir, camera.transform.forward);
                material.SetMatrix(sp_clipToWorld, clipToWorld);


                cb.SetRenderTarget(m_lightingTexture);
                material.SetTexture(sp_densityTexture, m_densityTexture);
                cb.DrawMesh(m_quadMesh, Matrix4x4.identity, material, 0, 1);

                cb.SetRenderTarget(m_integratedTexture);
                material.SetTexture(sp_lightingTexture, m_lightingTexture);
                cb.DrawMesh(m_quadMesh, Matrix4x4.identity, material, 0, 2);

                cb.SetGlobalTexture(sp_volumetric_fog_texture, m_integratedTexture);

            }

            return true;
        }

        public void OnDestory()
        {
            if (m_densityTexture != null)
            {
                m_densityTexture.Release();
                m_densityTexture = null;
            }

            if (m_lightingTexture != null)
            {
                m_lightingTexture.Release();
                m_lightingTexture = null;
            }

            if (m_integratedTexture != null)
            {
                m_integratedTexture.Release();
                m_integratedTexture = null;
            }

        }



        void LoadMaterial()
        {
            if (m_material == null)
            {
                Shader shader = Shader.Find("X2M/Evironment/VolumetricFog");
                if (shader != null)
                {
                    m_material = new Material(shader);
                }
                else
                {
                    m_material = null;
                }
            }
        }

        public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescripor)
        {
            //RenderTextureDescriptor descriptor = new RenderTextureDescriptor(64 * 32, 16, RenderTextureFormat.ARGB32, 0, 1);

            //cmd.GetTemporaryRT(m_rthIntegrate.id, descriptor, FilterMode.Bilinear);
            //ConfigureTarget(m_TempRT.Identifier());

            LoadMaterial();

        }

        //public override void Setup(ref RenderTextureDescriptor cameraTextureDescriptor,
        //    RenderTargetHandle color, RenderTargetHandle depth)
        //{
        //    m_Source = color;
        //    m_SourceDepth = depth;
        //}
        public void Setup(ref RenderTextureDescriptor cameraTextureDescriptor,
            RenderTargetIdentifier color, RenderTargetIdentifier depth)
        {
            m_Source = color;
            m_SourceDepth = depth;
        }

        /// <inheritdoc/>
        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            Camera camera = renderingData.cameraData.camera;
            if (!(camera.cameraType == CameraType.SceneView || camera.cameraType == CameraType.Game))
            {
                return;
            }

            CommandBuffer cmd = CommandBufferPool.Get(m_ProfilerTag);
            cmd.SetGlobalFloat(sp_USE_VOLUMETRIC_FOG, 1);

            if (m_material != null && m_quadMesh != null)
            {
                if (SetCmdInfo(context, cmd, camera) == false)
                {
                    cmd.SetGlobalTexture(sp_volumetric_fog_texture, m_disableTexture);
                }
            }
            else
            {
                cmd.SetGlobalTexture(sp_volumetric_fog_texture, m_disableTexture);
            }

            if (m_SourceDepth.Equals(new RenderTargetIdentifier(default_depth_id)))
            {
                cmd.SetRenderTarget(m_Source, m_SourceDepth);
            }
            else
            {
                cmd.SetRenderTarget(m_Source);
            }

            context.ExecuteCommandBuffer(cmd);
            CommandBufferPool.Release(cmd);
        }

        public override void FrameCleanup(CommandBuffer cmd)
        {
            if (cmd == null)
                throw new ArgumentNullException("cmd");

            //cmd.ReleaseTemporaryRT(m_TempRT.id);
        }
    }

}
