using System;
using Direct3D = Microsoft.DirectX.Direct3D;
using DirectX = Microsoft.DirectX;

using ComponentFramework.Modules;
using ComponentFramework;
using MTV3D65;
using System.Reflection;

namespace OctreeRendering
{
    class InstancedMesh : Component
    {
        public const int InstancesPerBatch = 248;
        const int StartRegister = 256 - InstancesPerBatch;

        readonly DirectX.Vector4[] batchPositions = new DirectX.Vector4[InstancesPerBatch];

        Direct3D.VertexBuffer vb;
        Direct3D.IndexBuffer ib;
        Direct3D.PrimitiveType primitiveType;
        Direct3D.VertexDeclaration vertexDeclaration;

        int templateVertexCount;
        int templateIndexCount;
        int templatePrimitiveCount;

        public DirectX.Vector4[] positions;

        public int ViewedInstances { get; private set; }
        public int MaxInstances { get; private set; }

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

        public override void Initialize()
        {
            CameraState.ViewChanged += UpdateViewProjectionParameter;
            CameraState.ProjectionChanged += UpdateViewProjectionParameter;
        }

        protected override void DisposeInternal()
        {
            if (vertexDeclaration != null)
            {
                Device.VertexDeclaration = null;
                vertexDeclaration.Dispose();
            }
            if (vb != null)
            {
                Device.SetStreamSource(0, null, 0);
                vb.Dispose();
            }
            if (ib != null)
            {
                Device.Indices = null;
                ib.Dispose();
            }
        }

        public void CreateInstances<T>(T[] templateVertices, short[] templateIndices, Direct3D.PrimitiveType primitiveType, int maxInstances) 
            where T : struct, IInstancedVertex
        {
            DisposeInternal();

            MaxInstances = maxInstances;
            this.primitiveType = primitiveType;

            vertexDeclaration = new Direct3D.VertexDeclaration(Device, VertexHelper.GetElements<T>());

            templateVertexCount = templateVertices.Length;
            templateIndexCount = templateIndices.Length;

            var totalVertices = templateVertexCount * InstancesPerBatch;
            var totalIndices = templateIndexCount * InstancesPerBatch;

            templatePrimitiveCount = templateIndexCount;
            switch (primitiveType)
            {
                case Direct3D.PrimitiveType.LineList:       templatePrimitiveCount /= 2; break;
                case Direct3D.PrimitiveType.LineStrip:      templatePrimitiveCount -= 1; break;
                case Direct3D.PrimitiveType.TriangleFan:    templatePrimitiveCount -= 2; break;
                case Direct3D.PrimitiveType.TriangleList:   templatePrimitiveCount /= 3; break;
                case Direct3D.PrimitiveType.TriangleStrip:  templatePrimitiveCount -= 2; break;
            }

            vb = new Direct3D.VertexBuffer(typeof(T), totalVertices, Device, Direct3D.Usage.WriteOnly, Direct3D.VertexFormats.None, Direct3D.Pool.Managed);
            ib = new Direct3D.IndexBuffer(typeof(int), totalIndices, Device, Direct3D.Usage.WriteOnly, Direct3D.Pool.Managed);

            var vertexArray = (T[]) vb.Lock(0, typeof(T), Direct3D.LockFlags.None, totalVertices);
            var indexArray = (int[]) ib.Lock(0, typeof(int), Direct3D.LockFlags.None, totalIndices);
            for (int i = 0; i < InstancesPerBatch; i++)
            {
                for (int j = 0; j < templateVertexCount; j++)
                {
                    vertexArray[i * templateVertexCount + j] = templateVertices[j];
                    vertexArray[i * templateVertexCount + j].InstanceIndex = i;
                }
                for (int j = 0; j < templateIndexCount; j++)
                    indexArray[i * templateIndexCount + j] = templateIndices[j] + i * templateVertexCount;
            }
            ib.Unlock();
            vb.Unlock();

            positions = new DirectX.Vector4[maxInstances];
        }

        public void ResetBatch()
        {
            ViewedInstances = 0;
        }

        public void AddInstance(Vector3f position, float phi)
        {
            positions[ViewedInstances] = new DirectX.Vector4(position.X, position.Y, position.Z, phi);
            ViewedInstances++;
        }

        TVShader shader;
        public TVShader Shader
        {
            get { return shader; }
            set
            {
                shader = value;
                UpdateTextureParameter();
                UpdateViewProjectionParameter();
            }
        }

        int texture;
        public int Texture
        {
            get { return texture; }
            set 
            { 
                texture = value;
                UpdateTextureParameter();
            }
        }

        void UpdateTextureParameter()
        {
            if (shader == null) return;
            shader.SetEffectParamTexture("Texture", texture);
        }
        void UpdateViewProjectionParameter()
        {
            if (shader == null) return;
            shader.SetEffectParamMatrix("ViewProjection", CameraState.ViewProjection);
        }

        public override void Draw()
        {
            if (MaxInstances == 0 || shader == null) return;

            Device.VertexDeclaration = vertexDeclaration;
            Device.SetStreamSource(0, vb, 0);
            Device.Indices = ib;

            InternalObjects.Shader_Begin(shader);

            int drawnInstances = 0;
            while (drawnInstances < ViewedInstances)
            {
                var actualBatchSize = Math.Min(InstancesPerBatch, ViewedInstances - drawnInstances);
                Array.Copy(positions, drawnInstances, batchPositions, 0, actualBatchSize);
                drawnInstances += InstancesPerBatch;

                Device.SetVertexShaderConstant(StartRegister, batchPositions);
                Device.DrawIndexedPrimitives(primitiveType, 0, 0, templateVertexCount * actualBatchSize, 0, templatePrimitiveCount * actualBatchSize);
            }

            InternalObjects.Shader_End(shader);
        }

        [ServiceDependency]
        public ICameraState CameraState { private get; set; }
    }
}
