using System;
using System.Collections.Generic;
using System.IO;
using Kav.Data;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace Kav
{
    public class Renderer
    {
        private GraphicsDevice GraphicsDevice { get; }

        private VertexBuffer FullscreenTriangle { get; }

        private DeferredPBREffect DeferredPBREffect { get; }
        /* FIXME: these next two dont actually have anything to do with PBR */
        private DeferredPBR_GBufferEffect Deferred_GBufferEffect { get; }
        private DeferredPBR_AmbientLightEffect DeferredAmbientLightEffect { get; }
        private DeferredPBR_PointLightEffect DeferredPointLightEffect { get; }
        private DeferredPBR_DirectionalLightEffect DeferredDirectionalLightEffect { get; }
        private Deferred_ToonEffect Deferred_ToonEffect { get; }
        private SimpleDepthEffect SimpleDepthEffect { get; }
        private SimpleDepthEffectInstanced SimpleDepthEffectInstanced { get; }
        private LinearDepthEffect LinearDepthEffect { get; }
        private LinearDepthEffectInstanced LinearDepthEffectInstanced { get; }
        private Effect ToneMapEffect { get; }
        private SkyboxEffect SkyboxEffect { get; }
        private DiffuseLitSpriteEffect DiffuseLitSpriteEffect { get; }

        private Kav.Model UnitCube { get; }
        private Kav.Model UnitSphere { get; }

        public Renderer(
            GraphicsDevice graphicsDevice
        ) {
            GraphicsDevice = graphicsDevice;

            SimpleDepthEffect = new SimpleDepthEffect(GraphicsDevice);
            SimpleDepthEffectInstanced = new SimpleDepthEffectInstanced(GraphicsDevice);
            LinearDepthEffect = new LinearDepthEffect(GraphicsDevice);
            LinearDepthEffectInstanced = new LinearDepthEffectInstanced(GraphicsDevice);

            DeferredPBREffect = new DeferredPBREffect(GraphicsDevice);

            Deferred_GBufferEffect = new DeferredPBR_GBufferEffect(GraphicsDevice);
            DeferredAmbientLightEffect = new DeferredPBR_AmbientLightEffect(GraphicsDevice);
            DeferredPointLightEffect = new DeferredPBR_PointLightEffect(GraphicsDevice);
            DeferredDirectionalLightEffect = new DeferredPBR_DirectionalLightEffect(GraphicsDevice);
            ToneMapEffect = new Effect(graphicsDevice, Resources.ToneMapEffect);
            Deferred_ToonEffect = new Deferred_ToonEffect(GraphicsDevice);
            SkyboxEffect = new SkyboxEffect(GraphicsDevice);
            DiffuseLitSpriteEffect = new DiffuseLitSpriteEffect(GraphicsDevice);

            FullscreenTriangle = new VertexBuffer(GraphicsDevice, typeof(VertexPositionTexture), 3, BufferUsage.WriteOnly);
            FullscreenTriangle.SetData(new VertexPositionTexture[3] {
                new VertexPositionTexture(new Vector3(-1, -3, 0), new Vector2(0, 2)),
                new VertexPositionTexture(new Vector3(-1, 1, 0), new Vector2(0, 0)),
                new VertexPositionTexture(new Vector3(3, 1, 0), new Vector2(2, 0))
            });

            UnitCube = Kav.ModelLoader.Load(
                GraphicsDevice,
                Smuggler.Importer.ImportGLB(GraphicsDevice, new MemoryStream(Resources.UnitCubeModel))
            );

            UnitSphere = Kav.ModelLoader.Load(
                graphicsDevice,
                Smuggler.Importer.ImportGLB(graphicsDevice, new MemoryStream(Resources.UnitSphereModel))
            );
        }

        public static (T[], DynamicVertexBuffer) CreateInstanceVertexBuffer<T>(
            GraphicsDevice graphicsDevice,
            int instanceVertexCount
        ) where T : IVertexType {
            var positionData = new T[instanceVertexCount];

            var vertexBuffer = new DynamicVertexBuffer(
                graphicsDevice,
                typeof(T),
                instanceVertexCount,
                BufferUsage.WriteOnly
            );

            return (positionData, vertexBuffer);
        }

        public static RenderTargetCube CreateShadowCubeMap(
            GraphicsDevice graphicsDevice,
            int shadowMapSize
        ) {
            return new RenderTargetCube(
                graphicsDevice,
                shadowMapSize,
                false,
                SurfaceFormat.Single,
                DepthFormat.Depth24,
                0,
                RenderTargetUsage.PreserveContents
            );
        }

        public static DirectionalShadowMapData CreateDirectionalShadowMaps(
            GraphicsDevice graphicsDevice,
            int shadowMapSize,
            int numCascades
        ) {
            return new DirectionalShadowMapData(
                graphicsDevice,
                shadowMapSize,
                numCascades
            );
        }

        // TODO: we could make this a lot more efficient probably
        // draws mesh sprites with a forward rendered diffuse lighting technique
        public void MeshSpriteRender(
            RenderTarget2D renderTarget,
            PerspectiveCamera camera,
            IEnumerable<MeshSpriteDrawData> meshSpriteDrawDatas,
            AmbientLight ambientLight,
            IEnumerable<PointLight> pointLights,
            DirectionalLight? directionalLight
        ) {
            GraphicsDevice.SetRenderTarget(renderTarget);

            GraphicsDevice.DepthStencilState = DepthStencilState.Default;
            GraphicsDevice.RasterizerState = RasterizerState.CullNone;
            GraphicsDevice.SamplerStates[0] = SamplerState.PointClamp;
            GraphicsDevice.SamplerStates[1] = SamplerState.PointClamp;
            GraphicsDevice.BlendState = BlendState.AlphaBlend;

            DiffuseLitSpriteEffect.View = camera.View;
            DiffuseLitSpriteEffect.Projection = camera.Projection;

            DiffuseLitSpriteEffect.AmbientColor =
                ambientLight.Color.ToVector3();

            if (directionalLight.HasValue)
            {
                DiffuseLitSpriteEffect.DirectionalLightDirection =
                    directionalLight.Value.Direction;
                DiffuseLitSpriteEffect.DirectionalLightColor =
                    directionalLight.Value.Color.ToVector3() * directionalLight.Value.Intensity;
            }
            else
            {
                DiffuseLitSpriteEffect.DirectionalLightColor = Vector3.Zero;
            }

            var i = 0;
            foreach (var pointLight in pointLights)
            {
                if (i > DiffuseLitSpriteEffect.MaxPointLights) { break; }
                DiffuseLitSpriteEffect.PointLights[i] = pointLight;
                i += 1;
            }

            var boundingFrustum = new BoundingFrustum(camera.View * camera.Projection);

            foreach (var data in meshSpriteDrawDatas)
            {
                var matrix = BillboardTransforms(camera, data.TransformMatrix, data.BillboardConstraint);

                if (FrustumCull(boundingFrustum, data.MeshSprite, matrix))
                {
                    continue;
                }

                DiffuseLitSpriteEffect.NormalMapEnabled = data.Normal != null;

                DiffuseLitSpriteEffect.World = matrix;
                DiffuseLitSpriteEffect.UVOffset = data.UVOffset.Offset;
                DiffuseLitSpriteEffect.SubTextureDimensions = data.UVOffset.Percentage;

                GraphicsDevice.Textures[0] = data.Texture;
                GraphicsDevice.Textures[1] = data.Normal;

                GraphicsDevice.SetVertexBuffer(data.MeshSprite.VertexBuffer);
                GraphicsDevice.Indices = data.MeshSprite.IndexBuffer;

                foreach (var pass in DiffuseLitSpriteEffect.CurrentTechnique.Passes)
                {
                    pass.Apply();

                    GraphicsDevice.DrawIndexedPrimitives(
                        PrimitiveType.TriangleList,
                        0,
                        0,
                        data.MeshSprite.VertexBuffer.VertexCount,
                        0,
                        2
                    );
                }
            }
        }

        public void RenderMeshSpriteGBuffer(
            RenderTargetBinding[] gBuffer,
            PerspectiveCamera camera,
            IEnumerable<MeshSpriteDrawData> meshSpriteDrawDatas
        ) {
            GraphicsDevice.SetRenderTargets(gBuffer);
            GraphicsDevice.RasterizerState = RasterizerState.CullNone;
            GraphicsDevice.DepthStencilState = DepthStencilState.Default;
            GraphicsDevice.BlendState = BlendState.AlphaBlend;

            Deferred_GBufferEffect.HardwareInstancingEnabled = false;
            Deferred_GBufferEffect.View = camera.View;
            Deferred_GBufferEffect.Projection = camera.Projection;

            var boundingFrustum = new BoundingFrustum(camera.View * camera.Projection);

            foreach (var data in meshSpriteDrawDatas)
            {
                var matrix = BillboardTransforms(camera, data.TransformMatrix, data.BillboardConstraint);

                if (FrustumCull(boundingFrustum, data.MeshSprite, matrix))
                {
                    continue;
                }

                Deferred_GBufferEffect.World = matrix;

                Deferred_GBufferEffect.UVOffset = data.UVOffset.Offset;
                Deferred_GBufferEffect.SubTextureDimensions = data.UVOffset.Percentage;

                Deferred_GBufferEffect.Albedo = Color.White.ToVector3();
                Deferred_GBufferEffect.Metallic = 0f;
                Deferred_GBufferEffect.Roughness = 1f;

                Deferred_GBufferEffect.AlbedoTexture = data.Texture;
                Deferred_GBufferEffect.NormalTexture = data.Normal;
                Deferred_GBufferEffect.MetallicRoughnessTexture = null;

                RenderIndexed(GraphicsDevice, data, Deferred_GBufferEffect);
            }
        }

        private static Matrix BillboardTransforms(
            PerspectiveCamera camera,
            Matrix transform,
            SpriteBillboardConstraint billboardConstraint
        ) {
            if (billboardConstraint == SpriteBillboardConstraint.None)
            {
                return transform;
            }
            else if (billboardConstraint == SpriteBillboardConstraint.Horizontal)
            {
                return Matrix.CreateConstrainedBillboard(
                    transform.Translation,
                    camera.Position,
                    Vector3.Up,
                    camera.Forward,
                    camera.Position - transform.Translation
                );
            }
            else
            {
                return Matrix.CreateConstrainedBillboard(
                    transform.Translation,
                    camera.Position,
                    Vector3.Up,
                    null,
                    null
                );
            }
        }

        // Renders a series of drawable-transform pairs using an effect that has a World matrix.
        // Effect must be pre-configured!!
        public static void CullAndRenderIndexed<T, U>(
            GraphicsDevice graphicsDevice,
            BoundingFrustum boundingFrustum,
            IEnumerable<(T, Matrix)> drawableTransformPairs,
            U effect
        ) where T : IIndexDrawable, ICullable where U : Effect, IHasWorldMatrix
        {
            foreach (var (drawable, transform) in FrustumCull(boundingFrustum, drawableTransformPairs))
            {
                effect.World = transform;

                RenderIndexed(
                    graphicsDevice,
                    drawable,
                    effect
                );
            }
        }

        public static void RenderIndexed<T, U>(
            GraphicsDevice graphicsDevice,
            T drawable,
            U effect
        ) where T : IIndexDrawable where U : Effect
        {
            graphicsDevice.SetVertexBuffer(drawable.VertexBuffer);
            graphicsDevice.Indices = drawable.IndexBuffer;

            foreach (var pass in effect.CurrentTechnique.Passes)
            {
                pass.Apply();

                graphicsDevice.DrawIndexedPrimitives(
                    PrimitiveType.TriangleList,
                    0,
                    0,
                    drawable.VertexBuffer.VertexCount,
                    0,
                    drawable.IndexBuffer.IndexCount / 3
                );
            }
        }

        public static void RenderInstanced<T>(
            GraphicsDevice graphicsDevice,
            T drawable,
            VertexBuffer instanceVertexBuffer,
            int numInstances,
            Effect effect
        ) where T : IIndexDrawable {
            graphicsDevice.SetVertexBuffers(
                drawable.VertexBuffer,
                new VertexBufferBinding(instanceVertexBuffer, 0, 1)
            );
            graphicsDevice.Indices = drawable.IndexBuffer;

            foreach (var pass in effect.CurrentTechnique.Passes)
            {
                pass.Apply();

                graphicsDevice.DrawInstancedPrimitives(
                    PrimitiveType.TriangleList,
                    0,
                    0,
                    drawable.VertexBuffer.VertexCount,
                    0,
                    drawable.IndexBuffer.IndexCount / 3,
                    numInstances
                );
            }
        }

        // TODO: can probably make this static somehow
        public void RenderFullscreenEffect(
            Effect effect
        ) {
            foreach (var pass in effect.CurrentTechnique.Passes)
            {
                pass.Apply();
                GraphicsDevice.SetVertexBuffer(FullscreenTriangle);
                GraphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, 1);
            }
        }

        public void RenderDepthIndexed<T>(
            RenderTarget2D renderTarget,
            PerspectiveCamera camera,
            IEnumerable<(T, Matrix)> drawableTransforms
        ) where T : ICullable, IIndexDrawable
        {
            GraphicsDevice.SetRenderTarget(renderTarget);
            GraphicsDevice.DepthStencilState = DepthStencilState.Default;

            SimpleDepthEffect.View = camera.View;
            SimpleDepthEffect.Projection = camera.Projection;

            CullAndRenderIndexed(
                GraphicsDevice,
                new BoundingFrustum(camera.View * camera.Projection),
                drawableTransforms,
                SimpleDepthEffect
            );
        }

        public void RenderSkybox(
            RenderTarget2D renderTarget,
            PerspectiveCamera camera,
            TextureCube skybox
        ) {
            GraphicsDevice.SetRenderTarget(renderTarget);
            GraphicsDevice.RasterizerState.CullMode = CullMode.CullClockwiseFace;
            GraphicsDevice.DepthStencilState = DepthStencilState.DepthRead;

            SkyboxEffect.Skybox = skybox;

            var view = camera.View;
            view.Translation = Vector3.Zero;

            SkyboxEffect.View = view;
            SkyboxEffect.Projection = camera.Projection;

            RenderIndexed(
                GraphicsDevice,
                UnitCube.Meshes[0].MeshParts[0],
                SkyboxEffect
            );

            GraphicsDevice.RasterizerState.CullMode = CullMode.CullCounterClockwiseFace;
        }

        /// <summary>
        /// GBuffer binding must have 4 render targets.
        /// Assumes that vertex buffer has been filled.
        /// </summary>
        public void RenderGBufferInstanced<T>(
            RenderTargetBinding[] gBuffer,
            RenderTarget2D depthBuffer,
            PerspectiveCamera camera,
            T drawable,
            VertexBuffer instanceVertexBuffer,
            int numInstances
        ) where T : IGBufferDrawable, IIndexDrawable {
            GraphicsDevice.SetRenderTargets(gBuffer);
            GraphicsDevice.DepthStencilState = DepthStencilState.Default;
            GraphicsDevice.BlendState = BlendState.Opaque;

            Deferred_GBufferEffect.HardwareInstancingEnabled = true;

            Deferred_GBufferEffect.Albedo = drawable.Albedo;
            Deferred_GBufferEffect.Metallic = drawable.Metallic;
            Deferred_GBufferEffect.Roughness = drawable.Roughness;

            Deferred_GBufferEffect.NumTextureRows = drawable.NumTextureRows;
            Deferred_GBufferEffect.NumTextureColumns = drawable.NumTextureColumns;

            Deferred_GBufferEffect.AlbedoTexture = drawable.AlbedoTexture;
            Deferred_GBufferEffect.NormalTexture = drawable.NormalTexture;
            Deferred_GBufferEffect.MetallicRoughnessTexture = drawable.MetallicRoughnessTexture;

            Deferred_GBufferEffect.View = camera.View;
            Deferred_GBufferEffect.Projection = camera.Projection;

            RenderInstanced(
                GraphicsDevice,
                drawable,
                instanceVertexBuffer,
                numInstances,
                Deferred_GBufferEffect
            );

            // re-render to get depth
            GraphicsDevice.SetRenderTargets(depthBuffer);

            SimpleDepthEffectInstanced.View = camera.View;
            SimpleDepthEffectInstanced.Projection = camera.Projection;

            RenderInstanced(
                GraphicsDevice,
                drawable,
                instanceVertexBuffer,
                numInstances,
                SimpleDepthEffectInstanced
            );
        }

        public void RenderGBufferIndexed<T>(
            RenderTargetBinding[] gBuffer,
            PerspectiveCamera camera,
            IEnumerable<(T, Matrix)> drawableTransforms
        ) where T : ICullable, IIndexDrawable, IGBufferDrawable {
            GraphicsDevice.SetRenderTargets(gBuffer);
            GraphicsDevice.DepthStencilState = DepthStencilState.Default;
            GraphicsDevice.BlendState = BlendState.Opaque;

            Deferred_GBufferEffect.HardwareInstancingEnabled = false;
            Deferred_GBufferEffect.View = camera.View;
            Deferred_GBufferEffect.Projection = camera.Projection;

            var boundingFrustum = new BoundingFrustum(camera.View * camera.Projection);

            foreach (var (drawable, transform) in FrustumCull(boundingFrustum, drawableTransforms))
            {
                Deferred_GBufferEffect.World = transform;

                Deferred_GBufferEffect.HardwareInstancingEnabled = false;

                Deferred_GBufferEffect.Albedo = drawable.Albedo;
                Deferred_GBufferEffect.Metallic = drawable.Metallic;
                Deferred_GBufferEffect.Roughness = drawable.Roughness;

                Deferred_GBufferEffect.AlbedoTexture = drawable.AlbedoTexture;
                Deferred_GBufferEffect.NormalTexture = drawable.NormalTexture;
                Deferred_GBufferEffect.MetallicRoughnessTexture = drawable.MetallicRoughnessTexture;

                RenderIndexed(GraphicsDevice, drawable, Deferred_GBufferEffect);
            }
        }

        public void RenderAmbientLight(
            RenderTarget2D renderTarget,
            Texture2D gPosition,
            Texture2D gAlbedo,
            AmbientLight ambientLight
        ) {
            GraphicsDevice.SetRenderTarget(renderTarget);
            GraphicsDevice.BlendState = BlendState.Opaque;
            GraphicsDevice.DepthStencilState = DepthStencilState.DepthRead;

            DeferredAmbientLightEffect.GPosition = gPosition;
            DeferredAmbientLightEffect.GAlbedo = gAlbedo;
            DeferredAmbientLightEffect.AmbientColor = ambientLight.Color.ToVector3();

            RenderFullscreenEffect(DeferredAmbientLightEffect);
        }

        public void RenderPointLight(
            RenderTarget2D renderTarget,
            Texture2D gPosition,
            Texture2D gAlbedo,
            Texture2D gNormal,
            Texture2D gMetallicRoughness,
            TextureCube shadowMap,
            PerspectiveCamera camera,
            PointLight pointLight
        ) {
            GraphicsDevice.SetRenderTarget(renderTarget);
            GraphicsDevice.RasterizerState = RasterizerState.CullClockwise;
            GraphicsDevice.DepthStencilState = DepthStencilState.None;
            GraphicsDevice.BlendState = BlendState.Additive;

            DeferredPointLightEffect.GPosition = gPosition;
            DeferredPointLightEffect.GAlbedo = gAlbedo;
            DeferredPointLightEffect.GNormal = gNormal;
            DeferredPointLightEffect.GMetallicRoughness = gMetallicRoughness;
            DeferredPointLightEffect.ShadowMap = shadowMap;

            DeferredPointLightEffect.EyePosition = camera.Position;

            DeferredPointLightEffect.PointLightPosition = pointLight.Position;
            DeferredPointLightEffect.PointLightColor =
                pointLight.Color.ToVector3() * pointLight.Radius;

            DeferredPointLightEffect.FarPlane = 25f; // FIXME: magic value

            DeferredPointLightEffect.World =
                Matrix.CreateScale(pointLight.Radius) *
                Matrix.CreateTranslation(pointLight.Position);
            DeferredPointLightEffect.View = camera.View;
            DeferredPointLightEffect.Projection = camera.Projection;

            RenderIndexed(
                GraphicsDevice,
                UnitSphere.Meshes[0].MeshParts[0],
                DeferredPointLightEffect
            );
        }

        public void RenderDirectionalLight(
            RenderTarget2D renderTarget,
            Texture2D gPosition,
            Texture2D gAlbedo,
            Texture2D gNormal,
            Texture2D gMetallicRoughness,
            DirectionalShadowMapData shadowMapData,
            PerspectiveCamera camera,
            DirectionalLight directionalLight
        ) {
            GraphicsDevice.SetRenderTarget(renderTarget);
            GraphicsDevice.RasterizerState = RasterizerState.CullCounterClockwise;
            GraphicsDevice.DepthStencilState = DepthStencilState.DepthRead;
            GraphicsDevice.BlendState = BlendState.Additive;

            DeferredDirectionalLightEffect.GPosition = gPosition;
            DeferredDirectionalLightEffect.GAlbedo = gAlbedo;
            DeferredDirectionalLightEffect.GNormal = gNormal;
            DeferredDirectionalLightEffect.GMetallicRoughness = gMetallicRoughness;

            DeferredDirectionalLightEffect.ShadowMapSize = shadowMapData.ShadowMapSize;

            DeferredDirectionalLightEffect.ShadowMapOne = shadowMapData.ShadowMaps[0];
            DeferredDirectionalLightEffect.LightSpaceMatrixOne = shadowMapData.LightSpaceViews[0] * shadowMapData.LightSpaceProjections[0];
            DeferredDirectionalLightEffect.CascadeFarPlanes[0] = shadowMapData.CascadeFarPlanes[0];

            if (shadowMapData.NumShadowCascades > 1)
            {
                DeferredDirectionalLightEffect.ShadowMapTwo = shadowMapData.ShadowMaps[1];
                DeferredDirectionalLightEffect.LightSpaceMatrixTwo = shadowMapData.LightSpaceViews[1] * shadowMapData.LightSpaceProjections[1];
                DeferredDirectionalLightEffect.CascadeFarPlanes[1] = shadowMapData.CascadeFarPlanes[1];
            }
            if (shadowMapData.NumShadowCascades > 2)
            {
                DeferredDirectionalLightEffect.ShadowMapThree = shadowMapData.ShadowMaps[2];
                DeferredDirectionalLightEffect.LightSpaceMatrixThree = shadowMapData.LightSpaceViews[2] * shadowMapData.LightSpaceProjections[2];
                DeferredDirectionalLightEffect.CascadeFarPlanes[2] = shadowMapData.CascadeFarPlanes[2];
            }
            if (shadowMapData.NumShadowCascades > 3)
            {
                DeferredDirectionalLightEffect.ShadowMapFour = shadowMapData.ShadowMaps[3];
                DeferredDirectionalLightEffect.LightSpaceMatrixFour = shadowMapData.LightSpaceViews[3] * shadowMapData.LightSpaceProjections[3];
                DeferredDirectionalLightEffect.CascadeFarPlanes[3] = shadowMapData.CascadeFarPlanes[3];
            }

            DeferredDirectionalLightEffect.DirectionalLightDirection = directionalLight.Direction;
            DeferredDirectionalLightEffect.DirectionalLightColor =
                directionalLight.Color.ToVector3() * directionalLight.Intensity;

            DeferredDirectionalLightEffect.ViewMatrix = camera.View;
            DeferredDirectionalLightEffect.EyePosition = camera.Position;

            RenderFullscreenEffect(DeferredDirectionalLightEffect);
        }

        public void RenderDirectionalLightToon(
            RenderTarget2D renderTarget,
            Texture2D gPosition,
            Texture2D gAlbedo,
            Texture2D gNormal,
            Texture2D gMetallicRoughness,
            DirectionalShadowMapData shadowMapData,
            PerspectiveCamera camera,
            DirectionalLight directionalLight,
            bool ditheredShadows
        ) {
            GraphicsDevice.SetRenderTarget(renderTarget);
            GraphicsDevice.DepthStencilState = DepthStencilState.DepthRead;
            GraphicsDevice.BlendState = BlendState.Additive;

            Deferred_ToonEffect.GPosition = gPosition;
            Deferred_ToonEffect.GAlbedo = gAlbedo;
            Deferred_ToonEffect.GNormal = gNormal;
            Deferred_ToonEffect.GMetallicRoughness = gMetallicRoughness;

            Deferred_ToonEffect.DitheredShadows = ditheredShadows;

            Deferred_ToonEffect.EyePosition = camera.Position;

            Deferred_ToonEffect.DirectionalLightDirection = directionalLight.Direction;
            Deferred_ToonEffect.DirectionalLightColor =
                directionalLight.Color.ToVector3() * directionalLight.Intensity;

            Deferred_ToonEffect.ShadowMapOne = shadowMapData.ShadowMaps[0];
            Deferred_ToonEffect.LightSpaceMatrixOne = shadowMapData.LightSpaceViews[0] * shadowMapData.LightSpaceProjections[0];

            if (shadowMapData.NumShadowCascades > 1)
            {
                Deferred_ToonEffect.ShadowMapTwo = shadowMapData.ShadowMaps[1];
                Deferred_ToonEffect.LightSpaceMatrixTwo = shadowMapData.LightSpaceViews[1] * shadowMapData.LightSpaceProjections[1];
            }
            if (shadowMapData.NumShadowCascades > 2)
            {
                Deferred_ToonEffect.ShadowMapThree = shadowMapData.ShadowMaps[2];
                Deferred_ToonEffect.LightSpaceMatrixThree = shadowMapData.LightSpaceViews[1] * shadowMapData.LightSpaceProjections[2];
            }
            if (shadowMapData.NumShadowCascades > 3)
            {
                Deferred_ToonEffect.ShadowMapFour = shadowMapData.ShadowMaps[3];
                Deferred_ToonEffect.LightSpaceMatrixFour = shadowMapData.LightSpaceViews[2] * shadowMapData.LightSpaceProjections[3];
            }

            Deferred_ToonEffect.ViewMatrix = camera.View;

            RenderFullscreenEffect(Deferred_ToonEffect);
        }

        public void PrepareDirectionalShadowData(
            DirectionalShadowMapData shadowMapData,
            PerspectiveCamera camera,
            DirectionalLight directionalLight
        ) {
            var previousFarPlane = camera.NearPlane;
            for (var i = 0; i < shadowMapData.NumShadowCascades; i++)
            {
                var farPlane = camera.FarPlane / (MathHelper.Max((shadowMapData.NumShadowCascades - i - 1) * 2f, 1f));

                // divide the view frustum
                var shadowCamera = new PerspectiveCamera(
                    camera.Position,
                    camera.Forward,
                    camera.Up,
                    camera.FieldOfView,
                    camera.AspectRatio,
                    previousFarPlane,
                    farPlane
                );

                PrepareDirectionalShadowCascade(
                    shadowMapData,
                    i,
                    shadowCamera,
                    directionalLight
                );

                shadowMapData.CascadeFarPlanes[i] = farPlane;
                previousFarPlane = farPlane;
            }
        }

        private void PrepareDirectionalShadowCascade(
            DirectionalShadowMapData shadowMapData,
            int shadowCascadeIndex,
            PerspectiveCamera shadowCamera,
            DirectionalLight directionalLight
        ) {
            var cameraBoundingFrustum = new BoundingFrustum(shadowCamera.View * shadowCamera.Projection);
            Vector3[] frustumCorners = cameraBoundingFrustum.GetCorners();

            Vector3 frustumCenter = Vector3.Zero;
            for (var i = 0; i < frustumCorners.Length; i++)
            {
                frustumCenter += frustumCorners[i];
            }
            frustumCenter /= 8f;

            var lightView = Matrix.CreateLookAt(frustumCenter + directionalLight.Direction, frustumCenter, Vector3.Backward);

            for (var i = 0; i < frustumCorners.Length; i++)
            {
                frustumCorners[i] = Vector3.Transform(frustumCorners[i], lightView);
            }

            BoundingBox lightBox = BoundingBox.CreateFromPoints(frustumCorners);

            shadowMapData.LightSpaceViews[shadowCascadeIndex] = lightView;
            shadowMapData.LightSpaceProjections[shadowCascadeIndex] = Matrix.CreateOrthographicOffCenter(
                lightBox.Min.X,
                lightBox.Max.X,
                lightBox.Min.Y,
                lightBox.Max.Y,
                -lightBox.Max.Z - 10f, // TODO: near clip plane needs scene AABB info to get rid of this magic value
                -lightBox.Min.Z
            );
        }

        public void RenderDirectionalShadowsIndexed<T>(
            DirectionalShadowMapData shadowMapData,
            IEnumerable<(T, Matrix)> drawableTransforms
        ) where T : ICullable, IIndexDrawable {
            // render the individual shadow cascades
            for (var i = 0; i < shadowMapData.NumShadowCascades; i++)
            {
                RenderDirectionalShadowMapIndexed(
                    shadowMapData,
                    i,
                    drawableTransforms
                );
            }
        }

        private void RenderDirectionalShadowMapIndexed<T>(
            DirectionalShadowMapData shadowMapData,
            int shadowCascadeIndex,
            IEnumerable<(T, Matrix)> drawableTransforms
        ) where T : ICullable, IIndexDrawable {
            GraphicsDevice.SetRenderTarget(shadowMapData.ShadowMaps[shadowCascadeIndex]);
            GraphicsDevice.DepthStencilState = DepthStencilState.Default;
            GraphicsDevice.BlendState = BlendState.Opaque;

            SimpleDepthEffect.View = shadowMapData.LightSpaceViews[shadowCascadeIndex];
            SimpleDepthEffect.Projection = shadowMapData.LightSpaceProjections[shadowCascadeIndex];

            CullAndRenderIndexed(
                GraphicsDevice,
                new BoundingFrustum(SimpleDepthEffect.View * SimpleDepthEffect.Projection),
                drawableTransforms,
                SimpleDepthEffect
            );
        }

        public void RenderDirectionalShadowsInstanced<T>(
            DirectionalShadowMapData shadowMapData,
            T drawable,
            VertexBuffer instanceVertexBuffer,
            int numInstances
        ) where T : IIndexDrawable
        {
            // render the individual shadow cascades
            for (var i = 0; i < shadowMapData.NumShadowCascades; i++)
            {
                RenderDirectionalShadowMapInstanced(
                    shadowMapData,
                    i,
                    drawable,
                    instanceVertexBuffer,
                    numInstances
                );
            }
        }

        private void RenderDirectionalShadowMapInstanced<T>(
            DirectionalShadowMapData shadowMapData,
            int shadowCascadeIndex,
            T drawable,
            VertexBuffer instanceVertexBuffer,
            int numInstances
        ) where T : IIndexDrawable
        {
            GraphicsDevice.SetRenderTarget(shadowMapData.ShadowMaps[shadowCascadeIndex]);
            GraphicsDevice.DepthStencilState = DepthStencilState.Default;
            GraphicsDevice.BlendState = BlendState.Opaque;

            SimpleDepthEffectInstanced.View = shadowMapData.LightSpaceViews[shadowCascadeIndex];
            SimpleDepthEffectInstanced.Projection = shadowMapData.LightSpaceProjections[shadowCascadeIndex];

            RenderInstanced(
                GraphicsDevice,
                drawable,
                instanceVertexBuffer,
                numInstances,
                SimpleDepthEffectInstanced
            );
        }

        public void RenderPointShadowMapIndexed<T>(
            RenderTargetCube pointShadowCubeMap,
            IEnumerable<(T, Matrix)> modelTransforms,
            PointLight pointLight
        ) where T : ICullable, IIndexDrawable {
            GraphicsDevice.DepthStencilState = DepthStencilState.Default;
            GraphicsDevice.BlendState = BlendState.Opaque;

            foreach (CubeMapFace face in Enum.GetValues(typeof(CubeMapFace)))
            {
                GraphicsDevice.SetRenderTarget(pointShadowCubeMap, face);

                Vector3 targetDirection;
                Vector3 targetUpDirection;
                switch(face)
                {
                    case CubeMapFace.PositiveX:
                        targetDirection = Vector3.Right;
                        targetUpDirection = Vector3.Up;
                        break;

                    case CubeMapFace.NegativeX:
                        targetDirection = Vector3.Left;
                        targetUpDirection = Vector3.Up;
                        break;

                    case CubeMapFace.PositiveY:
                        targetDirection = Vector3.Up;
                        targetUpDirection = Vector3.Forward;
                        break;

                    case CubeMapFace.NegativeY:
                        targetDirection = Vector3.Down;
                        targetUpDirection = Vector3.Backward;
                        break;

                    case CubeMapFace.PositiveZ:
                        targetDirection = Vector3.Backward;
                        targetUpDirection = Vector3.Up;
                        break;

                    case CubeMapFace.NegativeZ:
                        targetDirection = Vector3.Forward;
                        targetUpDirection = Vector3.Up;
                        break;

                    default:
                        targetDirection = Vector3.Right;
                        targetUpDirection = Vector3.Up;
                        break;
                }

                LinearDepthEffect.View = Matrix.CreateLookAt(
                    pointLight.Position,
                    pointLight.Position + targetDirection,
                    targetUpDirection
                );
                LinearDepthEffect.Projection = Matrix.CreatePerspectiveFieldOfView(
                    MathHelper.PiOver2,
                    1,
                    0.1f,
                    25f // FIXME: magic value
                );
                LinearDepthEffect.FarPlane = 25f;

                LinearDepthEffect.LightPosition = pointLight.Position;

                CullAndRenderIndexed(
                    GraphicsDevice,
                    new BoundingFrustum(LinearDepthEffect.View * LinearDepthEffect.Projection),
                    modelTransforms,
                    LinearDepthEffect
                );
            }
        }

        public void RenderPointShadowMapInstanced<T>(
            RenderTargetCube pointShadowCubeMap,
            T drawable,
            VertexBuffer instanceVertexBuffer,
            int numInstances,
            PointLight pointLight
        ) where T : ICullable, IIndexDrawable
        {
            GraphicsDevice.DepthStencilState = DepthStencilState.Default;
            GraphicsDevice.BlendState = BlendState.Opaque;

            foreach (CubeMapFace face in Enum.GetValues(typeof(CubeMapFace)))
            {
                GraphicsDevice.SetRenderTarget(pointShadowCubeMap, face);

                Vector3 targetDirection;
                Vector3 targetUpDirection;
                switch(face)
                {
                    case CubeMapFace.PositiveX:
                        targetDirection = Vector3.Right;
                        targetUpDirection = Vector3.Up;
                        break;

                    case CubeMapFace.NegativeX:
                        targetDirection = Vector3.Left;
                        targetUpDirection = Vector3.Up;
                        break;

                    case CubeMapFace.PositiveY:
                        targetDirection = Vector3.Up;
                        targetUpDirection = Vector3.Forward;
                        break;

                    case CubeMapFace.NegativeY:
                        targetDirection = Vector3.Down;
                        targetUpDirection = Vector3.Backward;
                        break;

                    case CubeMapFace.PositiveZ:
                        targetDirection = Vector3.Backward;
                        targetUpDirection = Vector3.Up;
                        break;

                    case CubeMapFace.NegativeZ:
                        targetDirection = Vector3.Forward;
                        targetUpDirection = Vector3.Up;
                        break;

                    default:
                        targetDirection = Vector3.Right;
                        targetUpDirection = Vector3.Up;
                        break;
                }

                LinearDepthEffectInstanced.View = Matrix.CreateLookAt(
                    pointLight.Position,
                    pointLight.Position + targetDirection,
                    targetUpDirection
                );
                LinearDepthEffectInstanced.Projection = Matrix.CreatePerspectiveFieldOfView(
                    MathHelper.PiOver2,
                    1,
                    0.1f,
                    25f // FIXME: magic value
                );

                LinearDepthEffectInstanced.FarPlane = 25f;
                LinearDepthEffectInstanced.LightPosition = pointLight.Position;

                RenderInstanced(
                    GraphicsDevice,
                    drawable,
                    instanceVertexBuffer,
                    numInstances,
                    LinearDepthEffectInstanced
                );
            }
        }

        private static IEnumerable<(T, Matrix)> FrustumCull<T>(
            BoundingFrustum boundingFrustum,
            IEnumerable<(T, Matrix)> cullableTransforms
        ) where T : ICullable {
            foreach (var (cullable, transform) in cullableTransforms)
            {
                var boundingBox = TransformedBoundingBox(cullable.BoundingBox, transform);
                var containment = boundingFrustum.Contains(boundingBox);
                if (containment != ContainmentType.Disjoint)
                {
                    yield return (cullable, transform);
                }
            }
        }

        private static bool FrustumCull<T>(
            BoundingFrustum boundingFrustum,
            T cullable,
            Matrix transform
        ) where T : ICullable {
            var boundingBox = TransformedBoundingBox(cullable.BoundingBox, transform);
            var containment = boundingFrustum.Contains(boundingBox);
            return (containment == ContainmentType.Disjoint);
        }

        private static IEnumerable<T> FrustumCull<T>(
            BoundingFrustum boundingFrustum,
            IEnumerable<T> cullableTransformables
        ) where T : ICullable, ITransformable {
            foreach (var cullableTransformable in cullableTransformables)
            {
                var boundingBox = TransformedBoundingBox(cullableTransformable.BoundingBox, cullableTransformable.TransformMatrix);
                var containment = boundingFrustum.Contains(boundingBox);
                if (containment != ContainmentType.Disjoint)
                {
                    yield return cullableTransformable;
                }
            }
        }

        private static BoundingBox TransformedBoundingBox(BoundingBox boundingBox, Matrix matrix)
        {
            var center = (boundingBox.Min + boundingBox.Max) / 2f;
            var extent = (boundingBox.Max - boundingBox.Min) / 2f;

            var newCenter = Vector3.Transform(center, matrix);
            var newExtent = Vector3.TransformNormal(extent, AbsoluteMatrix(matrix));

            return new BoundingBox(newCenter - newExtent, newCenter + newExtent);
        }

        private static Matrix AbsoluteMatrix(Matrix matrix)
        {
            return new Matrix(
                Math.Abs(matrix.M11), Math.Abs(matrix.M12), Math.Abs(matrix.M13), Math.Abs(matrix.M14),
                Math.Abs(matrix.M21), Math.Abs(matrix.M22), Math.Abs(matrix.M23), Math.Abs(matrix.M24),
                Math.Abs(matrix.M31), Math.Abs(matrix.M32), Math.Abs(matrix.M33), Math.Abs(matrix.M34),
                Math.Abs(matrix.M41), Math.Abs(matrix.M42), Math.Abs(matrix.M43), Math.Abs(matrix.M44)
            );
        }
    }
}