using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using VisiMath;
using VisimBox.Renderer;
using VisimBox.Resources;
using OpenTK.Graphics.OpenGL;
using System.Runtime.InteropServices;

namespace VisimRenderGL
{
    class GLVertexBuffer : VertexBuffer
    {

        /// <summary>
        /// Initializes empty buffer with specified number of basic elements without cache
        /// </summary>
        /// <param name="count">Count of elements</param>
        /// <param name="usage">Usage of the buffer, default is static (create once)</param>        
        public GLVertexBuffer(Type vertexType, int count, ResourceUsage usage = ResourceUsage.Static)
            : base(vertexType, count, false, usage)
        {
        }

        /// <summary>
        /// Initializes empty buffer with specified number of basic elements
        /// </summary>
        /// <param name="count">Count of elements</param>
        /// <param name="cache">If true, cache is constructed and used</param>
        /// <param name="usage">Usage of the buffer, default is static (create once)</param>
        public GLVertexBuffer(Type vertexType, int count, bool usecache=false, ResourceUsage usage = ResourceUsage.Static)
            : base(vertexType, count, usecache, usage)
        {
            
        }

        /// <summary>
        /// Initialized vertex buffer from array of values
        /// </summary>
        /// <param name="values">Source values</param>
        /// <param name="cache">If true, cache is constructed and used</param>
        /// <param name="useArrayAsCache">If true, vertex buffer will store reference to the source array as cache (so any direct read/write from/to array will be reflected in cache. 
        /// Note, automatically sets cache using to true!</param>
        /// <param name="usage">Usage of the buffer, default is static (create once)</param>
        public GLVertexBuffer(IVertex[] values, bool cache = false, bool useArrayAsCache = false, ResourceUsage usage = ResourceUsage.Static)
            : base(values, cache, useArrayAsCache, usage)
        {
        }

        /// <summary>
        /// Constructs buffer from collection of values
        /// </summary>
        /// <param name="values">Source values</param>
        /// <param name="cache">If true, cache is constructed and used</param>
        /// <param name="usage">Usage of the buffer, default is static (create once)</param>
        public GLVertexBuffer(ICollection<IVertex> values, bool cache = false, ResourceUsage usage = ResourceUsage.Static) : 
            base(values, cache, usage)
        {
        }

        public GLVertexBuffer(IVertexArray varray, bool cache = false, ResourceUsage usage = ResourceUsage.Static) : base(usage)
        {
            VertexType = varray[0].GetType();
            CreateBuffer(varray);
            if (cache)
            {
                this._cached = true;
                this._cache = varray.ToArray();
            }
            else
            {
                this._cached = false;
            }
        }
        

        public override void Dispose()
        {
            ReleaseBuffer();
            base.Dispose();
        }

        protected void CreateBuffer(IVertexArray varray)
        {
            ConstructBuffer(0, true);
            SetBuffer(varray);
        }

        /// <summary>
        /// Sets content of the buffer to varray
        /// </summary>
        /// <param name="varray"></param>
        protected void SetBuffer(IVertexArray varray)
        {
            descriptor = varray.Descriptor;
            IntPtr varraypointer = varray.GetPointer();
            if (varraypointer == null)
                throw new ArgumentException("Wrong type of vertex array used!");


            int size = varray.GetSize();
            _count = varray.Count;
            int testsize;

            GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)size, varraypointer, GetHint(this._usage));
            varray.FreePointer();

            GL.GetBufferParameter(BufferTarget.ArrayBuffer, BufferParameterName.BufferSize, out testsize);
            if (size != testsize)
            {
                ReleaseBuffer();
                throw new ApplicationException("VertexBufferManager.CreateBuffer: Error when uploading vertex buffer!");
            }
        }


        public static BufferUsageHint GetHint(ResourceUsage usage)
        {
            BufferUsageHint bhint;
            switch (usage)
            {
                case ResourceUsage.Static:
                    bhint = BufferUsageHint.StaticDraw;
                    break;
                case ResourceUsage.Streamed:
                    bhint = BufferUsageHint.StreamCopy;
                    break;
                case ResourceUsage.WriteOnly:
                    bhint = BufferUsageHint.DynamicDraw;
                    break;
                case ResourceUsage.ReadOnly:
                    bhint = BufferUsageHint.DynamicRead;
                    break;
                case ResourceUsage.Dynamic:
                    bhint = BufferUsageHint.DynamicCopy;    //maybe not correct, check!!!
                    break;
                case ResourceUsage.StreamedRead:
                    bhint = BufferUsageHint.StreamRead;
                    break;
                case ResourceUsage.StreamedWrite:
                    bhint = BufferUsageHint.StreamDraw;
                    break;
                default:
                    bhint = BufferUsageHint.DynamicCopy;
                    break;
            }
            return bhint;
        }

        protected override void ConstructBuffer(int count, bool justPrepare = false)
        {
            if (_internalID != 0)
                ReleaseBuffer();
            uint rval = 0;
            GL.GenBuffers(1, out rval);
            GL.BindBuffer(BufferTarget.ArrayBuffer, rval);
            if (rval != 0)
            {
                _internalID = rval;
            }
            else throw new InvalidOperationException("Can't construct opengl vertex buffer");

            if (justPrepare)
                return;

            int size = count;

            GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)size, IntPtr.Zero, GetHint(this._usage));
        }

        protected void ReleaseBuffer()
        {
            if (_internalID != 0)
            {
                GL.DeleteBuffers(1, ref _internalID);
                _internalID = 0;
            }            
        }

        public override uint InternalID
        {
            get { return _internalID; }
        }

        public override void WriteCache()
        {
            //GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)size, varraypointer, bhint);
            if (!_cached)
                return;
            if (_cache == null)
                return;

            BindBuffer();

            GCHandle gch = GCHandle.Alloc(this._cache, GCHandleType.Pinned);
            IntPtr data =  gch.AddrOfPinnedObject();

            int size = Marshal.SizeOf(_cache[0]) * _cache.Length;
            int testsize;

            GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)size, data, GetHint(this._usage));

            gch.Free();

            GL.GetBufferParameter(BufferTarget.ArrayBuffer, BufferParameterName.BufferSize, out testsize);
            if (size != testsize)
            {
                ReleaseBuffer();
                throw new ApplicationException("VertexBufferManager.CreateBuffer: Error when uploading vertex buffer!");
            }
        }

        public override void ReadCache(int start, int count)
        {
            if ((!_cached) || (_cache == null))
                return;
            if ((start < 0) || ((start + count) > _cache.Length))
                throw new ArgumentException("Wrong start/count for reading cache!");


            BindBuffer();

            int length = count - start;

            int offset = Marshal.SizeOf(_cache[0]) * start;


            int size = Marshal.SizeOf(_cache[0]) * length;
            GCHandle gch = GCHandle.Alloc(this._cache, GCHandleType.Pinned);
            IntPtr data = gch.AddrOfPinnedObject();

            GL.GetBufferSubData(BufferTarget.ArrayBuffer, (IntPtr)offset, (IntPtr)size, data);
            gch.Free();
        }

        public override void WriteCache(int start, int count)
        {
            BindBuffer();
            if ((!_cached) || (_cache == null))
                return;
            if ((start < 0) || ((start + count) > _cache.Length))
                throw new ArgumentException("Wrong start/count for reading cache!");


            BindBuffer();

            int length = count - start;

            int offset = Marshal.SizeOf(_cache[0]) * start;


            int size = Marshal.SizeOf(_cache[0]) * length;
            GCHandle gch = GCHandle.Alloc(this._cache, GCHandleType.Pinned);
            IntPtr data = gch.AddrOfPinnedObject();

            GL.BufferSubData(BufferTarget.ArrayBuffer, (IntPtr)offset, (IntPtr)size, data);
            gch.Free();
        }

        public override IVertex ReadElement(int position)
        {
            if ((position > _count) || (position<0))
                throw new ArgumentException("ReadElement called with wrong position parameter");
            if (VertexType == null)
                throw new ArgumentException("ReadElement: VertexType is not set");
            BindBuffer();

            int size = VertexFactory.GetByteSize(VertexType);
            int offset = size * position;

            IVertex iv = VertexFactory.CreateNewVertex(VertexType);
            GCHandle gch = GCHandle.Alloc(iv, GCHandleType.Pinned);
            IntPtr data = gch.AddrOfPinnedObject();

            GL.GetBufferSubData(BufferTarget.ArrayBuffer, (IntPtr)offset, (IntPtr)size, data);
            gch.Free();
            return iv;
        }

        public override void WriteElement(int position, ref IVertex value)
        {
            if ((position > _count) || (position < 0))
                throw new ArgumentException("WriteElement called with wrong position parameter");
            if (VertexType != value.GetType())
                throw new ArgumentException("WriteElement: VertexType of buffer is not the same as specified value");
            BindBuffer();

            int size = VertexFactory.GetByteSize(VertexType);
            int offset = size * position;

            GCHandle gch = GCHandle.Alloc(value, GCHandleType.Pinned);
            IntPtr data = gch.AddrOfPinnedObject();

            GL.BufferSubData(BufferTarget.ArrayBuffer, (IntPtr)offset, (IntPtr)size, data);
            gch.Free();
        }

        public override void BindBuffer()
        {
            if (_internalID == 0)
                return;
            GL.BindBuffer(BufferTarget.ArrayBuffer, _internalID);
        }

        public override string FileName
        {
            get
            {
                return null;
            }
            set
            {                
            }
        }

    }
}
