using Direct3D = Microsoft.DirectX.Direct3D;

using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System;
using ComponentFramework.Modules;
using ComponentFramework;
using MTV3D65;

namespace OctreeRendering
{
    [AutoLoad]
    class WorldRenderer : Component
    {
        readonly Dictionary<TileType, InstancedMesh> meshes = new Dictionary<TileType, InstancedMesh>(TileTypeComparer.Default);

        TVCamera camera;
        TVRenderSurface occlusionSurface;

        bool cullingNecessary;

        int maxOcclusionQueries;
        int occlusionQueriesPerformed;
        int frustumCulledInstances;
        int synchronousQueries;

        public WorldRenderer(ICore core) : base(core)
        {
            Device = new Direct3D.Device(InternalObjects.GetDevice3D());
        }

        public override void Initialize()
        {
            camera = CameraState.Camera;

            InitializeMeshes();
            InitializeCulling();

            WorldState.WorldRebuilt += InitializeOcclusionQueries;
            WorldState.WorldRebuilt += InitializeMeshes;

            SampleState.OctreeCullingMode.Changed += InitializeCulling;

            SampleState.OctreeTraversalCoarseness.Changed += InitializeOcclusionQueries;
            SampleState.OcclusionSurfaceFactor.Changed += InitializeOcclusionSurface;

            CameraState.ViewChanged += () => { cullingNecessary = true; };
        }

        void InitializeMeshes()
        {
            foreach (var mesh in meshes.Values)
                Core.UnloadComponent(mesh);
            meshes.Clear();

            foreach (TileType type in Enum.GetValues(typeof(TileType)))
            {
                var mesh = Core.CreateAndLoadComponent<InstancedMesh>();

                mesh.Texture = TextureFactory.LoadCubeTexture(type + ".dds");

                var shader = Scene.CreateShader();
                shader.CreateFromEffectFile("TileInstancing.fx");
                shader.SetPassEnable(0, false);
                shader.SetPassEnable(1, true);
                mesh.Shader = shader;

                mesh.CreateInstances(
                    new[] {
                    new PositionInstance(new Vector3f(-0.5f, -0.5f, -0.5f)),
                    new PositionInstance(new Vector3f(0.5f, -0.5f, -0.5f)),
                    new PositionInstance(new Vector3f(0.5f, 0.5f, -0.5f)),
                    new PositionInstance(new Vector3f(-0.5f, 0.5f, -0.5f)),
                    new PositionInstance(new Vector3f(-0.5f, -0.5f, 0.5f)),
                    new PositionInstance(new Vector3f(0.5f, -0.5f, 0.5f)),
                    new PositionInstance(new Vector3f(0.5f, 0.5f, 0.5f)),
                    new PositionInstance(new Vector3f(-0.5f, 0.5f, 0.5f))
                }, new short[] {
                    0, 2, 1,  0, 3, 2,
                    1, 6, 5,  1, 2, 6,
                    0, 4, 7,  0, 7, 3,
                    3, 6, 2,  3, 7, 6,
                    4, 5, 6,  4, 6, 7,
                    0, 1, 5,  0, 5, 4
                }, Direct3D.PrimitiveType.TriangleList, WorldState.TotalInstances);

                meshes.Add(type, mesh);
            }
        }

        void InitializeCulling()
        {
            if (SampleState.OctreeCullingMode == CullingMode.None)
            {
                foreach (var mesh in meshes.Values)
                    mesh.ResetBatch();

                var halfOctreeSize = WorldState.Octree.Size / 2.0f;
                AddTiles(WorldState.Octree.Root, new Vector3f(halfOctreeSize), halfOctreeSize);
            }
            else
            {
                cullingNecessary = true;
                InitializeOcclusion();
            }
        }

        void InitializeOcclusion()
        {
            InitializeOcclusionSurface();
            InitializeOcclusionQueries();
        }

        void InitializeOcclusionSurface()
        {
            if (occlusionSurface != null)
                occlusionSurface.Destroy();

            if (SampleState.OctreeCullingMode.Value.UsesOcclusionQueries())
            {
                occlusionSurface = Scene.CreateRenderSurfaceEx(-1, -1, CONST_TV_RENDERSURFACEFORMAT.TV_TEXTUREFORMAT_R5G6B5,
                                                               true, true, 1.0f / SampleState.OcclusionSurfaceFactor);
                occlusionSurface.SetNewCamera(camera);
            }
        }

        void InitializeOcclusionQueries()
        {
            if (!SampleState.OctreeCullingMode.Value.UsesOcclusionQueries() || WorldState.Octree.Size == 0)
                camera.OccQuery_Init(0);
            else
            {
                var maxLevels = (int)Math.Log(WorldState.Octree.Size, 2) - SampleState.OctreeTraversalCoarseness;
                maxOcclusionQueries = 0;
                AssignQueryId(WorldState.Octree.Root, 0, maxLevels);
                camera.OccQuery_Init(maxOcclusionQueries);
            }
        }

        void AssignQueryId(Octree<TileInfo>.Node node, int level, int maxLevels)
        {
            node.Id = maxOcclusionQueries++;
            node.QueryPerformed = false;

            if (level == maxLevels || node.Leaf) return;
            foreach (var childNode in node.Children)
                if (childNode != null)
                    AssignQueryId(childNode, level + 1, maxLevels);
        }

        void CullOctree()
        {
            switch (SampleState.OctreeCullingMode.Value)
            {
                case CullingMode.None:
                    return;
                case CullingMode.FrustumBoxIntersection:
                    DoFrustumCulling();
                    break;
                case CullingMode.HardwareOcclusionQueries:
                    DoOcclusion();
                    break;
                case CullingMode.FurstumAndOcclusion:
                    onlyFlagNodes = true;
                    DoFrustumCulling();
                    onlyFlagNodes = false;
                    DoOcclusion();
                    break;
            }
        }

        bool onlyFlagNodes;
        void DoFrustumCulling()
        {
            var halfOctreeSize = WorldState.Octree.Size / 2.0f;
            var maxLevels = (int)Math.Log(WorldState.Octree.Size, 2) - SampleState.OctreeTraversalCoarseness;
            frustumCulledInstances = 0;

            if (!onlyFlagNodes)
                foreach (var mesh in meshes.Values)
                    mesh.ResetBatch();

            CullNode(WorldState.Octree.Root, 0, maxLevels, new Vector3f(halfOctreeSize), halfOctreeSize);
        }

        void CullNode(Octree<TileInfo>.Node node, int level, int maxLevels, Vector3f center, float size)
        {
            var boundingBox = new BoundingBox(center - new Vector3f(size), center + new Vector3f(size));
            ContainmentType containmentType = CameraState.Frustum.Contains(boundingBox);

            if (containmentType == ContainmentType.Disjoint)
            {
                SetContainmentType(node, level, maxLevels, ContainmentType.Disjoint);
                return;
            }

            if (level == maxLevels || node.Leaf || (containmentType == ContainmentType.Contains && node.Filled))
            {
                SetContainmentType(node, level, maxLevels, containmentType);
                if (!onlyFlagNodes)
                    AddTiles(node, center, size);
            }
            else
            {
                node.FrustumContainment = containmentType;

                size /= 2;
                for (int i = 0; i < 8; i++)
                {
                    var childNode = node.Children[i];
                    if (childNode != null)
                        CullNode(childNode, level + 1, maxLevels, center + size * OctreeHelper.Signs[i], size);
                }
            }
        }

        void SetContainmentType(Octree<TileInfo>.Node node, int level, int maxLevels, ContainmentType containmentType)
        {
            if (containmentType == ContainmentType.Disjoint)    
                frustumCulledInstances++;

            node.FrustumContainment = containmentType;

            if (level == maxLevels || node.Leaf) return;
            foreach (var childNode in node.Children)
                if (childNode != null)
                    SetContainmentType(childNode, level + 1, maxLevels, containmentType);
        }

        void DoOcclusion()
        {
            occlusionQueriesPerformed = 0;
            synchronousQueries = 0;

            var halfOctreeSize = WorldState.Octree.Size / 2.0f;
            var maxLevels = (int)Math.Log(WorldState.Octree.Size, 2) - SampleState.OctreeTraversalCoarseness;

            occlusionSurface.StartRender(true);
            Scene.SetColorWriteEnable(false, false, false, false);
            {
                foreach (var mesh in meshes.Values)
                {
                    mesh.Shader.SetPassEnable(0, true);
                    mesh.Shader.SetPassEnable(1, false);
                    mesh.Draw();
                    mesh.Shader.SetPassEnable(0, false);
                    mesh.Shader.SetPassEnable(1, true);
                    mesh.ResetBatch();
                }

                Device.RenderState.DepthBias = -CameraState.NearPlane / (CameraState.FarPlane - CameraState.NearPlane);
                OccludeNode(WorldState.Octree.Root, 0, maxLevels, new Vector3f(halfOctreeSize), halfOctreeSize);
                Device.RenderState.DepthBias = 0;
            }
            Scene.SetColorWriteEnable(true, true, true, true);
            occlusionSurface.EndRender();
        }

        void OccludeNode(Octree<TileInfo>.Node node, int level, int maxLevels, Vector3f center, float size)
        {
            if (SampleState.OctreeCullingMode.Value == CullingMode.FurstumAndOcclusion && node.FrustumContainment == ContainmentType.Disjoint)
            {
                DiscardNodeQueries(node, level, maxLevels);
                return;
            }

            var halfSize = new Vector3f(size);
            var boundingBox = new BoundingBox(center - halfSize, center + halfSize);

            var cameraInNode = boundingBox.Grow(new Vector3f(CameraState.NearPlane)).Contains(CameraState.Position);

            if (cameraInNode)
            {
                node.QueryPerformed = false;
                node.IsOccluded = false;
            }
            else
            {
                node.WasOccluded = node.IsOccluded;
                bool synchronousNode = false;
                if ((SampleState.UseSynchronousQueries && 
                    (SampleState.OctreeCullingMode.Value == CullingMode.HardwareOcclusionQueries || node.FrustumContainment == ContainmentType.Intersects)))
                    synchronousNode = true;

                if (!synchronousNode)
                    node.IsOccluded = node.QueryPerformed && camera.OccQuery_GetData(node.Id, true) == 0;
                else if (node.FrustumContainment == ContainmentType.Intersects)
                    node.SynchronousQueriesLeft = 2;

                camera.OccQuery_Begin(node.Id);
                camera.OccQuery_DrawBox(boundingBox.Min, boundingBox.Max);
                camera.OccQuery_End();
                node.QueryPerformed = true;
                occlusionQueriesPerformed++;

                synchronousNode |= SampleState.UseSynchronousQueries && node.SynchronousQueriesLeft > 0;
                if (synchronousNode)
                {
                    node.SynchronousQueriesLeft--;
                    node.IsOccluded = camera.OccQuery_GetData(node.Id, true) == 0;
                    synchronousQueries++;
                }
            }

            if (node.IsOccluded) return;

            if (level == maxLevels || node.Leaf)
                AddTiles(node, center, size);
            else
                if (node.Filled && node.FrustumContainment == ContainmentType.Contains)
                {
                    DiscardNodeQueries(node, level, maxLevels);
                    AddTiles(node, center, size);
                }
                else
                {
                    size /= 2;
                    for (int i = 0; i < 8; i++)
                    {
                        var childNode = node.Children[i];
                        if (childNode != null)
                            OccludeNode(childNode, level + 1, maxLevels, center + size * OctreeHelper.Signs[i], size);
                    }
                }
        }

        static void DiscardNodeQueries(Octree<TileInfo>.Node node, int level, int maxLevels)
        {
            node.QueryPerformed = false;

            if (level == maxLevels || node.Leaf) return;
            foreach (var childNode in node.Children)
                if (childNode != null)
                    DiscardNodeQueries(childNode, level + 1, maxLevels);
        }

        void AddTiles(Octree<TileInfo>.Node node, Vector3f center, float size)
        {
            var tile = node.Data;
            if (tile != null)
                meshes[tile.Type].AddInstance(center, tile.Phi);
            else if (!node.Leaf)
                AddChildrenTiles(node, center, size);
        }
        void AddChildrenTiles(Octree<TileInfo>.Node node, Vector3f center, float size)
        {
            size /= 2;
            for (int i = 0; i < 8; i++)
            {
                var child = node.Children[i];
                if (child != null)
                {
                    var tile = child.Data;
                    if (tile != null)
                        meshes[tile.Type].AddInstance(center + size * OctreeHelper.Signs[i], tile.Phi);
                    else
                        AddChildrenTiles(child, center + size * OctreeHelper.Signs[i], size);
                }
            }
        }

        public override void PreDraw()
        {
            if (cullingNecessary)
                CullOctree();
            cullingNecessary = false;
        }

        public override void PostDraw()
        {
            if (!SampleState.ShowConsole) return;

            const int lineHeight = 13;
            int y = 23;
            var color = Color.White.ToArgb();

            Screen2DText.TextureFont_DrawText("Total Instances : " + WorldState.TotalInstances, 10, y, color); y += lineHeight;
            Screen2DText.TextureFont_DrawText("Instances Drawn : " + meshes.Values.Sum(x => x.ViewedInstances), 10, y, color); y += lineHeight;
            Screen2DText.TextureFont_DrawText("Frustum-Culled Leaf Nodes : " + frustumCulledInstances, 10, y, color); y += lineHeight;
            Screen2DText.TextureFont_DrawText("Maximum Occlusion Queries : " + maxOcclusionQueries, 10, y, color); y += lineHeight;
            Screen2DText.TextureFont_DrawText("Occlusion Queries : " + occlusionQueriesPerformed, 10, y, color); y += lineHeight;
            Screen2DText.TextureFont_DrawText("Synchronous Queries : " + synchronousQueries, 10, y, color); y += lineHeight;
            y += lineHeight;
            Screen2DText.TextureFont_DrawText("Show/Hide Console [Escape] : " + SampleState.ShowConsole, 10, y, color); y += lineHeight;
            y += lineHeight;
            color = Color.LightBlue.ToArgb();
            Screen2DText.TextureFont_DrawText("** General Culling Settings **", 10, y, color); y += lineHeight;
            color = Color.Blue.ToArgb();
            Screen2DText.TextureFont_DrawText("Octree Culling Mode [1] : " + SampleState.OctreeCullingMode, 10, y, color); y += lineHeight;
            Screen2DText.TextureFont_DrawText("Octree Traversal Coarseness [F2/2] ? : " + SampleState.OctreeTraversalCoarseness, 10, y, color); y += lineHeight;
            y += lineHeight;
            color = Color.LightGreen.ToArgb();
            Screen2DText.TextureFont_DrawText("** Hardware Occlusion Query Settings **", 10, y, color); y += lineHeight;
            color = Color.Green.ToArgb();
            Screen2DText.TextureFont_DrawText("RenderSurface/Main-Buffer Size Factor [F3/3] ? : " + SampleState.OcclusionSurfaceFactor, 10, y, color); y += lineHeight;
            Screen2DText.TextureFont_DrawText("Use Synchronous Queries [4] ? : " + SampleState.UseSynchronousQueries, 10, y, color);
        }

        [ServiceDependency]
        public IWorldState WorldState { private get; set; }
        [ServiceDependency]
        public ICameraState CameraState { private get; set; }
        [ServiceDependency]
        public ISampleState SampleState { private get; set; }
    }
}
