#pragma once

#include <glad/glad.h>

namespace anm2ed::resource::shader
{
  struct Info
  {
    const char* vertex;
    const char* fragment;
  };

  inline constexpr auto VERTEX = R"(
  #version 330 core
  layout (location = 0) in vec2 i_position;
  layout (location = 1) in vec2 i_uv;
  out vec2 i_uv_out;
  uniform mat4 u_transform;
  void main()
  {
      i_uv_out = i_uv;
      gl_Position = u_transform * vec4(i_position, 0.0, 1.0);
  }
  )";

  inline constexpr auto AXIS_VERTEX = R"(
  #version 330 core
  layout (location = 0) in vec2 i_position; // full screen line segment: -1..1
  uniform vec2 u_origin_ndc; // world origin in NDC
  uniform int u_axis;       // 0 = X axis, 1 = Y axis

  void main()
  {
      vec2 pos = (u_axis == 0)
          ? vec2(i_position.x, u_origin_ndc.y) // horizontal line across screen
          : vec2(u_origin_ndc.x, i_position.x); // vertical line across screen;

      gl_Position = vec4(pos, 0.0, 1.0);
  }
  )";

  inline constexpr auto FRAGMENT = R"(
  #version 330 core
  out vec4 o_fragColor;
  uniform vec4 u_color;
  void main()
  {
      o_fragColor = u_color;
  }
  )";

  inline constexpr auto TEXTURE_FRAGMENT = R"(
  #version 330 core
  in vec2 i_uv_out;
  uniform sampler2D u_texture;
  uniform vec4 u_tint;
  uniform vec3 u_color_offset;
  out vec4 o_fragColor;
  void main()
  {
      vec4 texColor = texture(u_texture, i_uv_out);
      texColor *= u_tint;
      texColor.rgb += u_color_offset;
      o_fragColor = texColor;
  }
  )";

  inline constexpr auto GRID_VERTEX = R"(
  #version 330 core
  layout (location = 0) in vec2 i_position;
  layout (location = 1) in vec2 i_uv;

  out vec2 i_uv_out;

  void main()
  {
      i_uv_out = i_position;
      gl_Position = vec4(i_position, 0.0, 1.0);
  }
  )";

  inline constexpr auto GRID_FRAGMENT = R"(
  #version 330 core
  in vec2 i_uv_out;

  uniform mat4 u_transform;
  uniform vec2 u_size;
  uniform vec2 u_offset;
  uniform vec4 u_color;

  out vec4 o_fragColor;

  void main()
  {
      vec4 world4 = u_transform * vec4(i_uv_out, 0.0, 1.0);
      vec2 world = world4.xy / world4.w;

      vec2 cell = max(u_size, vec2(1.0));
      vec2 grid = (world - u_offset) / cell;

      vec2 frac = fract(grid);
      vec2 distToLine = min(frac, 1.0 - frac);
      float distance = min(distToLine.x, distToLine.y);

      float fw = min(fwidth(grid.x), fwidth(grid.y));
      float alpha = 1.0 - smoothstep(0.0, fw, distance);

      if (alpha <= 0.0)
          discard;

      o_fragColor = vec4(u_color.rgb, u_color.a * alpha);
  }
  )";

  inline constexpr auto DASHED_VERTEX = R"(
  #version 330 core
  layout (location = 0) in vec2 i_position;

  out vec2 v_local;

  uniform mat4 u_transform;

  void main()
  {
      v_local = i_position;
      gl_Position = u_transform * vec4(i_position, 0.0, 1.0);
  }
  )";

  inline constexpr auto DASHED_FRAGMENT = R"(
  #version 330 core
  in vec2 v_local;

  uniform vec4 u_color;
  uniform vec2 u_axis_x;
  uniform vec2 u_axis_y;
  uniform float u_dash_length;
  uniform float u_dash_gap;
  uniform float u_dash_offset;

  out vec4 o_fragColor;

  void main()
  {
      vec2 local = clamp(v_local, 0.0, 1.0);

      float lengthX = max(length(u_axis_x), 1e-4);
      float lengthY = max(length(u_axis_y), 1e-4);

      float dash = max(u_dash_length, 1e-4);
      float gap = max(u_dash_gap, 0.0);
      float period = max(dash + gap, 1e-4);

      vec2 pixel = max(fwidth(v_local), vec2(1e-5));

      float bottomMask = 1.0 - smoothstep(pixel.y, pixel.y * 2.0, local.y);
      float topMask = 1.0 - smoothstep(pixel.y, pixel.y * 2.0, 1.0 - local.y);
      float leftMask = 1.0 - smoothstep(pixel.x, pixel.x * 2.0, local.x);
      float rightMask = 1.0 - smoothstep(pixel.x, pixel.x * 2.0, 1.0 - local.x);

      float perimeterOffset = u_dash_offset;

      float bottomPos = mod(perimeterOffset + local.x * lengthX, period);
      if (bottomPos < 0.0) bottomPos += period;
      float bottomDash = bottomMask * (bottomPos <= dash ? 1.0 : 0.0);

      float rightPos = mod(perimeterOffset + lengthX + local.y * lengthY, period);
      if (rightPos < 0.0) rightPos += period;
      float rightDash = rightMask * (rightPos <= dash ? 1.0 : 0.0);

      float topPos = mod(perimeterOffset + lengthX + lengthY + (1.0 - local.x) * lengthX, period);
      if (topPos < 0.0) topPos += period;
      float topDash = topMask * (topPos <= dash ? 1.0 : 0.0);

      float leftPos = mod(perimeterOffset + 2.0 * lengthX + lengthY + (1.0 - local.y) * lengthY, period);
      if (leftPos < 0.0) leftPos += period;
      float leftDash = leftMask * (leftPos <= dash ? 1.0 : 0.0);

      float alpha = max(max(bottomDash, topDash), max(leftDash, rightDash));

      if (alpha <= 0.0)
          discard;

      o_fragColor = vec4(u_color.rgb, u_color.a * alpha);
  }
  )";

  constexpr auto UNIFORM_AXIS = "u_axis";
  constexpr auto UNIFORM_COLOR = "u_color";
  constexpr auto UNIFORM_TRANSFORM = "u_transform";
  constexpr auto UNIFORM_TINT = "u_tint";
  constexpr auto UNIFORM_COLOR_OFFSET = "u_color_offset";
  constexpr auto UNIFORM_OFFSET = "u_offset";
  constexpr auto UNIFORM_ORIGIN_NDC = "u_origin_ndc";
  constexpr auto UNIFORM_SIZE = "u_size";
  constexpr auto UNIFORM_MODEL = "u_model";
  constexpr auto UNIFORM_RECT_SIZE = "u_rect_size";
  constexpr auto UNIFORM_TEXTURE = "u_texture";
  constexpr auto UNIFORM_AXIS_X = "u_axis_x";
  constexpr auto UNIFORM_AXIS_Y = "u_axis_y";
  constexpr auto UNIFORM_DASH_LENGTH = "u_dash_length";
  constexpr auto UNIFORM_DASH_GAP = "u_dash_gap";
  constexpr auto UNIFORM_DASH_OFFSET = "u_dash_offset";

  enum ShaderType
  {
    LINE,
    DASHED,
    TEXTURE,
    AXIS,
    GRID,
    COUNT
  };

  const Info SHADERS[COUNT] = {{VERTEX, FRAGMENT},
                               {DASHED_VERTEX, DASHED_FRAGMENT},
                               {VERTEX, TEXTURE_FRAGMENT},
                               {AXIS_VERTEX, FRAGMENT},
                               {GRID_VERTEX, GRID_FRAGMENT}};
}

namespace anm2ed::resource
{
  class Shader
  {
  public:
    GLuint id{};

    Shader();
    Shader(const char*, const char*);
    Shader& operator=(Shader&&) noexcept;
    ~Shader();
    bool is_valid() const;
  };
}
