using System.Reflection;
using System.Runtime.InteropServices;
using Engine.Graphics.Buffer.Vertex;
using OpenTK.Graphics.OpenGL;
using Serilog;

namespace Engine.Graphics.Buffer;

public class VertexArray : OpenGlObject
{
  private int _enabledAttribs = 0;

  public VertexArray()
  {
    GL.CreateVertexArrays(1, out int handle);
    Handle = handle;

    Log.Debug("Vertex array {Handle} created", Handle);
  }

  public void BindIndexBuffer(IndexBuffer parBuffer)
  {
    GL.VertexArrayElementBuffer(Handle, parBuffer.Handle);

    Log.Debug("Index buffer {Buffer} bound to vertex array {Handle}", parBuffer.Handle, Handle);
  }

  public void BindVertexBuffer<T>(VertexBuffer<T> parBuffer, int parBindingIndex = 0, int parDivisor = 0)
    where T : struct, IVertex
  {
    ArgumentOutOfRangeException.ThrowIfNegative(parBindingIndex);
    ArgumentOutOfRangeException.ThrowIfNegative(parDivisor);

    var stride = Marshal.SizeOf<T>();
    var fields = IVertex.GetFields<T>();

    GL.VertexArrayVertexBuffer(Handle, parBindingIndex, parBuffer.Handle, 0, stride);

    foreach (var field in fields)
    {
      var attribute = field.GetCustomAttribute<VertexAttribute>()!;

      var offset = Marshal.OffsetOf<T>(field.Name).ToInt32();
      SetupAttribute(attribute, _enabledAttribs, offset, parBindingIndex);

      _enabledAttribs += attribute.RepeatCount;
    }

    GL.VertexArrayBindingDivisor(Handle, parBindingIndex, parDivisor);

    Log.Debug("Vertex buffer {Buffer} bound to vertex array {Handle} at {BindingIndex} binding with {Divisor} divisor",
      parBuffer.Handle, Handle, parBindingIndex, parDivisor);
  }

  private void SetupAttribute(VertexAttribute parAttribute, int parBaseLocation, int parBaseOffset, int parBindingIndex)
  {
    var size = parAttribute.ComponentCount * IVertex.AttributeSize(parAttribute.Type);

    for (var i = 0; i < parAttribute.RepeatCount; i++)
    {
      var location = parBaseLocation + i;
      var offset = parBaseOffset + (i * size);

      GL.EnableVertexArrayAttrib(Handle, location);
      GL.VertexArrayAttribFormat(Handle, location, parAttribute.ComponentCount, parAttribute.Type,
        parAttribute.Normalized,
        offset);
      GL.VertexArrayAttribBinding(Handle, location, parBindingIndex);
    }
  }

  internal override void Bind()
  {
    GL.BindVertexArray(Handle);
  }

  internal override void Unbind()
  {
    GL.BindVertexArray(0);
  }

  protected override void Destroy()
  {
    GL.DeleteVertexArray(Handle);
  }
}