using OpenTK.Graphics.OpenGL;
using OpenTK.Mathematics;
using Serilog;

namespace Engine.Graphics.Shader;

public class Program : OpenGlObject
{
  private readonly Dictionary<string, int> _uniforms = new();

  public Program(string parVertexSource, string parFragmentSource)
  {
    var vertexShader = CompileSource(parVertexSource, ShaderType.VertexShader);
    var fragmentShader = CompileSource(parFragmentSource, ShaderType.FragmentShader);

    Handle = LinkProgram(vertexShader, fragmentShader);
  }

  public void SetUniform<T>(string parName, in T parValue)
  {
    try
    {
      var location = GetUniformLocation(parName);

      switch (parValue)
      {
        case bool boolValue:
          GL.ProgramUniform1(Handle, location, boolValue ? 1 : 0);
          break;
        case int intValue:
          GL.ProgramUniform1(Handle, location, intValue);
          break;
        case float floatValue:
          GL.ProgramUniform1(Handle, location, floatValue);
          break;
        case Vector2 vec2:
          GL.ProgramUniform2(Handle, location, vec2);
          break;
        case Vector3 vec3:
          GL.ProgramUniform3(Handle, location, vec3);
          break;
        case Vector4 vec4:
          GL.ProgramUniform4(Handle, location, vec4);
          break;
        case Matrix4 matrix:
          GL.ProgramUniformMatrix4(Handle, location, false, ref matrix);
          break;
        case int[] intArray:
          GL.ProgramUniform1(Handle, location, intArray.Length, intArray);
          break;
        case float[] floatArray:
          GL.ProgramUniform1(Handle, location, floatArray.Length, floatArray);
          break;
        default:
          throw new ArgumentException($"Unsupported uniform type: {typeof(T).Name}");
      }
    }
    catch (Exception ex)
    {
      Log.Error(ex, "Failed to set uniform {UniformName}", parName);
      throw;
    }
  }

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

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

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

  private int GetUniformLocation(string parName)
  {
    if (_uniforms.TryGetValue(parName, out var location))
    {
      return location;
    }

    location = GL.GetUniformLocation(Handle, parName);
    if (location < 0)
    {
      throw new ArgumentException($"Uniform '{parName}' not found in shader program");
    }

    _uniforms.Add(parName, location);
    return location;
  }

  private static int CompileSource(string parSource, ShaderType parType)
  {
    var shaderId = GL.CreateShader(parType);
    GL.ShaderSource(shaderId, parSource);
    GL.CompileShader(shaderId);

    GL.GetShader(shaderId, ShaderParameter.CompileStatus, out var status);
    if (status == 0)
    {
      var log = GL.GetShaderInfoLog(shaderId);
      GL.DeleteShader(shaderId);

      throw new ShaderCompilationException(parType, log);
    }

    return shaderId;
  }

  private static int LinkProgram(int parVertexShader, int parFragmentShader)
  {
    var programId = GL.CreateProgram();
    try
    {
      GL.AttachShader(programId, parVertexShader);
      GL.AttachShader(programId, parFragmentShader);
      GL.LinkProgram(programId);

      GL.GetProgram(programId, GetProgramParameterName.LinkStatus, out var linkStatus);
      if (linkStatus == 0)
      {
        var log = GL.GetProgramInfoLog(programId);
        GL.DeleteProgram(programId);

        throw new ShaderLinkException(log);
      }

      GL.ValidateProgram(programId);
      GL.GetProgram(programId, GetProgramParameterName.ValidateStatus, out var validateStatus);
      if (validateStatus == 0)
      {
        var log = GL.GetProgramInfoLog(programId);
        GL.DeleteProgram(programId);

        throw new ShaderValidationException(log);
      }
    }
    finally
    {
      GL.DetachShader(programId, parVertexShader);
      GL.DetachShader(programId, parFragmentShader);
      GL.DeleteShader(parVertexShader);
      GL.DeleteShader(parFragmentShader);
    }

    return programId;
  }
}

public class ShaderCompilationException(ShaderType parType, string parMessage)
  : Exception($"Failed to compile {parType} shader: {parMessage}")
{
  public ShaderType ShaderType { get; } = parType;
}

public class ShaderLinkException(string parMessage) : Exception($"Failed to link shader program: {parMessage}");

public class ShaderValidationException(string parMessage)
  : Exception($"Failed to validate shader program: {parMessage}");