#include "LineCanvas.h"

static const char* codeVS = R"(
layout (location = 0) out vec4 out_color;

struct Vertex {
  vec4 pos;
  vec4 rgba;
};

layout(std430, buffer_reference) readonly buffer VertexBuffer {
  Vertex vertices[];
};

layout(push_constant) uniform PushConstants {
  mat4 mvp;
  VertexBuffer vb;
};

void main() {
  // Vertex v = vb.vertices[gl_VertexIndex]; <--- does not work on Snapdragon Adreno
  out_color = vb.vertices[gl_VertexIndex].rgba;
  gl_Position = mvp * vb.vertices[gl_VertexIndex].pos;
})";

static const char* codeFS = R"(
layout (location = 0) in vec4 in_color;
layout (location = 0) out vec4 out_color;

void main() {
  out_color = in_color;
})";

void LineCanvas2D::render(const char* nameImGuiWindow)
{
  LVK_PROFILER_FUNCTION();

  ImGui::SetNextWindowPos(ImVec2(0, 0));
  ImGui::SetNextWindowSize(ImGui::GetMainViewport()->Size);
  ImGui::Begin(
      nameImGuiWindow, nullptr,
      ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings |
          ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoInputs);

  ImDrawList* drawList = ImGui::GetBackgroundDrawList();

  for (const LineData& l : lines_) {
    drawList->AddLine(ImVec2(l.p1.x, l.p1.y), ImVec2(l.p2.x, l.p2.y), ImColor(l.color.r, l.color.g, l.color.b, l.color.a));
  }

  ImGui::End();
}

void LineCanvas3D::line(const vec3& p1, const vec3& p2, const vec4& c)
{
  lines_.push_back({ .pos = vec4(p1, 1.0f), .color = c });
  lines_.push_back({ .pos = vec4(p2, 1.0f), .color = c });
}

void LineCanvas3D::plane(
    const vec3& o, const vec3& v1, const vec3& v2, int n1, int n2, float s1, float s2, const vec4& color, const vec4& outlineColor)
{
  line(o - s1 / 2.0f * v1 - s2 / 2.0f * v2, o - s1 / 2.0f * v1 + s2 / 2.0f * v2, outlineColor);
  line(o + s1 / 2.0f * v1 - s2 / 2.0f * v2, o + s1 / 2.0f * v1 + s2 / 2.0f * v2, outlineColor);

  line(o - s1 / 2.0f * v1 + s2 / 2.0f * v2, o + s1 / 2.0f * v1 + s2 / 2.0f * v2, outlineColor);
  line(o - s1 / 2.0f * v1 - s2 / 2.0f * v2, o + s1 / 2.0f * v1 - s2 / 2.0f * v2, outlineColor);

  for (int i = 1; i < n1; i++) {
    float t       = ((float)i - (float)n1 / 2.0f) * s1 / (float)n1;
    const vec3 o1 = o + t * v1;
    line(o1 - s2 / 2.0f * v2, o1 + s2 / 2.0f * v2, color);
  }

  for (int i = 1; i < n2; i++) {
    const float t = ((float)i - (float)n2 / 2.0f) * s2 / (float)n2;
    const vec3 o2 = o + t * v2;
    line(o2 - s1 / 2.0f * v1, o2 + s1 / 2.0f * v1, color);
  }
}

void LineCanvas3D::box(const mat4& m, const vec3& size, const vec4& c)
{
  vec3 pts[8] = {
    vec3(+size.x, +size.y, +size.z), vec3(+size.x, +size.y, -size.z), vec3(+size.x, -size.y, +size.z), vec3(+size.x, -size.y, -size.z),
    vec3(-size.x, +size.y, +size.z), vec3(-size.x, +size.y, -size.z), vec3(-size.x, -size.y, +size.z), vec3(-size.x, -size.y, -size.z),
  };

  for (auto& p : pts)
    p = vec3(m * vec4(p, 1.f));

  line(pts[0], pts[1], c);
  line(pts[2], pts[3], c);
  line(pts[4], pts[5], c);
  line(pts[6], pts[7], c);

  line(pts[0], pts[2], c);
  line(pts[1], pts[3], c);
  line(pts[4], pts[6], c);
  line(pts[5], pts[7], c);

  line(pts[0], pts[4], c);
  line(pts[1], pts[5], c);
  line(pts[2], pts[6], c);
  line(pts[3], pts[7], c);
}

void LineCanvas3D::box(const mat4& m, const BoundingBox& box, const glm::vec4& color)
{
  this->box(m * glm::translate(mat4(1.f), .5f * (box.min_ + box.max_)), 0.5f * vec3(box.max_ - box.min_), color);
}

void LineCanvas3D::frustum(const mat4& camView, const mat4& camProj, const vec4& color)
{
  const vec3 corners[] = { vec3(-1, -1, -1), vec3(+1, -1, -1), vec3(+1, +1, -1), vec3(-1, +1, -1),
                           vec3(-1, -1, +1), vec3(+1, -1, +1), vec3(+1, +1, +1), vec3(-1, +1, +1) };

  vec3 pp[8];

  for (int i = 0; i < 8; i++) {
    vec4 q = glm::inverse(camView) * glm::inverse(camProj) * vec4(corners[i], 1.0f);
    pp[i]  = vec3(q.x / q.w, q.y / q.w, q.z / q.w);
  }
  line(pp[0], pp[4], color);
  line(pp[1], pp[5], color);
  line(pp[2], pp[6], color);
  line(pp[3], pp[7], color);
  // near
  line(pp[0], pp[1], color);
  line(pp[1], pp[2], color);
  line(pp[2], pp[3], color);
  line(pp[3], pp[0], color);
  // x
  line(pp[0], pp[2], color);
  line(pp[1], pp[3], color);
  // far
  line(pp[4], pp[5], color);
  line(pp[5], pp[6], color);
  line(pp[6], pp[7], color);
  line(pp[7], pp[4], color);
  // x
  line(pp[4], pp[6], color);
  line(pp[5], pp[7], color);

  const vec4 gridColor = color * 0.7f;
  const int gridLines  = 100;

  // bottom
  {
    vec3 p1       = pp[0];
    vec3 p2       = pp[1];
    const vec3 s1 = (pp[4] - pp[0]) / float(gridLines);
    const vec3 s2 = (pp[5] - pp[1]) / float(gridLines);
    for (int i = 0; i != gridLines; i++, p1 += s1, p2 += s2)
      line(p1, p2, gridColor);
  }
  // top
  {
    vec3 p1       = pp[2];
    vec3 p2       = pp[3];
    const vec3 s1 = (pp[6] - pp[2]) / float(gridLines);
    const vec3 s2 = (pp[7] - pp[3]) / float(gridLines);
    for (int i = 0; i != gridLines; i++, p1 += s1, p2 += s2)
      line(p1, p2, gridColor);
  }
  // left
  {
    vec3 p1       = pp[0];
    vec3 p2       = pp[3];
    const vec3 s1 = (pp[4] - pp[0]) / float(gridLines);
    const vec3 s2 = (pp[7] - pp[3]) / float(gridLines);
    for (int i = 0; i != gridLines; i++, p1 += s1, p2 += s2)
      line(p1, p2, gridColor);
  }
  // right
  {
    vec3 p1       = pp[1];
    vec3 p2       = pp[2];
    const vec3 s1 = (pp[5] - pp[1]) / float(gridLines);
    const vec3 s2 = (pp[6] - pp[2]) / float(gridLines);
    for (int i = 0; i != gridLines; i++, p1 += s1, p2 += s2)
      line(p1, p2, gridColor);
  }
}

void LineCanvas3D::render(lvk::IContext& ctx, const lvk::Framebuffer& desc, lvk::ICommandBuffer& buf, uint32_t numSamples)
{
  LVK_PROFILER_FUNCTION();

  if (lines_.empty()) {
    return;
  }

  const uint32_t requiredSize = lines_.size() * sizeof(LineData);

  if (currentBufferSize_[currentFrame_] < requiredSize) {
    linesBuffer_[currentFrame_] = ctx.createBuffer(
        { .usage = lvk::BufferUsageBits_Storage, .storage = lvk::StorageType_HostVisible, .size = requiredSize, .data = lines_.data() });
    currentBufferSize_[currentFrame_] = requiredSize;
  } else {
    ctx.upload(linesBuffer_[currentFrame_], lines_.data(), requiredSize);
  }

  if (pipeline_.empty() || pipelineSamples_ != numSamples) {
    pipelineSamples_ = numSamples;

    vert_     = ctx.createShaderModule({ codeVS, lvk::Stage_Vert, "Shader Module: imgui (vert)" });
    frag_     = ctx.createShaderModule({ codeFS, lvk::Stage_Frag, "Shader Module: imgui (frag)" });
    pipeline_ = ctx.createRenderPipeline(
        {
            .topology     = lvk::Topology_Line,
            .smVert       = vert_,
            .smFrag       = frag_,
            .color        = { {
                       .format            = ctx.getFormat(desc.color[0].texture),
                       .blendEnabled      = true,
                       .srcRGBBlendFactor = lvk::BlendFactor_SrcAlpha,
                       .dstRGBBlendFactor = lvk::BlendFactor_OneMinusSrcAlpha,
            } },
            .depthFormat  = desc.depthStencil.texture ? ctx.getFormat(desc.depthStencil.texture) : lvk::Format_Invalid,
            .cullMode     = lvk::CullMode_None,
            .samplesCount = numSamples,
        },
        nullptr);
  }

  struct {
    mat4 mvp;
    uint64_t addr;
  } pc{
    .mvp  = mvp_,
    .addr = ctx.gpuAddress(linesBuffer_[currentFrame_]),
  };
  buf.cmdBindRenderPipeline(pipeline_);
  buf.cmdPushConstants(pc);
  buf.cmdDraw(lines_.size());

  currentFrame_ = (currentFrame_ + 1) % LVK_ARRAY_NUM_ELEMENTS(linesBuffer_);
}
