using System.Runtime.InteropServices;
using Engine.Graphics.Pixel;
using OpenTK.Graphics.OpenGL;
using OpenTK.Mathematics;
using Serilog;

namespace Engine.Graphics.Texture;

public abstract class Texture : OpenGlObject, ITexture
{
  public Vector2i Size => new(Width, Height);

  public int Width
  {
    get => _width;
    protected set
    {
      if (value <= 0)
      {
        throw new ArgumentException("Width must be greater than 0");
      }

      _width = value;
    }
  }

  public int Height
  {
    get => _height;
    protected set
    {
      if (value <= 0)
      {
        throw new ArgumentException("Height must be greater than 0");
      }

      _height = value;
    }
  }

  private int _width;
  private int _height;

  protected Texture(int parWidth, int parHeight)
  {
    Width = parWidth;
    Height = parHeight;

    GL.CreateTextures(TextureTarget.Texture2D, 1, out int handle);
    Handle = handle;

    GL.TextureParameter(Handle, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
    GL.TextureParameter(Handle, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
    GL.TextureParameter(Handle, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
    GL.TextureParameter(Handle, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);

    Log.Debug("Texture {Handle} created with {Width}x{Height}", Handle, Width, Height);
  }

  public void UploadPixels<T>(int parX, int parY, int parWidth, int parHeight, T[,] parPixels)
    where T : struct, IPixel
  {
    if (parX < 0 || parY < 0)
    {
      throw new ArgumentException("x and y must be greater than 0");
    }

    if (parWidth <= 0 || parHeight <= 0)
    {
      throw new ArgumentException("Width and height must be greater than 0");
    }

    if (parX + parWidth > Width || parY + parHeight > Height)
    {
      throw new ArgumentException("x + width and y + height must be less than width and height");
    }

    if (parPixels.Length != parWidth * parHeight)
    {
      throw new ArgumentException("Pixels array must be of size width * height");
    }

    var format = parPixels[0, 0].Format;
    var type = parPixels[0, 0].Type;

    GL.TextureSubImage2D(Handle, 0, parX, parY, parWidth, parHeight, format, type, parPixels);
  }

  public void ReadPixels<T>(int parX, int parY, int parWidth, int parHeight, T[,] parPixels)
    where T : struct, IPixel
  {
    if (parX < 0 || parY < 0)
    {
      throw new ArgumentException("x and y must be greater than 0");
    }

    if (parWidth <= 0 || parHeight <= 0)
    {
      throw new ArgumentException("Width and height must be greater than 0");
    }

    if (parX + parWidth > Width || parY + parHeight > Height)
    {
      throw new ArgumentException("x + width and y + height must be less than width and height");
    }

    if (parPixels.Length != parWidth * parHeight)
    {
      throw new ArgumentException("Pixels array must be of size width * height");
    }

    var format = default(T).Format;
    var type = default(T).Type;

    // Set alignment to avoid row padding issues
    GL.PixelStore(PixelStoreParameter.PackAlignment, 1);
    GL.PixelStore(PixelStoreParameter.UnpackAlignment, 1);

    GL.GetTextureSubImage(Handle, 0, parX, parY, 0, parWidth, parHeight, 1, format, type,
      parPixels.Length * Marshal.SizeOf<T>(),
      parPixels);
  }

  public void BindUnit(int parUnit = 0)
  {
    GL.BindTextureUnit(parUnit, Handle);
  }

  internal override void Bind()
  {
    GL.BindTexture(TextureTarget.Texture2D, Handle);
  }

  internal override void Unbind()
  {
    GL.BindTexture(TextureTarget.Texture2D, 0);
  }

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