// GLFW 版本的渲染入口：画触觉面板盒子 + 点阵实例化圆点，
// 右键拖拽环绕相机，滚轮调 FOV，内置简单波纹数据做演示。
#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <algorithm>
#include <cmath>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <iostream>
#include <vector>

#include "myshader.hh"
#include "stb_image_wrap.h"


struct AppState {
  // 点阵规格
  int rows = 8;
  int cols = 10;
  float pitch = 0.025f;
  float dotRadius = 0.008f;
  // 面板尺寸
  float panelW = 0.25f;
  float panelH = 0.35f;
  float panelD = 0.01f;
  // 数据范围
  int minV = 0;
  int maxV = 500;
  // 渲染模式：0=有光照，1=无光照（flat/unlit）
  int renderMode = 1;

  // 相机状态（环绕）
  float camYaw = -90.0f;
  float camPitch = 0.0f;
  float zoom = 45.0f;
  bool rightDown = false;
  double lastMouseX = 0.0;
  double lastMouseY = 0.0;

  bool valuesDirty = true;
  std::vector<float> values;

  glm::mat4 mvp{1.0f};
  glm::vec3 cameraPos{0.0f, 0.0f, 3.0f};
};

AppState g_state;

Shader* bg_shader = nullptr;
Shader* base_shader = nullptr;
Shader* room_shader = nullptr;
Shader* panel_shader = nullptr;
Shader* dots_shader = nullptr;
Shader* heatmap_shader = nullptr;

unsigned int panel_vao = 0;
unsigned int panel_vbo = 0;
unsigned int panel_ibo = 0;
int panel_index_count = 0;

unsigned int room_vao = 0;
unsigned int room_vbo = 0;
unsigned int room_ibo = 0;
int room_index_count = 0;

unsigned int dots_vao = 0;
unsigned int dots_vbo = 0;
unsigned int instance_vbo = 0;
int instance_count = 0;

unsigned int dot_tex = 0;

unsigned int bg_vao = 0;
unsigned int bg_vbo = 0;

unsigned int metal_tex = 0;
bool panel_geometry_dirty = true;
bool dot_geometry_dirty = true;

unsigned int base_vao = 0;
unsigned int base_vbo = 0;
unsigned int base_ibo = 0;
int base_index_count = 0;

unsigned int skirt_vao = 0;
unsigned int skirt_vbo = 0;
unsigned int skirt_ibo = 0;
int skirt_index_count = 0;

unsigned int heatmap_vao = 0;
unsigned int heatmap_vbo = 0;
unsigned int heatmap_ibo = 0;
int heatmap_index_count = 0;

unsigned int height_tex = 0;

constexpr int kLowRows = 7;
constexpr int kLowCols = 12;
constexpr int kHeightW = 1200;
constexpr int kHeightH = 700;
constexpr int kMeshCols = 240;
constexpr int kMeshRows = 140;
constexpr float kPlaneW = 1.2f;
constexpr float kPlaneH = 0.7f;
constexpr float kThickness = 0.12f;
constexpr float kDentMax = 0.08f;

std::vector<float> low_values;
std::vector<float> height_values;
bool height_dirty = true;

std::vector<glm::vec2> skirt_uvs;
std::vector<glm::vec3> skirt_normals;
std::vector<float> skirt_vertices;

void framebuffer_size_callback(GLFWwindow* /*window*/, int width, int height) {
  // 视口随窗口大小变化
  glViewport(0, 0, width, height);
}

void mouse_button_callback(GLFWwindow* window, int button, int action,
                           int /*mods*/) {
  // 右键按下/抬起，用于开启/关闭环绕相机拖拽
  if (button == GLFW_MOUSE_BUTTON_RIGHT) {
    if (action == GLFW_PRESS) {
      g_state.rightDown = true;
      glfwGetCursorPos(window, &g_state.lastMouseX, &g_state.lastMouseY);
    } else if (action == GLFW_RELEASE) {
      g_state.rightDown = false;
    }
  }
}

void cursor_pos_callback(GLFWwindow* /*window*/, double xpos, double ypos) {
  // 右键拖拽时，根据鼠标增量更新 yaw/pitch
  if (!g_state.rightDown) return;

  const float dx = static_cast<float>(xpos - g_state.lastMouseX);
  const float dy = static_cast<float>(ypos - g_state.lastMouseY);
  g_state.lastMouseX = xpos;
  g_state.lastMouseY = ypos;

  g_state.camYaw += dx * 0.3f;
  /* if (g_state.camYaw >= -70) {
    g_state.camYaw = -70;
  }
  if (g_state.camYaw <= -110) {
    g_state.camYaw = -110;
  } */
  g_state.camPitch = std::clamp(g_state.camPitch + dy * 0.3f, -89.0f, 89.0f);
}

void scroll_callback(GLFWwindow* /*window*/, double /*xoffset*/,
                     double yoffset) {
  // 滚轮调整视角 FOV（越小越近）
  const float factor = std::pow(0.9f, static_cast<float>(yoffset));
  g_state.zoom = std::clamp(g_state.zoom * factor, 5.0f, 80.0f);
}

void process_input(GLFWwindow* window) {
  if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
    glfwSetWindowShouldClose(window, true);
  }
}

GLFWwindow* glfw_init() {
  // 初始化 GLFW + GLAD，创建窗口与上下文
  if (!glfwInit()) {
    std::cerr << "Failed to init GLFW" << std::endl;
    return nullptr;
  }
  glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
  glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
  glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#if __APPLE__
  glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
  GLFWwindow* window = glfwCreateWindow(800, 600, "Tactile Module Test (GLFW)",
                                        nullptr, nullptr);
  if (window == nullptr) {
    std::cerr << "Failed to create GLFW window" << std::endl;
    glfwTerminate();
    return nullptr;
  }
  glfwMakeContextCurrent(window);
  if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
    std::cerr << "Failed to initialize GLAD" << std::endl;
    glfwDestroyWindow(window);
    glfwTerminate();
    return nullptr;
  }

  glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
  glfwSetMouseButtonCallback(window, mouse_button_callback);
  glfwSetCursorPosCallback(window, cursor_pos_callback);
  glfwSetScrollCallback(window, scroll_callback);

  glViewport(0, 0, 800, 600);
  glfwSwapInterval(1);
  return window;
}

int dot_count() {
  // 当前点阵总数
  return g_state.rows * g_state.cols;
}

void set_panel_size(float w, float h, float d) {
  g_state.panelW = w;
  g_state.panelH = h;
  g_state.panelD = d;
  panel_geometry_dirty = true;
}

void set_spec(int rows, int cols, float pitch, float dotRadius) {
  // 设置点阵规格，同时自动计算面板宽/深以留出圆点边界
  g_state.rows = std::max(0, rows);
  g_state.cols = std::max(0, cols);
  g_state.pitch = std::max(0.0f, pitch);
  g_state.dotRadius = std::max(0.0f, dotRadius);

  const float gridW =
      static_cast<float>(std::max(0, g_state.cols - 1)) * g_state.pitch;
  const float gridD =
      static_cast<float>(std::max(0, g_state.rows - 1)) * g_state.pitch;
  g_state.panelW = gridW + 2.0f * g_state.dotRadius;
  g_state.panelH = gridD + 2.0f * g_state.dotRadius;
  panel_geometry_dirty = true;
  dot_geometry_dirty = true;

  g_state.values.assign(dot_count(), static_cast<float>(g_state.minV));
  g_state.valuesDirty = true;
}

void init_background_geometry() {
  // 背景网格：全屏二三角形，顶点坐标直接在 NDC 空间
  if (bg_vbo) {
    glDeleteBuffers(1, &bg_vbo);
    bg_vbo = 0;
  }
  if (bg_vao) {
    glDeleteVertexArrays(1, &bg_vao);
    bg_vao = 0;
  }

  const float verts[] = {
      -1.0f, -1.0f, 1.0f, -1.0f, 1.0f,  1.0f,
      -1.0f, -1.0f, 1.0f, 1.0f,  -1.0f, 1.0f,
  };

  glGenVertexArrays(1, &bg_vao);
  glBindVertexArray(bg_vao);

  glGenBuffers(1, &bg_vbo);
  glBindBuffer(GL_ARRAY_BUFFER, bg_vbo);
  glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);

  glEnableVertexAttribArray(0);
  glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);

  glBindVertexArray(0);
}

void init_room_geometry() {
  // 房间：一个“倒扣的大盒子”（立方体），我们从里面看，所以法线朝向盒子内部
  if (room_ibo) {
    glDeleteBuffers(1, &room_ibo);
    room_ibo = 0;
  }
  if (room_vbo) {
    glDeleteBuffers(1, &room_vbo);
    room_vbo = 0;
  }
  if (room_vao) {
    glDeleteVertexArrays(1, &room_vao);
    room_vao = 0;
  }

  struct V {
    float x, y, z;
    float nx, ny, nz;
  };

  // 单位立方体：顶点范围 [-1, 1]，真正房间大小在 room.vert 里通过 uRoomHalfSize
  // 缩放 下面的法线是“朝内”的（方便在房间内部打光）
  V verts[24] = {
      // floor (y = -1), normal +Y
      {-1, -1, -1, 0, +1, 0},
      {+1, -1, -1, 0, +1, 0},
      {+1, -1, +1, 0, +1, 0},
      {-1, -1, +1, 0, +1, 0},

      // ceiling (y = +1), normal -Y
      {-1, +1, +1, 0, -1, 0},
      {+1, +1, +1, 0, -1, 0},
      {+1, +1, -1, 0, -1, 0},
      {-1, +1, -1, 0, -1, 0},

      // back wall (z = -1), normal +Z
      {-1, +1, -1, 0, 0, +1},
      {+1, +1, -1, 0, 0, +1},
      {+1, -1, -1, 0, 0, +1},
      {-1, -1, -1, 0, 0, +1},

      // front wall (z = +1), normal -Z
      {+1, +1, +1, 0, 0, -1},
      {-1, +1, +1, 0, 0, -1},
      {-1, -1, +1, 0, 0, -1},
      {+1, -1, +1, 0, 0, -1},

      // left wall (x = -1), normal +X
      {-1, +1, +1, +1, 0, 0},
      {-1, +1, -1, +1, 0, 0},
      {-1, -1, -1, +1, 0, 0},
      {-1, -1, +1, +1, 0, 0},

      // right wall (x = +1), normal -X
      {+1, +1, -1, -1, 0, 0},
      {+1, +1, +1, -1, 0, 0},
      {+1, -1, +1, -1, 0, 0},
      {+1, -1, -1, -1, 0, 0},
  };

  unsigned int idx[36] = {
      0,  1,  2,  0,  2,  3,   // floor
      4,  5,  6,  4,  6,  7,   // ceiling
      8,  9,  10, 8,  10, 11,  // back
      12, 13, 14, 12, 14, 15,  // front
      16, 17, 18, 16, 18, 19,  // left
      20, 21, 22, 20, 22, 23   // right
  };
  room_index_count = 36;

  glGenVertexArrays(1, &room_vao);
  glBindVertexArray(room_vao);

  glGenBuffers(1, &room_vbo);
  glBindBuffer(GL_ARRAY_BUFFER, room_vbo);
  glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);

  glGenBuffers(1, &room_ibo);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, room_ibo);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(idx), idx, GL_STATIC_DRAW);

  // layout 和 panel 一样：
  // location 0: position
  // location 1: normal
  glEnableVertexAttribArray(0);
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(V), (void*)0);
  glEnableVertexAttribArray(1);
  glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(V),
                        (void*)(3 * sizeof(float)));

  glBindVertexArray(0);
}

void init_panel_geometry() {
  // 面板盒子：VAO+VBO+IBO，包含顶面/底面/侧面 6 个面
  if (panel_ibo) {
    glDeleteBuffers(1, &panel_ibo);
    panel_ibo = 0;
  }
  if (panel_vbo) {
    glDeleteBuffers(1, &panel_vbo);
    panel_vbo = 0;
  }
  if (panel_vao) {
    glDeleteVertexArrays(1, &panel_vao);
    panel_vao = 0;
  }

  const float y = g_state.panelH * 0.5f;
  const float hw = g_state.panelW * 0.5f;
  const float hd = g_state.panelD * 0.5f;

  struct V {
    float x, y, z;
    float nx, ny, nz;
  };

  V verts[24] = {
      {-hw, +y, -hd, 0, +1, 0}, {+hw, +y, -hd, 0, +1, 0},
      {+hw, +y, +hd, 0, +1, 0}, {-hw, +y, +hd, 0, +1, 0},
      {-hw, +y, +hd, 0, 0, +1}, {+hw, +y, +hd, 0, 0, +1},
      {+hw, -y, +hd, 0, 0, +1}, {-hw, -y, +hd, 0, 0, +1},
      {-hw, -y, +hd, 0, -1, 0}, {+hw, -y, +hd, 0, -1, 0},
      {+hw, -y, -hd, 0, -1, 0}, {-hw, -y, -hd, 0, -1, 0},
      {+hw, +y, -hd, 0, 0, -1}, {-hw, +y, -hd, 0, 0, -1},
      {-hw, -y, -hd, 0, 0, -1}, {+hw, -y, -hd, 0, 0, -1},
      {-hw, +y, -hd, -1, 0, 0}, {-hw, +y, +hd, -1, 0, 0},
      {-hw, -y, +hd, -1, 0, 0}, {-hw, -y, -hd, -1, 0, 0},
      {+hw, +y, +hd, +1, 0, 0}, {+hw, +y, -hd, +1, 0, 0},
      {+hw, -y, -hd, +1, 0, 0}, {+hw, -y, +hd, +1, 0, 0},
  };

  unsigned int idx[36] = {0,  1,  2,  0,  2,  3,  4,  5,  6,  4,  6,  7,
                          8,  9,  10, 8,  10, 11, 12, 13, 14, 12, 14, 15,
                          16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23};
  panel_index_count = 36;

  glGenVertexArrays(1, &panel_vao);
  glBindVertexArray(panel_vao);

  glGenBuffers(1, &panel_vbo);
  glBindBuffer(GL_ARRAY_BUFFER, panel_vbo);
  glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);

  glGenBuffers(1, &panel_ibo);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, panel_ibo);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(idx), idx, GL_STATIC_DRAW);

  glEnableVertexAttribArray(0);
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(V), (void*)0);
  glEnableVertexAttribArray(1);
  glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(V),
                        (void*)(3 * sizeof(float)));
  glBindVertexArray(0);
}

void init_dot_geometry() {
  // 圆点：基于一个单位 quad，使用 instanced attributes 传位置/值
  if (instance_vbo) {
    glDeleteBuffers(1, &instance_vbo);
    instance_vbo = 0;
  }
  if (dots_vbo) {
    glDeleteBuffers(1, &dots_vbo);
    dots_vbo = 0;
  }
  if (dots_vao) {
    glDeleteVertexArrays(1, &dots_vao);
    dots_vao = 0;
  }

  struct V {
    float x, y;
    float u, v;
  };

  V quad[6] = {
      {-1, -1, 0, 0}, {1, -1, 1, 0}, {1, 1, 1, 1},
      {-1, -1, 0, 0}, {1, 1, 1, 1},  {-1, 1, 0, 1},
  };

  glGenVertexArrays(1, &dots_vao);
  glBindVertexArray(dots_vao);

  glGenBuffers(1, &dots_vbo);
  glBindBuffer(GL_ARRAY_BUFFER, dots_vbo);
  glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW);

  glEnableVertexAttribArray(0);
  glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(V), (void*)0);
  glEnableVertexAttribArray(1);
  glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(V),
                        (void*)(2 * sizeof(float)));

  glGenBuffers(1, &instance_vbo);
  glBindBuffer(GL_ARRAY_BUFFER, instance_vbo);
  glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 3 * std::max(1, dot_count()),
               nullptr, GL_DYNAMIC_DRAW);

  glEnableVertexAttribArray(2);
  glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
  glVertexAttribDivisor(2, 1);

  glEnableVertexAttribArray(3);
  glVertexAttribPointer(3, 1, GL_FLOAT, GL_FALSE, 3 * sizeof(float),
                        (void*)(2 * sizeof(float)));
  glVertexAttribDivisor(3, 1);

  glBindVertexArray(0);
}

float lerp_float(float a, float b, float t) { return a + (b - a) * t; }

float gaussian2d(float dx, float dy, float sigma) {
  const float s = std::max(1e-6f, sigma);
  const float inv = 1.0f / (2.0f * s * s);
  return std::exp(-(dx * dx + dy * dy) * inv);
}

float sample_height_value(float u, float v) {
    if (height_values.empty()) {
        return static_cast<float>(g_state.minV);
    }
    const float x = std::clamp(u, 0.0f, 1.0f) * static_cast<float>(kHeightW - 1);
    const float y = std::clamp(v, 0.0f, 1.0f) * static_cast<float>(kHeightH - 1);
    const int x0 = std::clamp(static_cast<int>(std::floor(x)), 0, kHeightW - 1);
    const int y0 = std::clamp(static_cast<int>(std::floor(y)), 0, kHeightH - 1);
    const int x1 = std::min(x0 + 1, kHeightW - 1);
    const int y1 = std::min(y0 + 1, kHeightH - 1);
    const float fx = x - static_cast<float>(x0);
    const float fy = y - static_cast<float>(y0);

    const float v00 = height_values[static_cast<size_t>(y0) * static_cast<size_t>(kHeightW) + static_cast<size_t>(x0)];
    const float v10 = height_values[static_cast<size_t>(y0) * static_cast<size_t>(kHeightW) + static_cast<size_t>(x1)];
    const float v01 = height_values[static_cast<size_t>(y1) * static_cast<size_t>(kHeightW) + static_cast<size_t>(x0)];
    const float v11 = height_values[static_cast<size_t>(y1) * static_cast<size_t>(kHeightW) + static_cast<size_t>(x1)];

    const float vx0 = lerp_float(v00, v10, fx);
    const float vx1 = lerp_float(v01, v11, fx);
    return lerp_float(vx0, vx1, fy);
}

void init_base_geometry() {
  if (base_ibo) {
    glDeleteBuffers(1, &base_ibo);
    base_ibo = 0;
  }
  if (base_vbo) {
    glDeleteBuffers(1, &base_vbo);
    base_vbo = 0;
  }
  if (base_vao) {
    glDeleteVertexArrays(1, &base_vao);
    base_vao = 0;
  }

  const float hw = kPlaneW * 0.5f;
  const float hh = kPlaneH * 0.5f;
  const float hz = kThickness * 0.5f;

  struct V {
    float x, y, z;
    float nx, ny, nz;
  };

  V verts[24] = {
      {-hw, -hh, +hz, 0, 0, +1}, {+hw, -hh, +hz, 0, 0, +1},
      {+hw, +hh, +hz, 0, 0, +1}, {-hw, +hh, +hz, 0, 0, +1},

      {+hw, -hh, -hz, 0, 0, -1}, {-hw, -hh, -hz, 0, 0, -1},
      {-hw, +hh, -hz, 0, 0, -1}, {+hw, +hh, -hz, 0, 0, -1},

      {-hw, -hh, -hz, -1, 0, 0}, {-hw, -hh, +hz, -1, 0, 0},
      {-hw, +hh, +hz, -1, 0, 0}, {-hw, +hh, -hz, -1, 0, 0},

      {+hw, -hh, +hz, +1, 0, 0}, {+hw, -hh, -hz, +1, 0, 0},
      {+hw, +hh, -hz, +1, 0, 0}, {+hw, +hh, +hz, +1, 0, 0},

      {-hw, +hh, +hz, 0, +1, 0}, {+hw, +hh, +hz, 0, +1, 0},
      {+hw, +hh, -hz, 0, +1, 0}, {-hw, +hh, -hz, 0, +1, 0},

      {-hw, -hh, -hz, 0, -1, 0}, {+hw, -hh, -hz, 0, -1, 0},
      {+hw, -hh, +hz, 0, -1, 0}, {-hw, -hh, +hz, 0, -1, 0},
  };

  unsigned int idx[6] = {0, 1, 2, 0, 2, 3};
  base_index_count = 6;

  glGenVertexArrays(1, &base_vao);
  glBindVertexArray(base_vao);

  glGenBuffers(1, &base_vbo);
  glBindBuffer(GL_ARRAY_BUFFER, base_vbo);
  glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);

  glGenBuffers(1, &base_ibo);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, base_ibo);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(idx), idx, GL_STATIC_DRAW);

  glEnableVertexAttribArray(0);
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(V), (void*)0);
  glEnableVertexAttribArray(1);
  glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(V),
                        (void*)(3 * sizeof(float)));

  glBindVertexArray(0);
}

void init_skirt_geometry() {
    if (skirt_ibo) {
        glDeleteBuffers(1, &skirt_ibo);
        skirt_ibo = 0;
    }
    if (skirt_vbo) {
        glDeleteBuffers(1, &skirt_vbo);
        skirt_vbo = 0;
    }
    if (skirt_vao) {
        glDeleteVertexArrays(1, &skirt_vao);
        skirt_vao = 0;
    }

    skirt_uvs.clear();
    skirt_normals.clear();

    const int cols = std::max(2, kMeshCols);
    const int rows = std::max(2, kMeshRows);

    auto push_border = [&](int c, int r, const glm::vec3& n) {
        const float u = (cols > 1) ? static_cast<float>(c) / static_cast<float>(cols - 1) : 0.0f;
        const float v = (rows > 1) ? static_cast<float>(r) / static_cast<float>(rows - 1) : 0.0f;
        skirt_uvs.emplace_back(u, v);
        skirt_normals.emplace_back(n);
    };

    for (int c = 0; c < cols; ++c) {
        push_border(c, 0, glm::vec3(0.0f, -1.0f, 0.0f));
    }
    for (int r = 1; r < rows - 1; ++r) {
        push_border(cols - 1, r, glm::vec3(1.0f, 0.0f, 0.0f));
    }
    for (int c = cols - 1; c >= 0; --c) {
        push_border(c, rows - 1, glm::vec3(0.0f, 1.0f, 0.0f));
    }
    for (int r = rows - 2; r >= 1; --r) {
        push_border(0, r, glm::vec3(-1.0f, 0.0f, 0.0f));
    }

    const int border_count = static_cast<int>(skirt_uvs.size());
    skirt_vertices.assign(static_cast<size_t>(border_count) * 2 * 6, 0.0f);

    std::vector<unsigned int> idx;
    idx.reserve(static_cast<size_t>(border_count) * 6);
    for (int i = 0; i < border_count; ++i) {
        const int next = (i + 1) % border_count;
        const unsigned int top0 = static_cast<unsigned int>(i * 2);
        const unsigned int bot0 = static_cast<unsigned int>(i * 2 + 1);
        const unsigned int top1 = static_cast<unsigned int>(next * 2);
        const unsigned int bot1 = static_cast<unsigned int>(next * 2 + 1);

        idx.push_back(top0);
        idx.push_back(top1);
        idx.push_back(bot1);
        idx.push_back(top0);
        idx.push_back(bot1);
        idx.push_back(bot0);
    }
    skirt_index_count = static_cast<int>(idx.size());

    glGenVertexArrays(1, &skirt_vao);
    glBindVertexArray(skirt_vao);

    glGenBuffers(1, &skirt_vbo);
    glBindBuffer(GL_ARRAY_BUFFER, skirt_vbo);
    glBufferData(GL_ARRAY_BUFFER, skirt_vertices.size() * sizeof(float), skirt_vertices.data(), GL_DYNAMIC_DRAW);

    glGenBuffers(1, &skirt_ibo);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, skirt_ibo);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, idx.size() * sizeof(unsigned int), idx.data(), GL_STATIC_DRAW);

    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));

    glBindVertexArray(0);
}

void init_heatmap_geometry() {
  if (heatmap_ibo) {
    glDeleteBuffers(1, &heatmap_ibo);
    heatmap_ibo = 0;
  }
  if (heatmap_vbo) {
    glDeleteBuffers(1, &heatmap_vbo);
    heatmap_vbo = 0;
  }
  if (heatmap_vao) {
    glDeleteVertexArrays(1, &heatmap_vao);
    heatmap_vao = 0;
  }

  const int cols = std::max(2, kMeshCols);
  const int rows = std::max(2, kMeshRows);
  const int vertex_count = cols * rows;
  std::vector<float> verts;
  verts.reserve(vertex_count * 5);

  for (int r = 0; r < rows; ++r) {
    const float v = (rows > 1)
                        ? static_cast<float>(r) / static_cast<float>(rows - 1)
                        : 0.0f;
    const float y = (v - 0.5f) * kPlaneH;
    for (int c = 0; c < cols; ++c) {
      const float u = (cols > 1)
                          ? static_cast<float>(c) / static_cast<float>(cols - 1)
                          : 0.0f;
      const float x = (u - 0.5f) * kPlaneW;
      verts.push_back(x);
      verts.push_back(y);
      verts.push_back(0.0f);
      verts.push_back(u);
      verts.push_back(v);
    }
  }

  std::vector<unsigned int> idx;
  idx.reserve((cols - 1) * (rows - 1) * 6);
  for (int r = 0; r < rows - 1; ++r) {
    for (int c = 0; c < cols - 1; ++c) {
      const unsigned int i0 = static_cast<unsigned int>(r * cols + c);
      const unsigned int i1 = static_cast<unsigned int>(r * cols + c + 1);
      const unsigned int i2 = static_cast<unsigned int>((r + 1) * cols + c + 1);
      const unsigned int i3 = static_cast<unsigned int>((r + 1) * cols + c);
      idx.push_back(i0);
      idx.push_back(i1);
      idx.push_back(i2);
      idx.push_back(i0);
      idx.push_back(i2);
      idx.push_back(i3);
    }
  }
  heatmap_index_count = static_cast<int>(idx.size());

  glGenVertexArrays(1, &heatmap_vao);
  glBindVertexArray(heatmap_vao);

  glGenBuffers(1, &heatmap_vbo);
  glBindBuffer(GL_ARRAY_BUFFER, heatmap_vbo);
  glBufferData(GL_ARRAY_BUFFER, verts.size() * sizeof(float), verts.data(),
               GL_STATIC_DRAW);

  glGenBuffers(1, &heatmap_ibo);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, heatmap_ibo);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, idx.size() * sizeof(unsigned int),
               idx.data(), GL_STATIC_DRAW);

  glEnableVertexAttribArray(0);
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
  glEnableVertexAttribArray(1);
  glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float),
                        (void*)(3 * sizeof(float)));

  glBindVertexArray(0);
}

void init_height_texture() {
  if (height_tex) {
    glDeleteTextures(1, &height_tex);
    height_tex = 0;
  }
  if (height_values.size() !=
      static_cast<size_t>(kHeightW) * static_cast<size_t>(kHeightH)) {
    height_values.assign(
        static_cast<size_t>(kHeightW) * static_cast<size_t>(kHeightH), 0.0f);
  }

  glGenTextures(1, &height_tex);
  glBindTexture(GL_TEXTURE_2D, height_tex);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F, kHeightW, kHeightH, 0, GL_RED,
               GL_FLOAT, height_values.data());
    glBindTexture(GL_TEXTURE_2D, 0);
}

void update_skirt_vertices() {
    if (skirt_vertices.empty() || skirt_uvs.empty()) {
        return;
    }
    const float baseZ = -kThickness * 0.5f;
    const float backZ = baseZ + kThickness;
    const float minV = static_cast<float>(g_state.minV);
    const float maxV = static_cast<float>(g_state.maxV);
    const float invRange = 1.0f / std::max(1e-6f, (maxV - minV));

    const int border_count = static_cast<int>(skirt_uvs.size());
    for (int i = 0; i < border_count; ++i) {
        const float u = skirt_uvs[i].x;
        const float v = skirt_uvs[i].y;
        const float value = sample_height_value(u, v);
        const float t = std::clamp((value - minV) * invRange, 0.0f, 1.0f);
        const float h = t * kDentMax;

        const float x = (u - 0.5f) * kPlaneW;
        const float y = (v - 0.5f) * kPlaneH;
        const glm::vec3 n = skirt_normals[i];

        const int topIndex = i * 2;
        const int botIndex = i * 2 + 1;

        const size_t topBase = static_cast<size_t>(topIndex) * 6;
        skirt_vertices[topBase + 0] = x;
        skirt_vertices[topBase + 1] = y;
        skirt_vertices[topBase + 2] = baseZ + h;
        skirt_vertices[topBase + 3] = n.x;
        skirt_vertices[topBase + 4] = n.y;
        skirt_vertices[topBase + 5] = n.z;

        const size_t botBase = static_cast<size_t>(botIndex) * 6;
        skirt_vertices[botBase + 0] = x;
        skirt_vertices[botBase + 1] = y;
        skirt_vertices[botBase + 2] = backZ;
        skirt_vertices[botBase + 3] = n.x;
        skirt_vertices[botBase + 4] = n.y;
        skirt_vertices[botBase + 5] = n.z;
    }

    glBindBuffer(GL_ARRAY_BUFFER, skirt_vbo);
    glBufferSubData(GL_ARRAY_BUFFER, 0, skirt_vertices.size() * sizeof(float), skirt_vertices.data());
}

void update_demo_heightmap(float t) {
  if (low_values.size() !=
      static_cast<size_t>(kLowRows) * static_cast<size_t>(kLowCols)) {
    low_values.assign(
        static_cast<size_t>(kLowRows) * static_cast<size_t>(kLowCols), 0.0f);
  }
  if (height_values.size() !=
      static_cast<size_t>(kHeightW) * static_cast<size_t>(kHeightH)) {
    height_values.assign(
        static_cast<size_t>(kHeightW) * static_cast<size_t>(kHeightH), 0.0f);
  }

  const float minV = static_cast<float>(g_state.minV);
  const float maxV = static_cast<float>(g_state.maxV);
  const float range = std::max(1.0f, maxV - minV);

  const float t1 = t * 0.25f;
  const float t2 = t * 0.18f;
  const float t3 = t * 0.12f;

  const float cx1 = 0.30f + 0.12f * std::sin(t1);
  const float cy1 = 0.50f + 0.10f * std::cos(t1 * 0.9f);
  const float cx2 = 0.72f + 0.10f * std::cos(t2 * 1.1f);
  const float cy2 = 0.36f + 0.10f * std::sin(t2 * 0.8f);
  const float cx3 = 0.50f + 0.05f * std::sin(t3);
  const float cy3 = 0.72f + 0.05f * std::cos(t3 * 1.2f);

  const float a1 = 0.70f + 0.15f * std::sin(t * 0.20f);
  const float a2 = 0.55f + 0.20f * std::cos(t * 0.16f);
  const float a3 = 0.25f + 0.10f * std::sin(t * 0.13f);

  const float s1 = 0.18f;
  const float s2 = 0.12f;
  const float s3 = 0.10f;

  for (int r = 0; r < kLowRows; ++r) {
    const float v = (kLowRows > 1) ? static_cast<float>(r) /
                                         static_cast<float>(kLowRows - 1)
                                   : 0.0f;
    for (int c = 0; c < kLowCols; ++c) {
      const float u = (kLowCols > 1) ? static_cast<float>(c) /
                                           static_cast<float>(kLowCols - 1)
                                     : 0.0f;
      const float g1 = gaussian2d(u - cx1, v - cy1, s1);
      const float g2 = gaussian2d(u - cx2, v - cy2, s2);
      const float g3 = gaussian2d(u - cx3, v - cy3, s3);
      float value01 = 0.05f + a1 * g1 + a2 * g2 + a3 * g3;
      value01 = std::clamp(value01, 0.0f, 1.0f);
      low_values[static_cast<size_t>(r) * static_cast<size_t>(kLowCols) +
                 static_cast<size_t>(c)] = minV + range * value01;
    }
  }

  const float invHighW =
      (kHeightW > 1) ? (1.0f / static_cast<float>(kHeightW - 1)) : 0.0f;
  const float invHighH =
      (kHeightH > 1) ? (1.0f / static_cast<float>(kHeightH - 1)) : 0.0f;
  const float lowW = static_cast<float>(kLowCols - 1);
  const float lowH = static_cast<float>(kLowRows - 1);

  for (int y = 0; y < kHeightH; ++y) {
    const float gy = static_cast<float>(y) * invHighH * lowH;
    const int y0 = std::clamp(static_cast<int>(gy), 0, kLowRows - 1);
    const int y1 = std::min(y0 + 1, kLowRows - 1);
    const float fy = gy - static_cast<float>(y0);
    for (int x = 0; x < kHeightW; ++x) {
      const float gx = static_cast<float>(x) * invHighW * lowW;
      const int x0 = std::clamp(static_cast<int>(gx), 0, kLowCols - 1);
      const int x1 = std::min(x0 + 1, kLowCols - 1);
      const float fx = gx - static_cast<float>(x0);

      const float v00 =
          low_values[static_cast<size_t>(y0) * static_cast<size_t>(kLowCols) +
                     static_cast<size_t>(x0)];
      const float v10 =
          low_values[static_cast<size_t>(y0) * static_cast<size_t>(kLowCols) +
                     static_cast<size_t>(x1)];
      const float v01 =
          low_values[static_cast<size_t>(y1) * static_cast<size_t>(kLowCols) +
                     static_cast<size_t>(x0)];
      const float v11 =
          low_values[static_cast<size_t>(y1) * static_cast<size_t>(kLowCols) +
                     static_cast<size_t>(x1)];

      const float vx0 = lerp_float(v00, v10, fx);
      const float vx1 = lerp_float(v01, v11, fx);
      const float vxy = lerp_float(vx0, vx1, fy);
      height_values[static_cast<size_t>(y) * static_cast<size_t>(kHeightW) +
                    static_cast<size_t>(x)] = vxy;
    }
  }
  height_dirty = true;
}

void upload_height_texture_if_needed() {
  if (!height_dirty || !height_tex) {
    return;
  }
  glBindTexture(GL_TEXTURE_2D, height_tex);
  glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
  glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kHeightW, kHeightH, GL_RED, GL_FLOAT,
                  height_values.data());
  glBindTexture(GL_TEXTURE_2D, 0);
  height_dirty = false;
}

void render_heatmap() {
  if (!heatmap_shader || !heatmap_vao || heatmap_index_count <= 0 ||
      !height_tex) {
    return;
  }

  heatmap_shader->use();
  heatmap_shader->setMat4("uMVP", g_state.mvp);
  heatmap_shader->setFloat("uMinV", static_cast<float>(g_state.minV));
  heatmap_shader->setFloat("uMaxV", static_cast<float>(g_state.maxV));
  heatmap_shader->setFloat("uHeightScale", kDentMax);
  heatmap_shader->setFloat("uBaseZ", -kThickness * 0.5f);
  heatmap_shader->setVec2("uTexelSize",
                          glm::vec2(1.0f / static_cast<float>(kHeightW),
                                    1.0f / static_cast<float>(kHeightH)));
  heatmap_shader->setVec2("uPlaneSize", glm::vec2(kPlaneW, kPlaneH));
  heatmap_shader->setVec3("uCameraPos", g_state.cameraPos);
  heatmap_shader->setVec3("uLightDir",
                          glm::normalize(glm::vec3(-0.25f, 0.35f, 1.0f)));
  heatmap_shader->setInt("uHeightTex", 0);

  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_2D, height_tex);

  glBindVertexArray(heatmap_vao);
  glDrawElements(GL_TRIANGLES, heatmap_index_count, GL_UNSIGNED_INT, nullptr);
  glBindVertexArray(0);

  glBindTexture(GL_TEXTURE_2D, 0);
}

// void init_dot_texture() {
//     // 简单 4x4 RGBA 程序化纹理，模拟金属纹理感
//     if (dot_tex) {
//         glDeleteTextures(1, &dot_tex);
//         dot_tex = 0;
//     }

//     // Simple procedural 4x4 texture to mimic a brushed metal feel.
//     const unsigned char pixels[] = {
//         180, 175, 170, 255, 185, 180, 175, 255, 190, 185, 180, 255, 185, 180,
//         175, 255, 185, 180, 175, 255, 190, 185, 180, 255, 195, 190, 185, 255,
//         190, 185, 180, 255, 190, 185, 180, 255, 195, 190, 185, 255, 200, 195,
//         190, 255, 195, 190, 185, 255, 185, 180, 175, 255, 190, 185, 180, 255,
//         195, 190, 185, 255, 190, 185, 180, 255,
//     };

//     glGenTextures(1, &dot_tex);
//     glBindTexture(GL_TEXTURE_2D, dot_tex);
//     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
//     GL_LINEAR_MIPMAP_LINEAR); glTexParameteri(GL_TEXTURE_2D,
//     GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D,
//     GL_TEXTURE_WRAP_S,GL_LINEAR_MIPMAP_LINEAR);
//     glTexParameteri(GL_TEXTURE_2D,
//     GL_TEXTURE_WRAP_T,GL_LINEAR_MIPMAP_LINEGL_LINEARAR);
//     glPixelStorei(GL_UNPACK_ALIGNMENT, 1GL_LINEARGL_CLAMP_TO_EDGE
//     glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 40, GL_RGBA,
//     GL_UNSIGNED_BYTE, pixels); glGenerateMipmap(GL_TEXTURE_GL_CLAMP_TO_EDGE2D
// glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
//     glBindTexture(GL_TEXTURE_2D, 0);
// }

void init_programs() {
  // 加载并编译 shader program
  const std::string vsb_path = "../shaders/bg.vert";
  const std::string fsb_path = "../shaders/bg.frag";
  const std::string vsbase_path = "../shaders/base.vert";
  const std::string fsbase_path = "../shaders/base.frag";
  const std::string vsh_path = "../shaders/heatmap.vert";
  const std::string fsh_path = "../shaders/heatmap.frag";

  bg_shader = new Shader(vsb_path.c_str(), fsb_path.c_str());
  base_shader = new Shader(vsbase_path.c_str(), fsbase_path.c_str());
  heatmap_shader = new Shader(vsh_path.c_str(), fsh_path.c_str());
}

void update_instance_buffer_if_needed() {
  // 如果有新数据，重新填充实例缓冲：每个点 3 个 float (x,z,value)
  if (dot_count() <= 0) {
    instance_count = 0;
    return;
  }
  if (!g_state.valuesDirty) return;

  const int n = dot_count();
  instance_count = n;
  std::vector<float> inst;
  inst.resize(n * 3);

  const float w =
      static_cast<float>(std::max(0, g_state.cols - 1)) * g_state.pitch;
  const float h =
      static_cast<float>(std::max(0, g_state.rows - 1)) * g_state.pitch;
  for (int i = 0; i < n; ++i) {
    const int r = (g_state.cols > 0) ? (i / g_state.cols) : 0;
    const int c = (g_state.cols > 0) ? (i % g_state.cols) : 0;

    const float x = (static_cast<float>(c) * g_state.pitch) - w * 0.5f;
    const float y = (static_cast<float>(r) * g_state.pitch) - h * 0.5f;
    inst[i * 3 + 0] = x;
    inst[i * 3 + 1] = y;
    inst[i * 3 + 2] = (i < static_cast<int>(g_state.values.size()))
                          ? g_state.values[i]
                          : static_cast<float>(g_state.minV);
  }

  glBindBuffer(GL_ARRAY_BUFFER, instance_vbo);
  glBufferSubData(GL_ARRAY_BUFFER, 0, inst.size() * sizeof(float), inst.data());
  g_state.valuesDirty = false;
}

void update_matrices(int fbWidth, int fbHeight) {
  // 计算 MVP：透视投影 * 观察（环绕相机）* 单位模型
  if (fbWidth <= 0 || fbHeight <= 0) {
    g_state.mvp = glm::mat4(1.0f);
    return;
  }

  const float aspect =
      static_cast<float>(fbWidth) / static_cast<float>(fbHeight);
  const float radius = 0.5f * std::sqrt(kPlaneW * kPlaneW + kPlaneH * kPlaneH);
  const float distance = std::max(0.6f, radius * 2.4f);

  // 目标点：面板中心
  const glm::vec3 center(0.0f, 0.0f, 0.0f);
  const float yawRad = glm::radians(g_state.camYaw);
  const float pitchRad = glm::radians(g_state.camPitch);
  const float cosPitch = std::cos(pitchRad);

  const glm::vec3 eye =
      center + glm::vec3(distance * cosPitch * std::cos(yawRad),
                         distance * std::sin(pitchRad),
                         distance * cosPitch * std::sin(yawRad));
  g_state.cameraPos = eye;

  // 构建视图矩阵与透视矩阵
  const glm::mat4 view = glm::lookAt(eye, center, glm::vec3(0.0f, 1.0f, 0.0f));
  const glm::mat4 proj =
      glm::perspective(glm::radians(g_state.zoom), aspect, 0.01f,
                       std::max(10.0f, distance * 10.0f));
  g_state.mvp = proj * view;
}

void render_background(int fbWidth, int fbHeight) {
  // 屏幕空间网格，不受相机旋转影响
  if (!bg_shader || !bg_vao) return;

  glDisable(GL_DEPTH_TEST);
  glDepthMask(GL_FALSE);

  bg_shader->use();
  bg_shader->setVec2("uViewport", glm::vec2(static_cast<float>(fbWidth),
                                            static_cast<float>(fbHeight)));
  bg_shader->setFloat("uMinorStep", 24.0f);
  bg_shader->setFloat("uMajorStep", 120.0f);

  glBindVertexArray(bg_vao);
  glDrawArrays(GL_TRIANGLES, 0, 6);
  glBindVertexArray(0);

  glDepthMask(GL_TRUE);
  glEnable(GL_DEPTH_TEST);
}

void render_base() {
  if (!base_shader || !base_vao) {
    return;
  }

  base_shader->use();
  base_shader->setMat4("uMVP", g_state.mvp);
  base_shader->setVec3("uCameraPos", g_state.cameraPos);
  base_shader->setVec3("uLightDir",
                       glm::normalize(glm::vec3(-0.25f, 0.35f, 1.0f)));
  base_shader->setVec3("uColor", glm::vec3(0.10f, 0.15f, 0.85f));

  glBindVertexArray(base_vao);
  glDrawElements(GL_TRIANGLES, base_index_count, GL_UNSIGNED_INT, nullptr);
    glBindVertexArray(0);
}

void render_skirt() {
    if (!base_shader || !skirt_vao || skirt_index_count <= 0) {
        return;
    }

    base_shader->use();
    base_shader->setMat4("uMVP", g_state.mvp);
    base_shader->setVec3("uCameraPos", g_state.cameraPos);
    base_shader->setVec3("uLightDir", glm::normalize(glm::vec3(-0.25f, 0.35f, 1.0f)));
    base_shader->setVec3("uColor", glm::vec3(0.10f, 0.15f, 0.85f));

    glBindVertexArray(skirt_vao);
    glDrawElements(GL_TRIANGLES, skirt_index_count, GL_UNSIGNED_INT, nullptr);
    glBindVertexArray(0);
}

void render_room() {
  // 3D 房间背景：用一个大盒子把场景包起来（相当于“屋子墙壁/地面”）
  if (!room_shader || !room_vao) return;

  // 房间尺寸：根据面板尺寸做一个“够大”的包围盒（单位：世界坐标）
  const float base = std::max({g_state.panelW, g_state.panelH, g_state.panelD});
  const glm::vec3 roomHalfSize(
      std::max(1.0f, base * 1.1f),  // X: 左右墙离中心的距离
      std::max(1.0f, base * 1.1f),  // Y: 地面/天花板离中心的距离
      std::max(1.0f, base * 1.1f)   // Z: 前后墙离中心的距离
  );

  // 网格尺寸（让房间更有空间感）
  const float minorStep = std::max(0.05f, base * 0.25f);
  const float majorStep = minorStep * 5.0f;

  room_shader->use();
  room_shader->setMat4("uMVP", g_state.mvp);
  room_shader->setVec3("uCameraPos", g_state.cameraPos);
  room_shader->setVec3("uRoomHalfSize", roomHalfSize);
  room_shader->setFloat("uMinorStep", minorStep);
  room_shader->setFloat("uMajorStep", majorStep);
  room_shader->setInt("uRenderMode", g_state.renderMode);

  glBindVertexArray(room_vao);
  glDrawElements(GL_TRIANGLES, room_index_count, GL_UNSIGNED_INT, nullptr);
  glBindVertexArray(0);
}

void render_panel() {
  // 绘制面板盒子
  if (!panel_shader || !panel_vao) return;

  panel_shader->use();
  panel_shader->setMat4("uMVP", g_state.mvp);
  panel_shader->setVec3("uCameraPos", g_state.cameraPos);
  panel_shader->setFloat("uPanelW", g_state.panelW);
  panel_shader->setFloat("uPanelH", g_state.panelH);
  panel_shader->setFloat("uPanelD", g_state.panelD);
  panel_shader->setInt("uRows", g_state.rows);
  panel_shader->setInt("uCols", g_state.cols);
  panel_shader->setFloat("uPitch", g_state.pitch);
  panel_shader->setFloat("uDotRadius", g_state.dotRadius);
  panel_shader->setInt("uRenderMode", g_state.renderMode);

  glBindVertexArray(panel_vao);
  glDrawElements(GL_TRIANGLES, panel_index_count, GL_UNSIGNED_INT, nullptr);
  glBindVertexArray(0);
}

void render_dots() {
  // 实例化绘制圆点
  if (!dots_shader || !dots_vao || instance_count <= 0) return;

  dots_shader->use();
  dots_shader->setMat4("uMVP", g_state.mvp);
  dots_shader->setInt("uRenderMode", g_state.renderMode);
  dots_shader->setFloat("uDotRadius", g_state.dotRadius);
  dots_shader->setFloat("uBaseZ", -(g_state.panelD * 0.5f) - 0.001f);
  dots_shader->setFloat("uMinV", static_cast<float>(g_state.minV));
  dots_shader->setFloat("uMaxV", static_cast<float>(g_state.maxV));
  dots_shader->setInt("uHasData", 1);
  dots_shader->setVec3("uCameraPos", g_state.cameraPos);
  dots_shader->setInt("uDotTex", 0);
  if (dot_tex) {
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, dot_tex);
  }

  glBindVertexArray(dots_vao);
  glDrawArraysInstanced(GL_TRIANGLES, 0, 6, instance_count);
  glBindVertexArray(0);

  if (dot_tex) {
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, 0);
  }
}

void update_demo_values(float t) {
  // 生成演示用波纹数据，保持实例缓冲在动
  const int n = dot_count();
  if (n <= 0) {
    instance_count = 0;
    return;
  }
  if (static_cast<int>(g_state.values.size()) != n) {
    g_state.values.assign(n, static_cast<float>(g_state.minV));
  }
  for (int i = 0; i < n; ++i) {
    const float phase = t * 0.6f + static_cast<float>(i) * 0.35f;
    const float wave = 0.5f + 0.5f * std::sin(phase);
    const float minV = static_cast<float>(g_state.minV);
    const float maxV = static_cast<float>(g_state.maxV);
    g_state.values[i] = minV + (maxV - minV) * wave;
  }
  g_state.valuesDirty = true;
}

void destroy_context() {
  delete bg_shader;
  delete base_shader;
  delete room_shader;
  delete panel_shader;
  delete dots_shader;
  delete heatmap_shader;
  if (room_vao) glDeleteVertexArrays(1, &room_vao);
  if (room_vbo) glDeleteBuffers(1, &room_vbo);
  if (room_ibo) glDeleteBuffers(1, &room_ibo);
  if (panel_vao) glDeleteVertexArrays(1, &panel_vao);
  if (panel_vbo) glDeleteBuffers(1, &panel_vbo);
  if (panel_ibo) glDeleteBuffers(1, &panel_ibo);
  if (dots_vao) glDeleteVertexArrays(1, &dots_vao);
  if (dots_vbo) glDeleteBuffers(1, &dots_vbo);
  if (instance_vbo) glDeleteBuffers(1, &instance_vbo);
  if (bg_vao) glDeleteVertexArrays(1, &bg_vao);
  if (bg_vbo) glDeleteBuffers(1, &bg_vbo);
    if (dot_tex) glDeleteTextures(1, &dot_tex);
    if (base_vao) glDeleteVertexArrays(1, &base_vao);
    if (base_vbo) glDeleteBuffers(1, &base_vbo);
    if (base_ibo) glDeleteBuffers(1, &base_ibo);
    if (skirt_vao) glDeleteVertexArrays(1, &skirt_vao);
    if (skirt_vbo) glDeleteBuffers(1, &skirt_vbo);
    if (skirt_ibo) glDeleteBuffers(1, &skirt_ibo);
    if (heatmap_vao) glDeleteVertexArrays(1, &heatmap_vao);
    if (heatmap_vbo) glDeleteBuffers(1, &heatmap_vbo);
    if (heatmap_ibo) glDeleteBuffers(1, &heatmap_ibo);
  if (height_tex) glDeleteTextures(1, &height_tex);
}

void load_metal_texture(const std::string& path) {
  if (metal_tex) {
    glDeleteTextures(1, &metal_tex);
    metal_tex = 0;
  }
  glGenTextures(1, &metal_tex);
  glBindTexture(GL_TEXTURE_2D, metal_tex);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
                  GL_LINEAR_MIPMAP_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
  int width, height, channels;
  unsigned char* data = stbi_load(path.c_str(), &width, &height, &channels, 0);
  if (data) {
    std::cout << "load texture image path: " << path << std::endl;
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB,
                 GL_UNSIGNED_BYTE, data);
    glGenerateMipmap(GL_TEXTURE_2D);
    glBindTexture(GL_TEXTURE_2D, 0);
  }
  stbi_image_free(data);
}

int main() {
  // 初始化窗口/上下文
  GLFWwindow* window = glfw_init();
  if (window == nullptr) {
    return -1;
  }

  glEnable(GL_DEPTH_TEST);
  glDepthFunc(GL_LESS);

  // 初始化规格与资源
  init_programs();
    init_background_geometry();
    init_base_geometry();
    init_heatmap_geometry();
    init_skirt_geometry();
    update_demo_heightmap(0.0f);
    init_height_texture();
    update_skirt_vertices();
  while (!glfwWindowShouldClose(window)) {
    const float currentFrame = static_cast<float>(glfwGetTime());
    process_input(window);

    int fbWidth = 0, fbHeight = 0;
    glfwGetFramebufferSize(window, &fbWidth, &fbHeight);
    glViewport(0, 0, fbWidth, fbHeight);
    glClearColor(0.06f, 0.07f, 0.09f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        update_matrices(fbWidth, fbHeight);
        update_demo_heightmap(currentFrame);
        upload_height_texture_if_needed();
        update_skirt_vertices();

        render_background(fbWidth, fbHeight);
        render_base();
        render_skirt();
        render_heatmap();

    glfwSwapBuffers(window);
    glfwPollEvents();
  }

  destroy_context();
  glfwDestroyWindow(window);
  glfwTerminate();
  return 0;
}
