using System;
using System.Collections.Generic;
using System.Linq;
using SharpDX;
using SharpDX.Direct3D;
using SharpDX.Direct3D11;
using Framefield.Core.Rendering;
using Buffer = SharpDX.Direct3D11.Buffer;

namespace Framefield.Core.ID0f2da784_a015_4a7f_a255_45e2b284af9e
{
    public class Class_ReplicateFast : FXSourceCodeFunction
    {
        #region Renderer
        public class Renderer : BaseRenderer
        {
            public override void SetupEffect(OperatorPartContext context)
            {
                base.SetupEffect(context);
                try
                {
                    SetupMaterialConstBuffer(context);
                    SetupFogSettingsConstBuffer(context);
                    SetupPointLightsConstBuffer(context);
                }
                catch (Exception e)
                {
                    Logger.Error(ParentFunc, "Error building constant buffer: {0} - Source: {1}", e.Message, e.Source);
                }
            }
            public OperatorPart.Function ParentFunc {get;set;}            
        }
        #endregion

        public Class_ReplicateFast()
        {
            _renderer = new Renderer(){ParentFunc = this};
        }

        public override void Dispose()
        {
            Utilities.DisposeObj(ref _renderer);
            base.Dispose();
        }

        //>>> _inputids
        private enum InputId
        {
            Code = 0,
            SceneInput = 1,
            Count = 2,
            Radius = 3,
            StretchX = 4,
            StretchY = 5,
            StretchZ = 6,
            Shape = 7,
            LayerX = 8,
            LayerY = 9,
            Scatter = 10,
            OffsetX = 11,
            OffsetY = 12,
            OffsetZ = 13,
            OffsetRandomX = 14,
            OffsetRandomY = 15,
            OffsetRandomZ = 16,
            NoiseTime = 17,
            NoiseAmount = 18,
            NoiseScale = 19,
            Size = 20,
            SizeRandom = 21,
            Orientation = 22,
            RotateX = 23,
            RotateY = 24,
            RotateZ = 25,
            RotateRandomX = 26,
            RotateRandomY = 27,
            RotateRandomZ = 28,
            Seed = 29
        }
        //<<< _inputids


        private bool _firstEval = true;
        public override OperatorPartContext Eval(OperatorPartContext context, List<OperatorPart> inputs, int outputIdx)
        {
            //>>> _params
            var Code = inputs[(int)InputId.Code].Eval(context).Text;
            var SceneInput = inputs[(int)InputId.SceneInput];
            var Count = inputs[(int)InputId.Count].Eval(context).Value;
            var Radius = inputs[(int)InputId.Radius].Eval(context).Value;
            var StretchX = inputs[(int)InputId.StretchX].Eval(context).Value;
            var StretchY = inputs[(int)InputId.StretchY].Eval(context).Value;
            var StretchZ = inputs[(int)InputId.StretchZ].Eval(context).Value;
            var Stretch = new Vector3(StretchX, StretchY, StretchZ);
            var Shape = (int) inputs[(int)InputId.Shape].Eval(context).Value;
            var LayerX = inputs[(int)InputId.LayerX].Eval(context).Value;
            var LayerY = inputs[(int)InputId.LayerY].Eval(context).Value;
            var Layer = new Vector2(LayerX, LayerY);
            var Scatter = inputs[(int)InputId.Scatter].Eval(context).Value;
            var OffsetX = inputs[(int)InputId.OffsetX].Eval(context).Value;
            var OffsetY = inputs[(int)InputId.OffsetY].Eval(context).Value;
            var OffsetZ = inputs[(int)InputId.OffsetZ].Eval(context).Value;
            var Offset = new Vector3(OffsetX, OffsetY, OffsetZ);
            var OffsetRandomX = inputs[(int)InputId.OffsetRandomX].Eval(context).Value;
            var OffsetRandomY = inputs[(int)InputId.OffsetRandomY].Eval(context).Value;
            var OffsetRandomZ = inputs[(int)InputId.OffsetRandomZ].Eval(context).Value;
            var OffsetRandom = new Vector3(OffsetRandomX, OffsetRandomY, OffsetRandomZ);
            var NoiseTime = inputs[(int)InputId.NoiseTime].Eval(context).Value;
            var NoiseAmount = inputs[(int)InputId.NoiseAmount].Eval(context).Value;
            var NoiseScale = inputs[(int)InputId.NoiseScale].Eval(context).Value;
            var Size = inputs[(int)InputId.Size].Eval(context).Value;
            var SizeRandom = inputs[(int)InputId.SizeRandom].Eval(context).Value;
            var Orientation = (int) inputs[(int)InputId.Orientation].Eval(context).Value;
            var RotateX = inputs[(int)InputId.RotateX].Eval(context).Value;
            var RotateY = inputs[(int)InputId.RotateY].Eval(context).Value;
            var RotateZ = inputs[(int)InputId.RotateZ].Eval(context).Value;
            var Rotate = new Vector3(RotateX, RotateY, RotateZ);
            var RotateRandomX = inputs[(int)InputId.RotateRandomX].Eval(context).Value;
            var RotateRandomY = inputs[(int)InputId.RotateRandomY].Eval(context).Value;
            var RotateRandomZ = inputs[(int)InputId.RotateRandomZ].Eval(context).Value;
            var RotateRandom = new Vector3(RotateRandomX, RotateRandomY, RotateRandomZ);
            var Seed = inputs[(int)InputId.Seed].Eval(context).Value;
            //<<< _params        

            if (!SceneInput.Connections.Any() || Count <= 0.5f )
            {
                return context;
            }

            if (_firstEval)
            {
                for (int i = 0; i < NumCodes(); ++i)
                    Compile(i);
                _firstEval = false;
                Changed = true;
            }

            const float toRad = (float) Math.PI/180f;

            var prevTransform = context.ObjectTWorld;
            var rand = new Random((int) Seed);
            Layer.X = Math.Max(1, (int) Layer.X);
            Layer.Y = Math.Max(1, (int) Layer.Y);

            //var meshCollector = new OperatorPart.CollectOpPartFunctionsOfType<Framefield.Core.OperatorPartTraits.IMeshSupplier>();
            _meshCollector.Clear();
            SceneInput.TraverseWithFunction(_meshCollector, null);
            var meshSupplier = _meshCollector.CollectedOpPartFunctions.FirstOrDefault();
            if (meshSupplier == null)
            {
                Logger.Error(this,"Found no mesh supplier, have you forgotten to add an input?");
                return context;
            }
            var meshes = new List<Mesh>();
            meshSupplier.AddMeshesTo(meshes);
            if (meshes.Count != 1)
            {
                Logger.Error(this,"Found more or less than 1 mesh");
                return context;
            }
            var instancedMesh = meshes[0];

            // instance data buffer
            const int instanceDataSize = 4*16; // float4x4
            var numInstances = (int) Count;
            var streamSize = numInstances * instanceDataSize;
            if (_instanceDataBuffer == null || _instanceDataBuffer.Description.SizeInBytes != streamSize)
            {
                Utilities.DisposeObj(ref _instanceDataBuffer);
                _instanceDataBuffer = new Buffer(context.D3DDevice, streamSize, ResourceUsage.Dynamic, BindFlags.VertexBuffer,
                                                 CpuAccessFlags.Write, ResourceOptionFlags.None, instanceDataSize);
            }

            DataStream instanceDataStream;
            context.D3DDevice.ImmediateContext.MapSubresource(_instanceDataBuffer, MapMode.WriteDiscard, MapFlags.None, out instanceDataStream);
            using (instanceDataStream)
            {
                instanceDataStream.Position = 0;

                for (var i = 0; i < numInstances; ++i)
                {
                    Vector3 t = new Vector3();

                    
                    switch (Shape)
                    {
                        // Point
                        case 0:
                            t = Vector3.Normalize(new Vector3(
                                            (float) (rand.NextDouble() - 0.5)*Radius*StretchX,
                                            (float) (rand.NextDouble() - 0.5)*Radius*StretchY,
                                            (float) (rand.NextDouble() - 0.5)*Radius*StretchZ)) 
                               * (float)rand.NextDouble() * Radius;
                                      
                            break;
                        // Sphere
                        case 1:
                        {
                            var inc = Math.PI*(3 - Math.Sqrt(5));
                            var off = 2/(Count + 1);
                            var y = i*off*Stretch.Y + (Stretch.Z -1) - 1 + (off/2);
                            var r = Math.Sqrt(1 - y*y);
                            var phi = i*Stretch.X*inc;
                            t = new Vector3((float) (Math.Cos(phi)*r*Radius),
                                            (float) (y*Radius),
                                            (float) (Math.Sin(phi)*r*Radius));
                            break;                            
                        }
                                                
                        // Ring
                        case 2:
                        {
                            float itemsPerShell =  Count/(Layer.X);    // with even distribution                            
                            float indexInShell = i%itemsPerShell;
                            float shellIndex = (i-indexInShell)/itemsPerShell;
                            
                            float itemsPerRing = (itemsPerShell / Math.Max(1,Layer.Y));
                            float indexInRing = (int)(indexInShell % itemsPerRing);
                            float layerIndex = (int)(indexInShell / itemsPerRing);
                          
                            float a = indexInRing / itemsPerRing;
                            
                            t = new Vector3((float) (Math.Sin(Math.PI*2*a)*(Radius + shellIndex * Stretch.X)),
                                            Layer.Y <= 1 ? 0
                                                         : (layerIndex/(Layer.Y-1)  -0.5f) *Stretch.Y ,
                                            (float) (Math.Cos(Math.PI*2*a)*(Radius + shellIndex*Stretch.Z)));
                            break;
                        }
                        
                        // Box (can also be used as a plane)
                        case 3:
                        {
                            var volume = Math.Abs(StretchX) * Math.Abs(StretchY) * Math.Abs(StretchZ);
                            
                            var s1= new Vector3(Math.Abs(StretchX), Math.Abs(StretchY), Math.Abs(StretchZ));
                            var a= s1.Y/s1.X;
                            var b= s1.Z/s1.X;
                            
                            var xx = (float)Math.Pow( 1/ (a*b), 1.0/3.0);
                            var s2 = s1 * (xx/s1.X);
                                                        
                            var edgeCount= Math.Pow(Count,1.0/3.0);
                            
                            var itemsX= Math.Max(1, Math.Round(edgeCount * s2.X));
                            var itemsY= Math.Max(1, Math.Round(edgeCount * s2.Y));
                            var itemsZ= Math.Max(1, Math.Round(edgeCount * s2.Z));

                            if( itemsZ> itemsX && itemsZ> itemsY) {
                                itemsZ= Count/itemsX/itemsY;
                            }
                            else if( itemsX > itemsY && itemsX > itemsZ) {
                                itemsX= Count/itemsY/itemsZ;
                            }
                            else {
                                itemsY= Count/itemsX/itemsZ;
                            }

                            var x = (int)(i % itemsX)/ itemsX;
                            var y = (int)((i/itemsX) % itemsY) / itemsY;
                            var z = (int)((i/itemsX/itemsY)) / itemsZ;
                                                                                    
                            t = new Vector3((float) (x- (itemsX > 1 ? 0.5 : 0)),
                                            (float) (y- (itemsY > 1 ? 0.5 : 0)),
                                            (float) (z- (itemsZ > 1 ? 0.5 : 0))) * Stretch;
                            break;
                        }

                    }

                    if (NoiseAmount != 0)
                    {
                        _noiseTime = NoiseTime;
                        _frequency = 1/NoiseScale;
                        var noiseOffset = new Vector3(getNoise(t.X / Radius - 6.3f), getNoise(t.Z / Radius + 9.3f), getNoise((t.Y - 0.3f)));
                        t += noiseOffset * NoiseAmount;
                    }

                    float s = Size + (float)rand.NextDouble() * SizeRandom;
                    var scale = new Vector3(s, s, s);
                    
                    //Matrix rotation;
                    Matrix transform;
                    Matrix.RotationYawPitchRoll(Rotate.Y * toRad + (float)(rand.NextDouble()) * RotateRandom.Y * toRad,
                                                Rotate.X * toRad + (float)(rand.NextDouble()) * RotateRandom.X * toRad,
                                                Rotate.Z * toRad + (float)(rand.NextDouble()) * RotateRandom.Z * toRad,
                                                out transform);
                    
                                                                    
                    var tScale = Matrix.Scaling( scale.X, scale.Y, scale.Z);
                    transform= tScale * transform;
                    var t2= Matrix.Translation(new Vector3(OffsetX + (float)(rand.NextDouble() -0.5f) * (OffsetRandomX + Scatter),
                                                           OffsetY + (float)(rand.NextDouble() -0.5f) * (OffsetRandomY + Scatter),
                                                           OffsetZ + (float)(rand.NextDouble() -0.5f) * (OffsetRandomZ + Scatter)));
                    transform = transform * t2;

                    if (Orientation == 0)
                    {
                        if (t.Length() > 0)
                        {
                            var dir = -t;
                            dir.Normalize();
                            var helperDir = new Vector3(0, -1, 0);
                            if (Math.Abs(dir.X) < 0.001 && Math.Abs(dir.Z) < 0.001)
                                helperDir = new Vector3(0, 0, 1);
                            var xAxis = Vector3.Cross(dir, helperDir);
                            xAxis.Normalize();
                            var yAxis = Vector3.Cross(-xAxis, dir);
                            yAxis.Normalize();
                            var m = Matrix.Identity;
                            m.Row1 = new Vector4(xAxis, 0);
                            m.Row2 = new Vector4(yAxis, 0);
                            m.Row3 = new Vector4(dir, 0);
                            transform *= m;
                        }
                    }
                    else if (Orientation == 2)
                    {
                        //if (Shape == 2)
                        //{
                        //    var rotationAroundCenter = Matrix.Identity;
                        //    Matrix.RotationY((float)(2 * Math.PI * i / Count), out rotationAroundCenter);
                        //    transform *= rotationAroundCenter;
                        //}
                        var cameraToWorld = context.WorldToCamera;
                        cameraToWorld.Invert();
                        var newObjectToWorld = context.ObjectTWorld * cameraToWorld;
                        newObjectToWorld.Row4 = context.ObjectTWorld.Row4;
                        transform *=    newObjectToWorld;                     
                        
                    }

                    transform *= Matrix.Transformation(new Vector3(), new Quaternion(), new Vector3(1,1,1), new Vector3(), new Quaternion(), t);
                    transform *= prevTransform; 

                    instanceDataStream.Write(transform);
                }
            }
            context.D3DDevice.ImmediateContext.UnmapSubresource(_instanceDataBuffer, 0);

            var prevEffect = context.Effect;
            var prevRenderer = context.Renderer;
            context.Effect = _effect;
            context.Renderer = _renderer;

            try
            {
                _renderer.SetupEffect(context);

                if (context.DepthStencilView != null)
                    context.D3DDevice.ImmediateContext.OutputMerger.SetTargets(context.DepthStencilView, context.RenderTargetView);
                else
                    context.D3DDevice.ImmediateContext.OutputMerger.SetTargets(context.RenderTargetView);

                if (context.BlendState != null)
                {
                    context.D3DDevice.ImmediateContext.OutputMerger.BlendState = context.BlendState;
                }

                if (context.DepthStencilState != null)
                {
                    context.D3DDevice.ImmediateContext.OutputMerger.DepthStencilState = context.DepthStencilState;
                }

                if (context.RasterizerState != null)
                {
                    context.D3DDevice.ImmediateContext.Rasterizer.State = context.RasterizerState;
                }

                var technique = _effect.GetTechniqueByIndex(0);
                var pass = technique.GetPassByIndex(0);

                context.D3DDevice.ImmediateContext.Rasterizer.SetViewport(context.Viewport);
                context.D3DDevice.ImmediateContext.InputAssembler.InputLayout = new InputLayout(context.D3DDevice, pass.Description.Signature, _instanceDataInputElements);
                context.D3DDevice.ImmediateContext.InputAssembler.PrimitiveTopology = PrimitiveTopology.TriangleList;
                context.D3DDevice.ImmediateContext.InputAssembler.SetVertexBuffers(0, new [] { instancedMesh.Vertices, _instanceDataBuffer}, new [] {76, instanceDataSize}, new [] {0,0} );

                pass.Apply(context.D3DDevice.ImmediateContext);
                context.D3DDevice.ImmediateContext.DrawInstanced(instancedMesh.NumTriangles*3, numInstances, 0, 0);
            }
            catch (Exception exception)
            {
                Logger.Error(this,"Replicate2 - An error occured during evaluation: {0}", exception.Message);
            }

            context.Effect = prevEffect;
            context.Renderer = prevRenderer;

            return context;
        }

        #region helpers

        public float Noise(int x, int seed)
        {
            int n = x + seed*137;
            n = (n << 13) ^ n;
            return (float) (1.0 - ((n*(n*n*15731 + 789221) + 1376312589) & 0x7fffffff)/1073741824.0);
        }

        public float Lerp(float a, float b, float t)
        {
            return a + t*(b - a);
        }

        public float Fade(float t)
        {
            return t*t*t*(t*(t*6 - 15) + 10);
        }

        public float Interpolate(float a, float b, float t)
        {
            float ft = t*3.1415927f;
            float f = (float) (1.0 - Math.Cos(ft))*0.5f;
            return a*(1.0f - f) + b*f;
        }

        private float getNoise(float value)
        {
            float noiseSum = 0.0f;
            value *= _frequency;
            value += _noiseTime+10000;


            noiseSum = Lerp(Noise((int) value, SEED),
                            Noise((int) value + 1, SEED),
                            Fade(value - (float) Math.Floor(value)));
            return noiseSum;
        }

        #endregion

        private const int SEED = 0;
        private float _frequency = 1;
        private float _noiseTime;
        private OperatorPart.CollectOpPartFunctionsOfType<Framefield.Core.OperatorPartTraits.IMeshSupplier> _meshCollector = new OperatorPart.CollectOpPartFunctionsOfType<Framefield.Core.OperatorPartTraits.IMeshSupplier>();
        private Renderer _renderer;
        private Buffer _instanceDataBuffer;
        private InputElement[] _instanceDataInputElements = new []
                                                                {
                                                                    new InputElement("POSITION", 0, SharpDX.DXGI.Format.R32G32B32A32_Float, 0, 0),
                                                                    new InputElement("NORMAL", 0, SharpDX.DXGI.Format.R32G32B32_Float, 16, 0),
                                                                    new InputElement("COLOR", 0, SharpDX.DXGI.Format.R32G32B32A32_Float, 28, 0),
                                                                    new InputElement("TEXCOORD", 0, SharpDX.DXGI.Format.R32G32_Float, 44, 0),
                                                                    new InputElement("TANGENT", 0, SharpDX.DXGI.Format.R32G32B32_Float, 52, 0),
                                                                    new InputElement("BINORMAL", 0, SharpDX.DXGI.Format.R32G32B32_Float, 64, 0),
                                                                    new InputElement("INSTANCE_OBJ_TO_WORLD_ROW", 0, SharpDX.DXGI.Format.R32G32B32A32_Float, 0,
                                                                                     1, InputClassification.PerInstanceData, 1),
                                                                    new InputElement("INSTANCE_OBJ_TO_WORLD_ROW", 1, SharpDX.DXGI.Format.R32G32B32A32_Float, 16,
                                                                                     1, InputClassification.PerInstanceData, 1),
                                                                    new InputElement("INSTANCE_OBJ_TO_WORLD_ROW", 2, SharpDX.DXGI.Format.R32G32B32A32_Float, 32,
                                                                                     1, InputClassification.PerInstanceData, 1),
                                                                    new InputElement("INSTANCE_OBJ_TO_WORLD_ROW", 3, SharpDX.DXGI.Format.R32G32B32A32_Float, 48,
                                                                                     1, InputClassification.PerInstanceData, 1),
                                                                };
    }
}

