using UnityEngine;
using UnityEngine.Rendering;
using System.Collections;

namespace VLB
{
    public static class MaterialManager
    {
        public static MaterialPropertyBlock materialPropertyBlock = new MaterialPropertyBlock();

        public enum BlendingMode
        {
            Additive,
            SoftAdditive,
            TraditionalTransparency,
            Count
        }

        public enum ColorGradient
        {
            Off,
            MatrixLow,
            MatrixHigh,
            Count
        }

        public enum Noise3D
        {
            Off,
            On,
            Count
        }

        public static class SD
        {
            public enum DepthBlend
            {
                Off,
                On,
                Count
            }

            public enum DynamicOcclusion
            {
                Off,
                ClippingPlane,
                DepthTexture,
                Count
            }

            public enum MeshSkewing
            {
                Off,
                On,
                Count
            }

            public enum ShaderAccuracy
            {
                Fast,
                High,
                Count
            }
        }

        public static class HD
        {
            public enum Attenuation
            {
                Linear,
                Quadratic,
                Count
            }

            public enum Shadow
            {
                Off,
                On,
                Count
            }

            public enum Cookie
            {
                Off,
                SingleChannel,
                RGBA,
                Count
            }
        }

        static readonly UnityEngine.Rendering.BlendMode[] BlendingMode_SrcFactor = new UnityEngine.Rendering.BlendMode[(int)BlendingMode.Count]
        {
            UnityEngine.Rendering.BlendMode.One,                // Additive
            UnityEngine.Rendering.BlendMode.OneMinusDstColor,   // SoftAdditive
            UnityEngine.Rendering.BlendMode.SrcAlpha,           // TraditionalTransparency
        };

        static readonly UnityEngine.Rendering.BlendMode[] BlendingMode_DstFactor = new UnityEngine.Rendering.BlendMode[(int)BlendingMode.Count]
        {
            UnityEngine.Rendering.BlendMode.One,                // Additive
            UnityEngine.Rendering.BlendMode.One,                // SoftAdditive
            UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha,   // TraditionalTransparency
        };

        static readonly bool[] BlendingMode_AlphaAsBlack = new bool[(int)BlendingMode.Count]
        {
            true,   // Additive
            true,   // SoftAdditive
            false,  // TraditionalTransparency
        };

        interface IStaticProperties
        {
            int GetPropertiesCount();
            int GetMaterialID();
            void ApplyToMaterial(Material mat);
            ShaderMode GetShaderMode();
        }

        // STATIC PROPERTIES SD
        public struct StaticPropertiesSD : IStaticProperties
        {
            public ShaderMode GetShaderMode() { return ShaderMode.SD; }
            public static int staticPropertiesCount { get { return (int)BlendingMode.Count * (int)Noise3D.Count * (int)SD.DepthBlend.Count * (int)ColorGradient.Count * (int)SD.DynamicOcclusion.Count * (int)SD.MeshSkewing.Count * (int)SD.ShaderAccuracy.Count; } }
            public int GetPropertiesCount() { return staticPropertiesCount; }

            public BlendingMode blendingMode;
            public Noise3D noise3D;
            public SD.DepthBlend depthBlend;
            public ColorGradient colorGradient;
            public SD.DynamicOcclusion dynamicOcclusion;
            public SD.MeshSkewing meshSkewing;
            public SD.ShaderAccuracy shaderAccuracy;

            int blendingModeID      { get { return (int)blendingMode; } }
            int noise3DID           { get { return Config.Instance.featureEnabledNoise3D ? (int)noise3D : 0; } }
            int depthBlendID        { get { return Config.Instance.featureEnabledDepthBlend ? (int)depthBlend : 0; } }
            int colorGradientID     { get { return Config.Instance.featureEnabledColorGradient != FeatureEnabledColorGradient.Off ? (int)colorGradient : 0; } }
            int dynamicOcclusionID  { get { return Config.Instance.featureEnabledDynamicOcclusion ? (int)dynamicOcclusion : 0; } }
            int meshSkewingID       { get { return Config.Instance.featureEnabledMeshSkewing ? (int)meshSkewing : 0; } }
            int shaderAccuracyID    { get { return Config.Instance.featureEnabledShaderAccuracyHigh ? (int)shaderAccuracy : 0; } }

            public int GetMaterialID()
            {
                return (((((((blendingModeID)
                        * (int)Noise3D.Count + noise3DID)
                        * (int)SD.DepthBlend.Count + depthBlendID)
                        * (int)ColorGradient.Count + colorGradientID)
                        * (int)SD.DynamicOcclusion.Count + dynamicOcclusionID)
                        * (int)SD.MeshSkewing.Count + meshSkewingID)
                        * (int)SD.ShaderAccuracy.Count + shaderAccuracyID)
                        ;
            }

            public void ApplyToMaterial(Material mat)
            {
                mat.SetKeywordEnabled(ShaderKeywords.AlphaAsBlack, BlendingMode_AlphaAsBlack[(int)blendingMode]);
                mat.SetKeywordEnabled(ShaderKeywords.ColorGradientMatrixLow,  colorGradient == ColorGradient.MatrixLow);
                mat.SetKeywordEnabled(ShaderKeywords.ColorGradientMatrixHigh, colorGradient == ColorGradient.MatrixHigh);
                mat.SetKeywordEnabled(ShaderKeywords.SD.DepthBlend, depthBlend == SD.DepthBlend.On);
                mat.SetKeywordEnabled(ShaderKeywords.Noise3D, noise3D == Noise3D.On);
                mat.SetKeywordEnabled(ShaderKeywords.SD.OcclusionClippingPlane, dynamicOcclusion == SD.DynamicOcclusion.ClippingPlane);
                mat.SetKeywordEnabled(ShaderKeywords.SD.OcclusionDepthTexture, dynamicOcclusion == SD.DynamicOcclusion.DepthTexture);
                mat.SetKeywordEnabled(ShaderKeywords.SD.MeshSkewing, meshSkewing == SD.MeshSkewing.On);
                mat.SetKeywordEnabled(ShaderKeywords.SD.ShaderAccuracyHigh, shaderAccuracy == SD.ShaderAccuracy.High);

                mat.SetBlendingMode(ShaderProperties.BlendSrcFactor, BlendingMode_SrcFactor[(int)blendingMode]);
                mat.SetBlendingMode(ShaderProperties.BlendDstFactor, BlendingMode_DstFactor[(int)blendingMode]);
                mat.SetZTest(ShaderProperties.ZTest, CompareFunction.LessEqual);
            }
        }

        // STATIC PROPERTIES HD
        public struct StaticPropertiesHD : IStaticProperties
        {
            public ShaderMode GetShaderMode() { return ShaderMode.HD; }
            public static int staticPropertiesCount { get { return (int)BlendingMode.Count * (int)HD.Attenuation.Count * (int)Noise3D.Count * (int)ColorGradient.Count * (int)HD.Shadow.Count * (int)HD.Cookie.Count * Config.Instance.raymarchingQualitiesCount; } }
            public int GetPropertiesCount() { return staticPropertiesCount; }

            public BlendingMode blendingMode;
            public HD.Attenuation attenuation;
            public Noise3D noise3D;
            public ColorGradient colorGradient;
            public HD.Shadow shadow;
            public HD.Cookie cookie;
            public int raymarchingQualityIndex;

            int blendingModeID { get { return (int)blendingMode; } }
            int attenuationID { get { return (int)attenuation; } }
            int noise3DID { get { return Config.Instance.featureEnabledNoise3D ? (int)noise3D : 0; } }
            int colorGradientID { get { return Config.Instance.featureEnabledColorGradient != FeatureEnabledColorGradient.Off ? (int)colorGradient : 0; } }
            int dynamicOcclusionID { get { return Config.Instance.featureEnabledShadow ? (int)shadow : 0; } }
            int cookieID { get { return Config.Instance.featureEnabledCookie ? (int)cookie : 0; } }
            int raymarchingQualityID { get { return raymarchingQualityIndex; } }

            public int GetMaterialID()
            {
                return (((((((blendingModeID)
                        * (int)HD.Attenuation.Count + attenuationID)
                        * (int)Noise3D.Count + noise3DID)
                        * (int)ColorGradient.Count + colorGradientID)
                        * (int)HD.Shadow.Count + dynamicOcclusionID)
                        * (int)HD.Cookie.Count + cookieID)
                        * (int)Config.Instance.raymarchingQualitiesCount + raymarchingQualityID)
                        ;
            }

            public void ApplyToMaterial(Material mat)
            {
                mat.SetKeywordEnabled(ShaderKeywords.AlphaAsBlack, BlendingMode_AlphaAsBlack[(int)blendingMode]);
                mat.SetKeywordEnabled(ShaderKeywords.HD.AttenuationLinear, attenuation == HD.Attenuation.Linear);
                mat.SetKeywordEnabled(ShaderKeywords.HD.AttenuationQuad, attenuation == HD.Attenuation.Quadratic);
                mat.SetKeywordEnabled(ShaderKeywords.ColorGradientMatrixLow, colorGradient == ColorGradient.MatrixLow);
                mat.SetKeywordEnabled(ShaderKeywords.ColorGradientMatrixHigh, colorGradient == ColorGradient.MatrixHigh);
                mat.SetKeywordEnabled(ShaderKeywords.Noise3D, noise3D == Noise3D.On);
                mat.SetKeywordEnabled(ShaderKeywords.HD.Shadow, shadow == HD.Shadow.On);
                mat.SetKeywordEnabled(ShaderKeywords.HD.CookieSingleChannel, cookie == HD.Cookie.SingleChannel);
                mat.SetKeywordEnabled(ShaderKeywords.HD.CookieRGBA, cookie == HD.Cookie.RGBA);

                for (int i = 0; i < Config.Instance.raymarchingQualitiesCount; ++i)
                    mat.SetKeywordEnabled(ShaderKeywords.HD.GetRaymarchingQuality(i), raymarchingQualityIndex == i);

                mat.SetBlendingMode(ShaderProperties.BlendSrcFactor, BlendingMode_SrcFactor[(int)blendingMode]);
                mat.SetBlendingMode(ShaderProperties.BlendDstFactor, BlendingMode_DstFactor[(int)blendingMode]);
                mat.SetZTest(ShaderProperties.ZTest, CompareFunction.Always);
            }
        }

        public static Material NewMaterialPersistent(Shader shader, bool gpuInstanced)
        {
            if (!shader)
            {
                Debug.LogError("Invalid VLB Shader. Please try to reset the VLB Config asset or reinstall the plugin.");
                return null;
            }

            var material = new Material(shader);
            BatchingHelper.SetMaterialProperties(material, gpuInstanced);
            return material;
        }

        class MaterialsGroup
        {
            public Material[] materials = null;

            public MaterialsGroup(int count)
            {
                Debug.Assert(count > 0);
                materials = new Material[count];
            }
        }

        // SD Instanced Material Access
        static Hashtable ms_MaterialsGroupSD = new Hashtable(1);
        public static Material GetInstancedMaterial(uint groupID, ref StaticPropertiesSD staticProps)
        {
            IStaticProperties iStaticProp = staticProps;
            return GetInstancedMaterial(ms_MaterialsGroupSD, groupID, ref iStaticProp);
        }

        // HD Instanced Material Access
        static Hashtable ms_MaterialsGroupHD = new Hashtable(1);
        public static Material GetInstancedMaterial(uint groupID, ref StaticPropertiesHD staticProps)
        {
            IStaticProperties iStaticProp = staticProps;
            return GetInstancedMaterial(ms_MaterialsGroupHD, groupID, ref iStaticProp);
        }

        static Material GetInstancedMaterial(Hashtable groups, uint groupID, ref IStaticProperties staticProps) // pass StaticProperties by ref to avoid per value arg copy
        {
            MaterialsGroup group = (MaterialsGroup)groups[groupID];
            if (group == null)
            {
                group = new MaterialsGroup(staticProps.GetPropertiesCount());
                groups[groupID] = group;
            }

            int matID = staticProps.GetMaterialID();
            Debug.Assert(matID < staticProps.GetPropertiesCount());
            Debug.Assert(group.materials != null);
            var mat = group.materials[matID];
            if (mat == null)
            {
                mat = Config.Instance.NewMaterialTransient(staticProps.GetShaderMode(), gpuInstanced:true);
                if(mat)
                {
                    group.materials[matID] = mat;
                    staticProps.ApplyToMaterial(mat);
                }
            }

            return mat;
        }

        // Material Utils
        enum ZWrite { Off = 0, On = 1 }
        static void SetBlendingMode(this Material mat, int nameID, BlendMode value) { mat.SetInt(nameID, (int)value); }
        static void SetStencilRef(this Material mat, int nameID, int value) { mat.SetInt(nameID, value); }
        static void SetStencilComp(this Material mat, int nameID, CompareFunction value) { mat.SetInt(nameID, (int)value); }
        static void SetStencilOp(this Material mat, int nameID, StencilOp value) { mat.SetInt(nameID, (int)value); }
        static void SetCull(this Material mat, int nameID, CullMode value) { mat.SetInt(nameID, (int)value); }
        static void SetZWrite(this Material mat, int nameID, ZWrite value) { mat.SetInt(nameID, (int)value); }
        static void SetZTest(this Material mat, int nameID, CompareFunction value) { mat.SetInt(nameID, (int)value); }
    }
}
