// Copyright (c) Wojciech Figat. All rights reserved.

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace FlaxEngine
{
    /// <summary>
    /// General purpose utility for accessing mesh data (both read and write).
    /// </summary>
    public class MeshAccessor
    {
        /// <summary>
        /// Mesh data stream.
        /// </summary>
        public unsafe ref struct Stream
        {
            private Span<byte> _data;
            private PixelFormat _format;
            private int _stride, _count;
            private readonly PixelFormatSampler _sampler;

            internal Stream(Span<byte> data, PixelFormat format, int stride, int count)
            {
                _data = data;
                _stride = stride;
                _count = count;
                if (PixelFormatSampler.Get(format, out _sampler))
                {
                    _format = format;
                }
                else if (format != PixelFormat.Unknown)
                {
                    Debug.LogError($"Unsupported pixel format '{format}' to sample vertex attribute.");
                }
            }

            /// <summary>
            /// Gets the raw data block.
            /// </summary>
            public Span<byte> Data => _data;

            /// <summary>
            /// Gets the format of the data.
            /// </summary>
            public PixelFormat Format => _format;

            /// <summary>
            /// Gets the stride (in bytes) of the data.
            /// </summary>
            public int Stride => _stride;

            /// <summary>
            /// Gets the count of the items in the stride.
            /// </summary>
            public int Count => _count;

            /// <summary>
            /// Returns true if stream is valid.
            /// </summary>
            public bool IsValid => _format != PixelFormat.Unknown;

            /// <summary>
            /// Checks if the stream can use <seealso cref="SetLinear"/> via a single memory copy.
            /// </summary>
            /// <param name="expectedFormat">Source data format.</param>
            /// <returns>True if stream is linear and format matches expected input data.</returns>
            public bool IsLinear(PixelFormat expectedFormat)
            {
                return _format == expectedFormat && _stride == PixelFormatExtensions.SizeInBytes(_format);
            }

            /// <summary>
            /// Reads an integer value from a given item.
            /// </summary>
            /// <param name="index">Zero-based index of the item.</param>
            /// <returns>Loaded value.</returns>
            public int GetInt(int index)
            {
#if !BUILD_RELEASE
                if (_data == null)
                    throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence.");
#endif
                fixed (byte* data = _data)
                    return (int)_sampler.Read(data + index * _stride).X;
            }

            /// <summary>
            /// Reads a float value from a given item.
            /// </summary>
            /// <param name="index">Zero-based index of the item.</param>
            /// <returns>Loaded value.</returns>
            public float GetFloat(int index)
            {
#if !BUILD_RELEASE
                if (_data == null)
                    throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence.");
#endif
                fixed (byte* data = _data)
                    return _sampler.Read(data + index * _stride).X;
            }

            /// <summary>
            /// Reads a Float2 value from a given item.
            /// </summary>
            /// <param name="index">Zero-based index of the item.</param>
            /// <returns>Loaded value.</returns>
            public Float2 GetFloat2(int index)
            {
#if !BUILD_RELEASE
                if (_data == null)
                    throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence.");
#endif
                fixed (byte* data = _data)
                    return new Float2(_sampler.Read(data + index * _stride));
            }

            /// <summary>
            /// Reads a Float3 value from a given item.
            /// </summary>
            /// <param name="index">Zero-based index of the item.</param>
            /// <returns>Loaded value.</returns>
            public Float3 GetFloat3(int index)
            {
#if !BUILD_RELEASE
                if (_data == null)
                    throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence.");
#endif
                fixed (byte* data = _data)
                    return new Float3(_sampler.Read(data + index * _stride));
            }

            /// <summary>
            /// Reads a Float4 value from a given item.
            /// </summary>
            /// <param name="index">Zero-based index of the item.</param>
            /// <returns>Loaded value.</returns>
            public Float4 GetFloat4(int index)
            {
#if !BUILD_RELEASE
                if (_data == null)
                    throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence.");
#endif
                fixed (byte* data = _data)
                    return _sampler.Read(data + index * _stride);
            }

            /// <summary>
            /// Writes an integer value to a given item.
            /// </summary>
            /// <param name="index">Zero-based index of the item.</param>
            /// <param name="value">Value to assign.</param>
            public void SetInt(int index, int value)
            {
#if !BUILD_RELEASE
                if (_data == null)
                    throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence.");
#endif
                var v = new Float4(value);
                fixed (byte* data = _data)
                    _sampler.Write(data + index * _stride, &v);
            }

            /// <summary>
            /// Writes a float value to a given item.
            /// </summary>
            /// <param name="index">Zero-based index of the item.</param>
            /// <param name="value">Value to assign.</param>
            public void SetFloat(int index, float value)
            {
#if !BUILD_RELEASE
                if (_data == null)
                    throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence.");
#endif
                var v = new Float4(value);
                fixed (byte* data = _data)
                    _sampler.Write(data + index * _stride, &v);
            }

            /// <summary>
            /// Writes a Float2 value to a given item.
            /// </summary>
            /// <param name="index">Zero-based index of the item.</param>
            /// <param name="value">Value to assign.</param>
            public void SetFloat2(int index, Float2 value)
            {
#if !BUILD_RELEASE
                if (_data == null)
                    throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence.");
#endif
                var v = new Float4(value, 0.0f, 0.0f);
                fixed (byte* data = _data)
                    _sampler.Write(data + index * _stride, &v);
            }

            /// <summary>
            /// Writes a Float3 value to a given item.
            /// </summary>
            /// <param name="index">Zero-based index of the item.</param>
            /// <param name="value">Value to assign.</param>
            public void SetFloat3(int index, Float3 value)
            {
#if !BUILD_RELEASE
                if (_data == null)
                    throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence.");
#endif
                var v = new Float4(value, 0.0f);
                fixed (byte* data = _data)
                    _sampler.Write(data + index * _stride, &v);
            }

            /// <summary>
            /// Writes a Float4 value to a given item.
            /// </summary>
            /// <param name="index">Zero-based index of the item.</param>
            /// <param name="value">Value to assign.</param>
            public void SetFloat4(int index, Float4 value)
            {
#if !BUILD_RELEASE
                if (_data == null)
                    throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence.");
#endif
                fixed (byte* data = _data)
                    _sampler.Write(data + index * _stride, &value);
            }

            /// <summary>
            /// Copies a linear block of data into the stream. Assumes <seealso cref="IsLinear"/> returned true for the format of the input data.
            /// </summary>
            /// <remarks>Check input data and stream type with IsLinear before calling.</remarks>
            /// <param name="data">Pointer to the source data.</param>
            public void SetLinear(IntPtr data)
            {
#if !BUILD_RELEASE
                if (_data == null)
                    throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence.");
#endif
                new Span<byte>(data.ToPointer(), _data.Length).CopyTo(_data);
            }

            /// <summary>
            /// Copies the contents of the input <see cref="T:System.Span`1"/> into the elements of this stream.
            /// </summary>
            /// <param name="src">The source <see cref="T:System.Span`1"/>.</param>
            public void Set(Span<Float2> src)
            {
#if !BUILD_RELEASE
                if (_data == null)
                    throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence.");
#endif
                if (IsLinear(PixelFormat.R32G32_Float))
                {
                    src.CopyTo(MemoryMarshal.Cast<byte, Float2>(_data));
                }
                else
                {
                    var count = Count;
                    fixed (byte* data = _data)
                    {
                        for (int i = 0; i < count; i++)
                        {
                            var v = new Float4(src[i], 0.0f, 0.0f);
                            _sampler.Write(data + i * _stride, &v);
                        }
                    }
                }
            }

            /// <summary>
            /// Copies the contents of the input <see cref="T:System.Span`1"/> into the elements of this stream.
            /// </summary>
            /// <param name="src">The source <see cref="T:System.Span`1"/>.</param>
            public void Set(Span<Float3> src)
            {
#if !BUILD_RELEASE
                if (_data == null)
                    throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence.");
#endif
                if (IsLinear(PixelFormat.R32G32B32_Float))
                {
                    src.CopyTo(MemoryMarshal.Cast<byte, Float3>(_data));
                }
                else
                {
                    var count = Count;
                    fixed (byte* data = _data)
                    {
                        for (int i = 0; i < count; i++)
                        {
                            var v = new Float4(src[i], 0.0f);
                            _sampler.Write(data + i * _stride, &v);
                        }
                    }
                }
            }

            /// <summary>
            /// Copies the contents of the input <see cref="T:System.Span`1"/> into the elements of this stream.
            /// </summary>
            /// <param name="src">The source <see cref="T:System.Span`1"/>.</param>
            public void Set(Span<Color> src)
            {
#if !BUILD_RELEASE
                if (_data == null)
                    throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence.");
#endif
                if (IsLinear(PixelFormat.R32G32B32A32_Float))
                {
                    src.CopyTo(MemoryMarshal.Cast<byte, Color>(_data));
                }
                else
                {
                    var count = Count;
                    fixed (byte* data = _data)
                    {
                        for (int i = 0; i < count; i++)
                        {
                            var v = (Float4)src[i];
                            _sampler.Write(data + i * _stride, &v);
                        }
                    }
                }
            }

            /// <summary>
            /// Copies the contents of the input <see cref="T:System.Span`1"/> into the elements of this stream.
            /// </summary>
            /// <param name="src">The source <see cref="T:System.Span`1"/>.</param>
            public void Set(Span<uint> src)
            {
#if !BUILD_RELEASE
                if (_data == null)
                    throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence.");
#endif
                if (IsLinear(PixelFormat.R32_UInt))
                {
                    src.CopyTo(MemoryMarshal.Cast<byte, uint>(_data));
                }
                else if (IsLinear(PixelFormat.R16_UInt))
                {
                    var count = Count;
                    fixed (byte* data = _data)
                    {
                        for (int i = 0; i < count; i++)
                            ((ushort*)data)[i] = (ushort)src[i];
                    }
                }
                else
                {
                    var count = Count;
                    fixed (byte* data = _data)
                    {
                        for (int i = 0; i < count; i++)
                        {
                            var v = new Float4(src[i]);
                            _sampler.Write(data + i * _stride, &v);
                        }
                    }
                }
            }

            /// <summary>
            /// Copies the contents of this stream into a destination <see cref="T:System.Span`1" />.
            /// </summary>
            /// <param name="dst">The destination <see cref="T:System.Span`1" />.</param>
            public void CopyTo(Span<Float2> dst)
            {
#if !BUILD_RELEASE
                if (_data == null)
                    throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence.");
#endif
                if (IsLinear(PixelFormat.R32G32_Float))
                {
                    _data.CopyTo(MemoryMarshal.Cast<Float2, byte>(dst));
                }
                else
                {
                    var count = Count;
                    fixed (byte* data = _data)
                    {
                        for (int i = 0; i < count; i++)
                            dst[i] = new Float2(_sampler.Read(data + i * _stride));
                    }
                }
            }

            /// <summary>
            /// Copies the contents of this stream into a destination <see cref="T:System.Span`1" />.
            /// </summary>
            /// <param name="dst">The destination <see cref="T:System.Span`1" />.</param>
            public void CopyTo(Span<Float3> dst)
            {
#if !BUILD_RELEASE
                if (_data == null)
                    throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence.");
#endif
                if (IsLinear(PixelFormat.R32G32B32_Float))
                {
                    _data.CopyTo(MemoryMarshal.Cast<Float3, byte>(dst));
                }
                else
                {
                    var count = Count;
                    fixed (byte* data = _data)
                    {
                        for (int i = 0; i < count; i++)
                            dst[i] = new Float3(_sampler.Read(data + i * _stride));
                    }
                }
            }

            /// <summary>
            /// Copies the contents of this stream into a destination <see cref="T:System.Span`1" />.
            /// </summary>
            /// <param name="dst">The destination <see cref="T:System.Span`1" />.</param>
            public void CopyTo(Span<Color> dst)
            {
#if !BUILD_RELEASE
                if (_data == null)
                    throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence.");
#endif
                if (IsLinear(PixelFormat.R32G32B32A32_Float))
                {
                    _data.CopyTo(MemoryMarshal.Cast<Color, byte>(dst));
                }
                else
                {
                    var count = Count;
                    fixed (byte* data = _data)
                    {
                        for (int i = 0; i < count; i++)
                            dst[i] = (Color)_sampler.Read(data + i * _stride);
                    }
                }
            }

            /// <summary>
            /// Copies the contents of this stream into a destination <see cref="T:System.Span`1" />.
            /// </summary>
            /// <param name="dst">The destination <see cref="T:System.Span`1" />.</param>
            public void CopyTo(Span<uint> dst)
            {
#if !BUILD_RELEASE
                if (_data == null)
                    throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence.");
#endif
                if (IsLinear(PixelFormat.R32_UInt))
                {
                    _data.CopyTo(MemoryMarshal.Cast<uint, byte>(dst));
                }
                else if (IsLinear(PixelFormat.R16_UInt))
                {
                    var count = Count;
                    fixed (byte* data = _data)
                    {
                        for (int i = 0; i < count; i++)
                            dst[i] = ((ushort*)data)[i];
                    }
                }
                else
                {
                    var count = Count;
                    fixed (byte* data = _data)
                    {
                        for (int i = 0; i < count; i++)
                            dst[i] = (uint)_sampler.Read(data + i * _stride).X;
                    }
                }
            }

            /// <summary>
            /// Checks if stream is valid.
            /// </summary>
            /// <param name="stream">The stream to check.</param>
            /// <returns>True if stream is valid, otherwise false.</returns>
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            public static implicit operator bool(Stream stream)
            {
                return stream.IsValid;
            }
        }

        private byte[][] _data = new byte[(int)MeshBufferType.MAX][];
        private PixelFormat[] _formats = new PixelFormat[(int)MeshBufferType.MAX];
        private GPUVertexLayout[] _layouts = new GPUVertexLayout[(int)MeshBufferType.MAX];

        /// <summary>
        /// Loads the data from the mesh.
        /// </summary>
        /// <param name="mesh">The source mesh object to access.</param>
        /// <param name="forceGpu">If set to <c>true</c> the data will be downloaded from the GPU, otherwise it can be loaded from the drive (source asset file) or from memory (if cached). Downloading mesh from GPU requires this call to be made from the other thread than main thread. Virtual assets are always downloaded from GPU memory due to lack of dedicated storage container for the asset data.</param>
        /// <param name="buffers">Custom list of mesh buffers to load. Use empty value to access all of them.</param>
        /// <returns>True if failed, otherwise false.</returns>
        public bool LoadMesh(MeshBase mesh, bool forceGpu = false, Span<MeshBufferType> buffers = new())
        {
            if (mesh == null)
                return true;
            Span<MeshBufferType> allBuffers = stackalloc MeshBufferType[(int)MeshBufferType.MAX] { MeshBufferType.Index, MeshBufferType.Vertex0, MeshBufferType.Vertex1, MeshBufferType.Vertex2 };
            var buffersLocal = buffers.IsEmpty ? allBuffers : buffers;
            if (mesh.DownloadData(buffersLocal.ToArray(), out var meshBuffers, out var meshLayouts, forceGpu))
                return true;
            for (int i = 0; i < buffersLocal.Length; i++)
            {
                int buffer = (int)buffersLocal[i];
                _data[buffer] = meshBuffers[i];
                _layouts[buffer] = meshLayouts[i];

                // Get format if using a single item (eg. index buffer type)
                var format = PixelFormat.Unknown;
                if (meshLayouts[i] && meshLayouts[i].Elements.Length == 1)
                    format = meshLayouts[i].Elements[0].Format;
                _formats[buffer] = format;
            }
            return false;
        }

        /// <summary>
        /// Loads the data from the provided mesh buffer.
        /// </summary>
        /// <param name="bufferType">Type of the mesh buffer to load.</param>
        /// <param name="bufferData">Data used by that buffer.</param>
        /// <param name="layout">Layout of the elements in the buffer.</param>
        /// <returns>True if failed, otherwise false.</returns>
        public bool LoadBuffer(MeshBufferType bufferType, Span<byte> bufferData, GPUVertexLayout layout)
        {
            if (bufferData.IsEmpty)
                return true;
            if (layout == null)
                return true;
            int idx = (int)bufferType;
            _data[idx] = bufferData.ToArray();
            _layouts[idx] = layout;
            return false;
        }

        /// <summary>
        /// Allocates the data for the mesh vertex buffer.
        /// </summary>
        /// <param name="bufferType">Type of the mesh buffer to initialize.</param>
        /// <param name="count">Amount of items in the buffer.</param>
        /// <param name="layout">Layout of the elements in the buffer.</param>
        /// <returns>True if failed, otherwise false.</returns>
        public bool AllocateBuffer(MeshBufferType bufferType, int count, GPUVertexLayout layout)
        {
            if (count <= 0)
                return true;
            if (layout == null)
                return true;
            int idx = (int)bufferType;
            _data[idx] = new byte[count * layout.Stride];
            _layouts[idx] = layout;
            return false;
        }

        /// <summary>
        /// Allocates the data for the mesh buffer.
        /// </summary>
        /// <param name="bufferType">Type of the mesh buffer to initialize.</param>
        /// <param name="count">Amount of items in the buffer.</param>
        /// <param name="format">Format of the elements in the buffer.</param>
        /// <returns>True if failed, otherwise false.</returns>
        public bool AllocateBuffer(MeshBufferType bufferType, int count, PixelFormat format)
        {
            if (count <= 0)
                return true;
            int stride = PixelFormatExtensions.SizeInBytes(format);
            if (stride <= 0)
                return true;
            int idx = (int)bufferType;
            _data[idx] = new byte[count * stride];
            _formats[idx] = format;
            return false;
        }

        /// <summary>
        /// Updates the mesh vertex and index buffers with data assigned to the accessor (eg. via AllocateBuffer).
        /// </summary>
        /// <param name="mesh">The target mesh to update.</param>
        /// <param name="calculateBounds">True if auto-calculate mesh local bounding box based on input positions stream. Otherwise, mesh bound swill stay unchanged.</param>
        /// <returns>True if failed, otherwise false.</returns>
        public unsafe bool UpdateMesh(MeshBase mesh, bool calculateBounds = true)
        {
            if (mesh == null)
                return true;
            int IB = (int)MeshBufferType.Index;
            int VB0 = (int)MeshBufferType.Vertex0;
            int VB1 = (int)MeshBufferType.Vertex1;
            int VB2 = (int)MeshBufferType.Vertex2;

            uint vertices = 0, triangles = 0;
            fixed (byte* data0Ptr = _data[0])
            fixed (byte* data1Ptr = _data[1])
            fixed (byte* data2Ptr = _data[2])
            fixed (byte* data3Ptr = _data[3])
            {
                Span<IntPtr> dataPtr = stackalloc IntPtr[(int)MeshBufferType.MAX] { new IntPtr(data0Ptr), new IntPtr(data1Ptr), new IntPtr(data2Ptr), new IntPtr(data3Ptr) };
                IntPtr ibData = IntPtr.Zero;
                bool use16BitIndexBuffer = false;
                IntPtr[] vbData = new IntPtr[3];
                GPUVertexLayout[] vbLayout = new GPUVertexLayout[3];
                if (_data[VB0] != null && _data[VB0].Length != 0)
                {
                    vbData[0] = dataPtr[VB0];
                    vbLayout[0] = _layouts[VB0];
                    vertices = (uint)_data[VB0].Length / _layouts[VB0].Stride;
                }
                if (_data[VB1] != null && _data[VB1].Length != 0)
                {
                    vbData[1] = dataPtr[VB1];
                    vbLayout[1] = _layouts[VB1];
                    vertices = (uint)_data[VB1].Length / _layouts[VB1].Stride;
                }
                if (_data[VB2] != null && _data[VB2].Length != 0)
                {
                    vbData[2] = dataPtr[VB2];
                    vbLayout[2] = _layouts[VB2];
                    vertices = (uint)_data[VB2].Length / _layouts[VB2].Stride;
                }
                if (_data[IB] != null && _formats[IB] != PixelFormat.Unknown)
                {
                    ibData = dataPtr[IB];
                    use16BitIndexBuffer = _formats[IB] == PixelFormat.R16_UInt;
                    triangles = (uint)(_data[IB].Length / (PixelFormatExtensions.SizeInBytes(_formats[IB]) * 3));
                }

                if (mesh.Init(vertices, triangles, vbData, ibData, use16BitIndexBuffer, vbLayout))
                    return true;
            }

            if (calculateBounds)
            {
                // Calculate mesh bounds
                BoundingBox bounds;
                var positionStream = Position();
                if (!positionStream.IsValid)
                    return true;
                if (positionStream.IsLinear(PixelFormat.R32G32B32_Float))
                {
                    Span<byte> positionData = positionStream.Data;
                    BoundingBox.FromPoints(MemoryMarshal.Cast<byte, Float3>(positionData), out bounds);
                }
                else
                {
                    Float3 min = Float3.Maximum, max = Float3.Minimum;
                    PixelFormatSampler.Get(positionStream.Format, out var positionSampler);
                    int positionStride = positionStream.Stride;
                    fixed (byte* data = positionStream.Data)
                    {
                        for (int i = 0; i < vertices; i++)
                        {
                            Float3 pos = new Float3(positionSampler.Read(data + i * positionStride));
                            Float3.Min(ref min, ref pos, out min);
                            Float3.Max(ref max, ref pos, out max);
                        }
                    }
                    bounds = new BoundingBox(min, max);
                }
                mesh.SetBounds(ref bounds);
            }

            return false;
        }

        /// <summary>
        /// Access stream with index buffer.
        /// </summary>
        /// <returns>Mesh data stream (might be invalid if data is not provided).</returns>
        public Stream Index()
        {
            Span<byte> data = new Span<byte>();
            PixelFormat format = PixelFormat.Unknown;
            int stride = 0, count = 0;
            var ib = _data[(int)MeshBufferType.Index];
            if (ib != null)
            {
                data = ib;
                format = _formats[(int)MeshBufferType.Index];
                stride = PixelFormatExtensions.SizeInBytes(format);
                count = data.Length / stride;
            }
            return new Stream(data, format, stride, count);
        }

        /// <summary>
        /// Access stream with a specific vertex attribute.
        /// </summary>
        /// <param name="attribute">Type of the attribute.</param>
        /// <returns>Mesh data stream (might be invalid if attribute is not provided).</returns>
        public Stream Attribute(VertexElement.Types attribute)
        {
            Span<byte> data = new Span<byte>();
            PixelFormat format = PixelFormat.Unknown;
            int stride = 0, count = 0;
            for (int vbIndex = 0; vbIndex < 3 && format == PixelFormat.Unknown; vbIndex++)
            {
                int idx = vbIndex + 1;
                var layout = _layouts[idx];
                if (!layout)
                    continue;
                foreach (VertexElement e in layout.Elements)
                {
                    var vb = _data[idx];
                    if (e.Type == attribute && vb != null)
                    {
                        data = new Span<byte>(vb).Slice(e.Offset);
                        format = e.Format;
                        stride = (int)layout.Stride;
                        count = vb.Length / stride;
                        break;
                    }
                }
            }
            return new Stream(data, format, stride, count);
        }

        /// <summary>
        /// Access stream with vertex position attribute.
        /// </summary>
        /// <returns>Mesh data stream (might be invalid if attribute is not provided).</returns>
        public Stream Position()
        {
            return Attribute(VertexElement.Types.Position);
        }

        /// <summary>
        /// Access stream with vertex color attribute.
        /// </summary>
        /// <returns>Mesh data stream (might be invalid if attribute is not provided).</returns>
        public Stream Color()
        {
            return Attribute(VertexElement.Types.Color);
        }

        /// <summary>
        /// Access stream with vertex normal vector attribute.
        /// </summary>
        /// <returns>Mesh data stream (might be invalid if attribute is not provided).</returns>
        public Stream Normal()
        {
            return Attribute(VertexElement.Types.Normal);
        }

        /// <summary>
        /// Access stream with vertex tangent vector attribute.
        /// </summary>
        /// <returns>Mesh data stream (might be invalid if attribute is not provided).</returns>
        public Stream Tangent()
        {
            return Attribute(VertexElement.Types.Tangent);
        }

        /// <summary>
        /// Access stream with vertex skeleton bones blend indices attribute.
        /// </summary>
        /// <returns>Mesh data stream (might be invalid if attribute is not provided).</returns>
        public Stream BlendIndices()
        {
            return Attribute(VertexElement.Types.BlendIndices);
        }

        /// <summary>
        /// Access stream with vertex skeleton bones blend weights attribute.
        /// </summary>
        /// <returns>Mesh data stream (might be invalid if attribute is not provided).</returns>
        public Stream BlendWeights()
        {
            return Attribute(VertexElement.Types.BlendWeights);
        }

        /// <summary>
        /// Access stream with vertex texture coordinates attribute (specific UV channel).
        /// </summary>
        /// <param name="channel">UV channel index (zero-based).</param>
        /// <returns>Mesh data stream (might be invalid if attribute is not provided).</returns>
        public Stream TexCoord(int channel = 0)
        {
            return Attribute((VertexElement.Types)((byte)VertexElement.Types.TexCoord0 + channel));
        }

        /// <summary>
        /// Gets or sets the index buffer with triangle indices.
        /// </summary>
        /// <remarks>Uses <see cref="Index"/> stream to read or write data to the index buffer.</remarks>
        public uint[] Triangles
        {
            get => GetStreamUInt(Index());
            set => SetStreamUInt(Index(), value);
        }

        /// <summary>
        /// Gets or sets the vertex positions. Null if <see cref="VertexElement.Types.Position"/> does not exist in vertex buffers of the mesh.
        /// </summary>
        /// <remarks>Uses <see cref="Position"/> stream to read or write data to the vertex buffer.</remarks>
        public Float3[] Positions
        {
            get => GetStreamFloat3(VertexElement.Types.Position);
            set => SetStreamFloat3(VertexElement.Types.Position, value);
        }

        /// <summary>
        /// Gets or sets the vertex colors. Null if <see cref="VertexElement.Types.Color"/> does not exist in vertex buffers of the mesh.
        /// </summary>
        /// <remarks>Uses <see cref="Color"/> stream to read or write data to the vertex buffer.</remarks>
        public Color[] Colors
        {
            get => GetStreamColor(VertexElement.Types.Color);
            set => SetStreamColor(VertexElement.Types.Color, value);
        }

        /// <summary>
        /// Gets or sets the vertex normal vectors (unpacked, normalized). Null if <see cref="VertexElement.Types.Normal"/> does not exist in vertex buffers of the mesh.
        /// </summary>
        /// <remarks>Uses <see cref="Normal"/> stream to read or write data to the vertex buffer.</remarks>
        public Float3[] Normals
        {
            get => GetStreamFloat3(VertexElement.Types.Normal, UnpackNormal);
            set => SetStreamFloat3(VertexElement.Types.Normal, value, PackNormal);
        }

        /// <summary>
        /// Gets or sets the vertex tangent vectors (unpacked, normalized). Null if <see cref="VertexElement.Types.Tangent"/> does not exist in vertex buffers of the mesh.
        /// </summary>
        /// <remarks>Uses <see cref="Tangent"/> stream to read or write data to the vertex buffer.</remarks>
        public Float3[] Tangents
        {
            get => GetStreamFloat3(VertexElement.Types.Tangent, UnpackNormal);
            set => SetStreamFloat3(VertexElement.Types.Tangent, value, PackNormal);
        }

        /// <summary>
        /// Gets or sets the vertex UVs (texcoord channel 0). Null if <see cref="VertexElement.Types.TexCoord"/> does not exist in vertex buffers of the mesh.
        /// </summary>
        /// <remarks>Uses <see cref="TexCoord"/> stream to read or write data to the vertex buffer.</remarks>
        public Float2[] TexCoords
        {
            get => GetStreamFloat2(VertexElement.Types.TexCoord);
            set => SetStreamFloat2(VertexElement.Types.TexCoord, value);
        }

        /// <summary>
        /// Recalculates normal vectors for all vertices.
        /// </summary>
        public void ComputeNormals()
        {
            var positions = Position();
            var indices = Index();
            if (!positions)
                throw new Exception("Cannot compute tangents without positions.");
            if (!indices)
                throw new Exception("Cannot compute tangents without indices.");
            if (!Normal())
                throw new Exception("Cannot compute tangents without Normal vertex element.");
            var vertexCount = positions.Count;
            if (vertexCount == 0)
                return;
            var indexCount = indices.Count;

            // Compute per-face normals but store them per-vertex
            var normals = new Float3[vertexCount];
            for (int i = 0; i < indexCount; i += 3)
            {
                var i1 = indices.GetInt(i + 0);
                var i2 = indices.GetInt(i + 1);
                var i3 = indices.GetInt(i + 2);
                Float3 v1 = positions.GetFloat3(i1);
                Float3 v2 = positions.GetFloat3(i2);
                Float3 v3 = positions.GetFloat3(i3);
                Float3 n = Float3.Cross((v2 - v1), (v3 - v1)).Normalized;

                normals[i1] += n;
                normals[i2] += n;
                normals[i3] += n;
            }

            // Average normals
            for (int i = 0; i < normals.Length; i++)
                normals[i] = normals[i].Normalized;

            // Write back to the buffer
            Normals = normals;
        }

        /// <summary>
        /// Recalculates tangent vectors for all vertices based on normals.
        /// </summary>
        public void ComputeTangents()
        {
            var normals = Normal();
            var tangents = Tangent();
            if (!normals)
                throw new Exception("Cannot compute tangents without normals.");
            if (!tangents)
                throw new Exception("Cannot compute tangents without Tangent vertex element.");
            var count = normals.Count;
            for (int i = 0; i < count; i++)
            {
                Float3 normal = normals.GetFloat3(i);
                UnpackNormal(ref normal);
                RenderTools.CalculateTangentFrame(out var n, out var t, ref normal);
                tangents.SetFloat4(i, t);
            }
        }

        private uint[] GetStreamUInt(Stream stream)
        {
            uint[] result = null;
            if (stream.IsValid)
            {
                result = new uint[stream.Count];
                stream.CopyTo(result);
            }
            return result;
        }

        private void SetStreamUInt(Stream stream, uint[] value)
        {
            if (stream.IsValid)
            {
                stream.Set(value);
            }
        }

        private delegate void TransformDelegate3(ref Float3 value);

        private Float3[] GetStreamFloat3(VertexElement.Types attribute, TransformDelegate3 transform = null)
        {
            Float3[] result = null;
            var stream = Attribute(attribute);
            if (stream.IsValid)
            {
                result = new Float3[stream.Count];
                stream.CopyTo(result);
                if (transform != null)
                {
                    for (int i = 0; i < result.Length; i++)
                        transform(ref result[i]);
                }
            }
            return result;
        }

        private void SetStreamFloat3(VertexElement.Types attribute, Float3[] value, TransformDelegate3 transform = null)
        {
            var stream = Attribute(attribute);
            if (stream.IsValid)
            {
                if (transform != null)
                {
                    // TODO: transform in-place?
                    value = (Float3[])value.Clone();
                    for (int i = 0; i < value.Length; i++)
                        transform(ref value[i]);
                }
                stream.Set(value);
            }
        }

        private Float2[] GetStreamFloat2(VertexElement.Types attribute)
        {
            Float2[] result = null;
            var stream = Attribute(attribute);
            if (stream.IsValid)
            {
                result = new Float2[stream.Count];
                stream.CopyTo(result);
            }
            return result;
        }

        private void SetStreamFloat2(VertexElement.Types attribute, Float2[] value)
        {
            var stream = Attribute(attribute);
            if (stream.IsValid)
            {
                stream.Set(value);
            }
        }

        private Color[] GetStreamColor(VertexElement.Types attribute)
        {
            Color[] result = null;
            var stream = Attribute(attribute);
            if (stream.IsValid)
            {
                result = new Color[stream.Count];
                stream.CopyTo(result);
            }
            return result;
        }

        private void SetStreamColor(VertexElement.Types attribute, Color[] value)
        {
            var stream = Attribute(attribute);
            if (stream.IsValid)
            {
                stream.Set(value);
            }
        }

        /// <summary>
        /// Unpacks normal/tangent vector from normalized range to full range.
        /// </summary>
        /// <param name="value">In and out value.</param>
        public static void UnpackNormal(ref Float3 value)
        {
            // [0; 1] -> [-1; 1]
            value = value * 2.0f - 1.0f;
        }

        /// <summary>
        /// Unpacks normal/tangent vector from normalized range to full range.
        /// </summary>
        /// <param name="value">In and out value.</param>
        /// <param name="sign">Encoded sign in the alpha channel.</param>
        public static void UnpackNormal(ref Float4 value, out float sign)
        {
            sign = value.W > Mathf.Epsilon ? -1.0f : +1.0f;

            // [0; 1] -> [-1; 1]
            value = value * 2.0f - 1.0f;
        }

        /// <summary>
        /// Packs normal/tangent vector to normalized range from full range.
        /// </summary>
        /// <param name="value">In and out value.</param>
        public static void PackNormal(ref Float3 value)
        {
            // [-1; 1] -> [0; 1]
            value = value * 0.5f + 0.5f;
        }
    }
}
