#region License
//   Copyright 2019-2021 Kastellanos Nikolaos
//
//   Licensed under the Apache License, Version 2.0 (the "License");
//   you may not use this file except in compliance with the License.
//   You may obtain a copy of the License at
//
//       http://www.apache.org/licenses/LICENSE-2.0
//
//   Unless required by applicable law or agreed to in writing, software
//   distributed under the License is distributed on an "AS IS" BASIS,
//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//   See the License for the specific language governing permissions and
//   limitations under the License.
#endregion

using System;
using System.IO;
using System.Reflection;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace nkast.Aether.Shaders
{
    public class TilemapEffect : Effect, IEffectMatrices
    {
        #region Effect Parameters
        
        EffectParameter textureParam;
        EffectParameter textureAtlasParam;

        EffectParameter mapSizeParam;
        EffectParameter invAtlasSizeParam;
        
        EffectParameter diffuseColorParam;
        EffectParameter fogColorParam;
        EffectParameter fogVectorParam;

        // IEffectMatrices
        EffectParameter worldViewProjParam;

        #endregion

        #region Fields
        
        bool fogEnabled;
        bool vertexColorEnabled;

        Matrix world = Matrix.Identity;
        Matrix view = Matrix.Identity;
        Matrix projection = Matrix.Identity;

        Matrix worldView;

        Vector3 diffuseColor = Vector3.One;

        float alpha = 1;

        float fogStart = 0;
        float fogEnd = 1;
                
        Vector2 atlasSize;

        EffectDirtyFlags dirtyFlags = EffectDirtyFlags.All;

        static readonly string ResourceName = "nkast.Aether.Shaders.Resources.TilemapEffect";

        internal static byte[] LoadEffectResource(GraphicsDevice graphicsDevice, string name)
        {
            name = GetResourceName(graphicsDevice, name);
            using (Stream stream = typeof(TilemapEffect).Assembly.GetManifestResourceStream(name))
            using (MemoryStream ms = new MemoryStream())
            {
                stream.CopyTo(ms);
                return ms.ToArray();
            }
        }

        private static string GetResourceName(GraphicsDevice graphicsDevice, string name)
        {
            string platformName = "";
            string version = "";

#if XNA
            platformName = ".xna.WinReach";
#else
            switch (graphicsDevice.Adapter.Backend)
            {
                case GraphicsBackend.DirectX11:
                    platformName = ".dx11.fxo";
                    break;
                case GraphicsBackend.OpenGL:
                    platformName = ".ogl.fxo";
                    break;
                case GraphicsBackend.GLES:
                case GraphicsBackend.WebGL:
                    platformName = ".gles.fxo";
                    break;
                default:
                    throw new NotSupportedException("Backend");
            }

            // Detect version
            version = ".11";
            Version kniVersion = typeof(Effect).Assembly.GetName().Version;
            {
                if (kniVersion.Minor ==  9
                ||  kniVersion.Minor == 10
                ||  kniVersion.Minor == 11
                ||  kniVersion.Minor == 12
                ||  kniVersion.Minor == 13
                ||  kniVersion.Minor == 14)
                    version = ".10";
            }
            if (kniVersion.Major == 4)
            {
                if (kniVersion.Minor == 0
                ||  kniVersion.Minor == 1)
                    version = ".10";
                if (kniVersion.Minor == 2)
                    version = ".11";
            }
#endif

            return name + platformName + version;
        }

        #endregion
        
        #region Public Properties

        public Matrix Projection
        {
            get { return projection; }
            set
            {
                projection = value;
                dirtyFlags |= EffectDirtyFlags.WorldViewProj;
            }
        }

        public Matrix View
        {
            get { return view; }
            set
            {
                view = value;
                dirtyFlags |= EffectDirtyFlags.WorldViewProj | EffectDirtyFlags.Fog;
            }
        }

        public Matrix World
        {
            get { return world; }
            set
            {
                world = value;
                dirtyFlags |= EffectDirtyFlags.WorldViewProj | EffectDirtyFlags.Fog;
            }
        }
                
        /// <summary>
        /// Gets or sets the Tilemap texture.
        /// </summary>
        public Texture2D Texture
        {
            get { return textureParam.GetValueTexture2D(); }
            set { textureParam.SetValue(value); }
        }


        /// <summary>
        /// Gets or sets the Atlas texture.
        /// </summary>
        public Texture2D TextureAtlas
        {
            get { return textureAtlasParam.GetValueTexture2D(); }
            set { textureAtlasParam.SetValue(value); }
        }


        /// <summary>
        /// Gets or sets the Map size.
        /// </summary>
        public Vector2 MapSize
        {
            get { return mapSizeParam.GetValueVector2(); }
            set { mapSizeParam.SetValue(value); }
        }

        /// <summary>
        /// Gets or sets the Atlas size.
        /// </summary>
        public Vector2 AtlasSize
        {
            get { return atlasSize; }
            set 
            { 
                atlasSize = value;
                value.X = 1f/value.X;
                value.Y = 1f/value.Y;
                invAtlasSizeParam.SetValue(value);
            }
        }

        /// <summary>
        /// Gets or sets the material diffuse color (range 0 to 1).
        /// </summary>
        public Vector3 DiffuseColor
        {
            get { return diffuseColor; }

            set
            {
                diffuseColor = value;
                dirtyFlags |= EffectDirtyFlags.MaterialColor;
            }
        }


        /// <summary>
        /// Gets or sets the material alpha.
        /// </summary>
        public float Alpha
        {
            get { return alpha; }

            set
            {
                alpha = value;
                dirtyFlags |= EffectDirtyFlags.MaterialColor;
            }
        }


        /// <summary>
        /// Gets or sets the fog enable flag.
        /// </summary>
        public bool FogEnabled
        {
            get { return fogEnabled; }

            set
            {
                if (fogEnabled != value)
                {
                    fogEnabled = value;
                    dirtyFlags |= EffectDirtyFlags.FogEnable;
                    UpdateCurrentTechnique();
                }
            }
        }


        /// <summary>
        /// Gets or sets the fog start distance.
        /// </summary>
        public float FogStart
        {
            get { return fogStart; }

            set
            {
                fogStart = value;
                dirtyFlags |= EffectDirtyFlags.Fog;
            }
        }


        /// <summary>
        /// Gets or sets the fog end distance.
        /// </summary>
        public float FogEnd
        {
            get { return fogEnd; }

            set
            {
                fogEnd = value;
                dirtyFlags |= EffectDirtyFlags.Fog;
            }
        }


        /// <summary>
        /// Gets or sets the fog color.
        /// </summary>
        public Vector3 FogColor
        {
            get { return fogColorParam.GetValueVector3(); }
            set { fogColorParam.SetValue(value); }
        }


        /// <summary>
        /// Gets or sets whether vertex color is enabled.
        /// </summary>
        public bool VertexColorEnabled
        {
            get { return vertexColorEnabled; }

            set
            {
                if (vertexColorEnabled != value)
                {
                    vertexColorEnabled = value;
                    UpdateCurrentTechnique();
                }
            }
        }

        #endregion

        #region Methods

         public TilemapEffect(GraphicsDevice graphicsDevice)
            : base(graphicsDevice, LoadEffectResource(graphicsDevice, ResourceName))
        {    
            CacheEffectParameters(null);
        }

        public TilemapEffect(GraphicsDevice graphicsDevice, byte[] Bytecode): base(graphicsDevice, Bytecode)
        {   
            CacheEffectParameters(null);
        }

        protected TilemapEffect(TilemapEffect cloneSource)
            : base(cloneSource)
        {
            CacheEffectParameters(cloneSource);
            
            fogEnabled = cloneSource.fogEnabled;
            vertexColorEnabled = cloneSource.vertexColorEnabled;

            world = cloneSource.world;
            view = cloneSource.view;
            projection = cloneSource.projection;

            diffuseColor = cloneSource.diffuseColor;

            alpha = cloneSource.alpha;

            fogStart = cloneSource.fogStart;
            fogEnd = cloneSource.fogEnd;

            atlasSize = cloneSource.atlasSize;
        }
        
        public override Effect Clone()
        {
            return new TilemapEffect(this);
        }

        void CacheEffectParameters(TilemapEffect cloneSource)
        {   
            textureParam = Parameters["Texture"];
            textureAtlasParam = Parameters["TextureAtlas"];
            mapSizeParam = Parameters["MapSize"];
            invAtlasSizeParam = Parameters["InvAtlasSize"];
            diffuseColorParam = Parameters["DiffuseColor"];
            fogColorParam = Parameters["FogColor"];
            fogVectorParam = Parameters["FogVector"];
            // IEffectMatrices
            worldViewProjParam = Parameters["WorldViewProj"];
        }


        /// <summary>
        /// Lazily computes derived parameter values immediately before applying the effect.
        /// </summary>
        protected override void OnApply()
        {
            // Recompute the world+view+projection matrix or fog vector?
            dirtyFlags = SetWorldViewProjAndFog(dirtyFlags, ref world, ref view, ref projection, ref worldView, fogEnabled, fogStart, fogEnd, worldViewProjParam, fogVectorParam);

            // Recompute the diffuse/alpha material color parameter?
            if ((dirtyFlags & EffectDirtyFlags.MaterialColor) != 0)
            {
                diffuseColorParam.SetValue(new Vector4(diffuseColor * alpha, alpha));
                dirtyFlags &= ~EffectDirtyFlags.MaterialColor;
            }
        }

        private void UpdateCurrentTechnique()
        {
            int shaderIndex = 0;

            if (!fogEnabled)
                shaderIndex += 1;

            if (vertexColorEnabled)
                shaderIndex += 2;

            CurrentTechnique = Techniques[shaderIndex];
        }


        /// <summary>
        /// Lazily recomputes the world+view+projection matrix and
        /// fog vector based on the current effect parameter settings.
        /// </summary>
        static EffectDirtyFlags SetWorldViewProjAndFog(EffectDirtyFlags dirtyFlags,
                                                                ref Matrix world, ref Matrix view, ref Matrix projection, ref Matrix worldView,
                                                                bool fogEnabled, float fogStart, float fogEnd,
                                                                EffectParameter worldViewProjParam, EffectParameter fogVectorParam)
        {
            // Recompute the world+view+projection matrix?
            if ((dirtyFlags & EffectDirtyFlags.WorldViewProj) != 0)
            {
                Matrix worldViewProj;

                Matrix.Multiply(ref world, ref view, out worldView);
                Matrix.Multiply(ref worldView, ref projection, out worldViewProj);

                worldViewProjParam.SetValue(worldViewProj);

                dirtyFlags &= ~EffectDirtyFlags.WorldViewProj;
            }

            if (fogEnabled)
            {
                // Recompute the fog vector?
                if ((dirtyFlags & (EffectDirtyFlags.Fog | EffectDirtyFlags.FogEnable)) != 0)
                {
                    SetFogVector(ref worldView, fogStart, fogEnd, fogVectorParam);

                    dirtyFlags &= ~(EffectDirtyFlags.Fog | EffectDirtyFlags.FogEnable);
                }
            }
            else
            {
                // When fog is disabled, make sure the fog vector is reset to zero.
                if ((dirtyFlags & EffectDirtyFlags.FogEnable) != 0)
                {
                    fogVectorParam.SetValue(Vector4.Zero);

                    dirtyFlags &= ~EffectDirtyFlags.FogEnable;
                }
            }

            return dirtyFlags;
        }
        
        /// <summary>
        /// Sets a vector which can be dotted with the object space vertex position to compute fog amount.
        /// </summary>
        static void SetFogVector(ref Matrix worldView, float fogStart, float fogEnd, EffectParameter fogVectorParam)
        {
            if (fogStart == fogEnd)
            {
                // Degenerate case: force everything to 100% fogged if start and end are the same.
                fogVectorParam.SetValue(new Vector4(0, 0, 0, 1));
            }
            else
            {
                // We want to transform vertex positions into view space, take the resulting
                // Z value, then scale and offset according to the fog start/end distances.
                // Because we only care about the Z component, the shader can do all this
                // with a single dot product, using only the Z row of the world+view matrix.

                float scale = 1f / (fogStart - fogEnd);

                Vector4 fogVector = new Vector4();

                fogVector.X = worldView.M13 * scale;
                fogVector.Y = worldView.M23 * scale;
                fogVector.Z = worldView.M33 * scale;
                fogVector.W = (worldView.M43 + fogStart) * scale;

                fogVectorParam.SetValue(fogVector);
            }
        }
        
        #endregion


        enum EffectDirtyFlags
        {
            WorldViewProj = 1,
            //World = 2,
            //EyePosition = 4,
            MaterialColor = 8,
            Fog = 16,
            FogEnable = 32,
            //AlphaTest = 64,
            All = -1
        }
    }
}
