//------------------------------------------------------------------------------
//  imgui-glfw
//  Demonstrates basic integration with Dear Imgui (without custom
//  texture or custom font support).
//------------------------------------------------------------------------------

#include "imgui.h"
#define SOKOL_IMPL
#define SOKOL_GLCORE33
#include "sokol_gfx.h"
#include "sokol_time.h"
#define SOKOL_GL_IMPL
#include "util/sokol_gl.h"
#define GLFW_INCLUDE_NONE
#include <iostream>

#include "Camera.h"
#include "SkinnedMesh.h"
#include "GLFW/glfw3.h"

const int Width = 1024;
const int Height = 768;
const int MaxVertices = (1 << 16);
const int MaxIndices = MaxVertices * 3;

uint64_t last_time = 0;
bool show_imgui_demo_window = false;
bool show_another_window = false;

sg_pass_action pass_action;
sg_pipeline pip;
sg_bindings bind;

typedef struct {
  ImVec2 disp_size;
} vs_params_t;

static void draw_imgui(ImDrawData*);

#define HANDMADE_MATH_IMPLEMENTATION
#define HANDMADE_MATH_NO_SSE
#include "HandmadeMath.h"
#include "camera.h"

// ozz-animation headers
#include <cmath>   // fmodf
#include <memory>  // std::unique_ptr, std::make_unique

#include "AnimationController.h"
#include "ozz/animation/runtime/animation.h"
#include "ozz/animation/runtime/local_to_model_job.h"
#include "ozz/animation/runtime/sampling_job.h"
#include "ozz/animation/runtime/skeleton.h"
#include "ozz/base/containers/vector.h"
#include "ozz/base/io/archive.h"
#include "ozz/base/io/stream.h"
#include "ozz/base/log.h"
#include "ozz/base/maths/soa_transform.h"
#include "ozz/base/maths/vec_float.h"

// wrapper struct for managed ozz-animation C++ objects, mUST BE DEleted
// before shutdown, otherwise ozz-animation will report a memory leak
typedef struct {
  ozz::animation::Skeleton skeleton;
  ozz::animation::Animation animation;
  ozz::animation::SamplingCache cache;
  ozz::vector<ozz::math::SoaTransform> local_matrices;
  ozz::vector<ozz::math::Float4x4> model_matrices;
} ozz_t;

static struct {
  std::unique_ptr<ozz_t> ozz;
  sg_pass_action pass_action;
  Camera camera;
  struct {
    bool skeleton;
    bool animation;
    bool failed;
  } loaded;
  struct {
    double frame;
    double absolute;
    uint64_t laptime;
    float factor;
    float anim_ratio;
    bool anim_ratio_ui_override;
    bool paused;
  } time;
} state;

typedef struct {
  int32_t mousedX;
  int32_t mousedY;
  int32_t mouseX;
  int32_t mouseY;
  uint8_t mouseButton;
  int32_t mouseScroll;
  char key;
} GuiInputState;

GuiInputState gGuiInputState = {0, 0, 0, 0, 0, 0, 0};

enum class ControlMode {
  ControlModeNone,
  ControlModeFPS
};

ControlMode gControlMode = ControlMode::ControlModeNone;

// io buffers for skeleton and animation data files, we know the max file size upfront
static uint8_t skel_data_buffer[4 * 1024];
static uint8_t anim_data_buffer[32 * 1024];

static void draw_grid(void);
static void draw_ui(void);
// static void skeleton_data_loaded(const sfetch_response_t* response);
// static void animation_data_loaded(const sfetch_response_t* response);
static void frame(void);

void handle_mouse(GLFWwindow* w, GuiInputState* io_input_state) {
  if (!glfwGetWindowAttrib(w, GLFW_FOCUSED)) {
    return;
  }

  double mouse_x, mouse_y;
  glfwGetCursorPos(w, &mouse_x, &mouse_y);

  if (io_input_state->mouseButton) {
    io_input_state->mousedX = int32_t(mouse_x) - io_input_state->mouseX;
    io_input_state->mousedY = int32_t(mouse_y) - io_input_state->mouseY;
  } else {
    io_input_state->mousedX = 0;
    io_input_state->mousedY = 0;
  }
  io_input_state->mouseX = int32_t(mouse_x);
  io_input_state->mouseY = int32_t(mouse_y);

  io_input_state->mouseButton = glfwGetMouseButton(w, 0)
                                + (glfwGetMouseButton(w, 1) << 1)
                                + (glfwGetMouseButton(w, 2) << 2);
}

int main() {
  // window and GL context via GLFW and flextGL
  glfwInit();
  glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
  glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
  glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
  glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
  glfwWindowHint(GLFW_COCOA_RETINA_FRAMEBUFFER, GLFW_FALSE);
  glfwWindowHint(GLFW_SAMPLES, 16);
  GLFWwindow* w = glfwCreateWindow(Width, Height, "AnimTestbed", 0, 0);
  glfwMakeContextCurrent(w);
  glfwSwapInterval(1);

  // GLFW to ImGui input forwarding
  glfwSetMouseButtonCallback(
      w,
      [](GLFWwindow*, int btn, int action, int /*mods*/) {
        if ((btn >= 0) && (btn < 3)) {
          ImGui::GetIO().MouseDown[btn] = (action == GLFW_PRESS);
        }
      });
  glfwSetCursorPosCallback(w, [](GLFWwindow*, double pos_x, double pos_y) {
    ImGui::GetIO().MousePos.x = float(pos_x);
    ImGui::GetIO().MousePos.y = float(pos_y);
  });
  glfwSetScrollCallback(w, [](GLFWwindow*, double /*pos_x*/, double pos_y) {
    ImGui::GetIO().MouseWheel = float(pos_y);
  });
  glfwSetKeyCallback(
      w,
      [](GLFWwindow*, int key, int /*scancode*/, int action, int mods) {
        ImGuiIO& io = ImGui::GetIO();
        if ((key >= 0) && (key < 512)) {
          io.KeysDown[key] = (action == GLFW_PRESS) || (action == GLFW_REPEAT);
        }
        io.KeyCtrl = (0 != (mods & GLFW_MOD_CONTROL));
        io.KeyAlt = (0 != (mods & GLFW_MOD_ALT));
        io.KeyShift = (0 != (mods & GLFW_MOD_SHIFT));
      });
  glfwSetCharCallback(w, [](GLFWwindow*, unsigned int codepoint) {
    ImGui::GetIO().AddInputCharacter((ImWchar)codepoint);
  });

  // setup sokol_gfx and sokol_time
  stm_setup();
  sg_desc desc = {};
  sg_setup(&desc);
  assert(sg_isvalid());

  // setup sokol-gl
  sgl_desc_t sgldesc = {};
  sgldesc.sample_count = 0;
  sgl_setup(&sgldesc);

  printf ("default allocator: 0x%p\n", (void*)ozz::memory::default_allocator());
  SkinnedMesh skinned_mesh;
  skinned_mesh.LoadSkeleton("../media/MixamoYBot-skeleton.ozz");
  skinned_mesh.LoadAnimation("../media/Idle-loop.ozz");
  skinned_mesh.LoadAnimation("../media/Walking-loop.ozz");
  skinned_mesh.LoadAnimation("../media/RunningSlow-loop.ozz");
  skinned_mesh.LoadAnimation("../media/RunningFast-loop.ozz");
  skinned_mesh.SetCurrentAnimation(0);

  AnimationController animation_controller (&skinned_mesh);

//  state.ozz = std::make_unique<ozz_t>();
  state.time.factor = 1.0f;

  Camera_Init(&state.camera);

  // setup Dear Imgui
  ImGui::CreateContext();
  ImGui::StyleColorsDark();
  ImGuiIO& io = ImGui::GetIO();
  io.IniFilename = nullptr;
  io.Fonts->AddFontDefault();
  io.KeyMap[ImGuiKey_Tab] = GLFW_KEY_TAB;
  io.KeyMap[ImGuiKey_LeftArrow] = GLFW_KEY_LEFT;
  io.KeyMap[ImGuiKey_RightArrow] = GLFW_KEY_RIGHT;
  io.KeyMap[ImGuiKey_UpArrow] = GLFW_KEY_UP;
  io.KeyMap[ImGuiKey_DownArrow] = GLFW_KEY_DOWN;
  io.KeyMap[ImGuiKey_Home] = GLFW_KEY_HOME;
  io.KeyMap[ImGuiKey_End] = GLFW_KEY_END;
  io.KeyMap[ImGuiKey_Delete] = GLFW_KEY_DELETE;
  io.KeyMap[ImGuiKey_Backspace] = GLFW_KEY_BACKSPACE;
  io.KeyMap[ImGuiKey_Enter] = GLFW_KEY_ENTER;
  io.KeyMap[ImGuiKey_Escape] = GLFW_KEY_ESCAPE;
  io.KeyMap[ImGuiKey_A] = GLFW_KEY_A;
  io.KeyMap[ImGuiKey_C] = GLFW_KEY_C;
  io.KeyMap[ImGuiKey_V] = GLFW_KEY_V;
  io.KeyMap[ImGuiKey_X] = GLFW_KEY_X;
  io.KeyMap[ImGuiKey_Y] = GLFW_KEY_Y;
  io.KeyMap[ImGuiKey_Z] = GLFW_KEY_Z;

  // dynamic vertex- and index-buffers for imgui-generated geometry
  sg_buffer_desc vbuf_desc = {};
  vbuf_desc.usage = SG_USAGE_STREAM;
  vbuf_desc.size = MaxVertices * sizeof(ImDrawVert);
  bind.vertex_buffers[0] = sg_make_buffer(&vbuf_desc);

  sg_buffer_desc ibuf_desc = {};
  ibuf_desc.type = SG_BUFFERTYPE_INDEXBUFFER;
  ibuf_desc.usage = SG_USAGE_STREAM;
  ibuf_desc.size = MaxIndices * sizeof(ImDrawIdx);
  bind.index_buffer = sg_make_buffer(&ibuf_desc);

  // font texture for imgui's default font
  unsigned char* font_pixels;
  int font_width, font_height;
  io.Fonts->GetTexDataAsRGBA32(&font_pixels, &font_width, &font_height);
  sg_image_desc img_desc = {};
  img_desc.width = font_width;
  img_desc.height = font_height;
  img_desc.pixel_format = SG_PIXELFORMAT_RGBA8;
  img_desc.wrap_u = SG_WRAP_CLAMP_TO_EDGE;
  img_desc.wrap_v = SG_WRAP_CLAMP_TO_EDGE;
  img_desc.data.subimage[0][0] =
      sg_range{font_pixels, size_t(font_width * font_height * 4)};
  bind.fs_images[0] = sg_make_image(&img_desc);

  // shader object for imgui rendering
  sg_shader_desc shd_desc = {};
  auto& ub = shd_desc.vs.uniform_blocks[0];
  ub.size = sizeof(vs_params_t);
  ub.uniforms[0].name = "disp_size";
  ub.uniforms[0].type = SG_UNIFORMTYPE_FLOAT2;
  shd_desc.vs.source =
      "#version 330\n"
      "uniform vec2 disp_size;\n"
      "layout(location=0) in vec2 position;\n"
      "layout(location=1) in vec2 texcoord0;\n"
      "layout(location=2) in vec4 color0;\n"
      "out vec2 uv;\n"
      "out vec4 color;\n"
      "void main() {\n"
      "    gl_Position = vec4(((position/disp_size)-0.5)*vec2(2.0,-2.0), 0.5, "
      "1.0);\n"
      "    uv = texcoord0;\n"
      "    color = color0;\n"
      "}\n";
  shd_desc.fs.images[0].name = "tex";
  shd_desc.fs.images[0].image_type = SG_IMAGETYPE_2D;
  shd_desc.fs.source =
      "#version 330\n"
      "uniform sampler2D tex;\n"
      "in vec2 uv;\n"
      "in vec4 color;\n"
      "out vec4 frag_color;\n"
      "void main() {\n"
      "    frag_color = texture(tex, uv) * color;\n"
      "}\n";
  sg_shader shd = sg_make_shader(&shd_desc);

  // pipeline object for imgui rendering
  sg_pipeline_desc pip_desc = {};
  pip_desc.layout.buffers[0].stride = sizeof(ImDrawVert);
  auto& attrs = pip_desc.layout.attrs;
  attrs[0].format = SG_VERTEXFORMAT_FLOAT2;
  attrs[1].format = SG_VERTEXFORMAT_FLOAT2;
  attrs[2].format = SG_VERTEXFORMAT_UBYTE4N;
  pip_desc.shader = shd;
  pip_desc.index_type = SG_INDEXTYPE_UINT16;
  pip_desc.colors[0].blend.enabled = true;
  pip_desc.colors[0].blend.src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA;
  pip_desc.colors[0].blend.dst_factor_rgb = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
  pip_desc.colors[0].write_mask = SG_COLORMASK_RGB;
  pip = sg_make_pipeline(&pip_desc);

  // initial clear color
  pass_action.colors[0].action = SG_ACTION_CLEAR;
  pass_action.colors[0].value = {0.1f, 0.1f, 0.1f, 1.0f};

  // draw loop
  while (!glfwWindowShouldClose(w)) {
    state.time.frame = stm_sec(
        stm_round_to_common_refresh_rate(stm_laptime(&state.time.laptime)));

    state.time.absolute += state.time.frame * state.time.factor;

    int cur_width, cur_height;
    glfwGetFramebufferSize(w, &cur_width, &cur_height);

    // this is standard ImGui demo code
    ImGuiIO& io = ImGui::GetIO();
    io.DisplaySize = ImVec2(float(cur_width), float(cur_height));
    io.DeltaTime = (float)stm_sec(stm_laptime(&last_time));
    ImGui::NewFrame();

    ImGui::Begin("Camera");
    ImGui::SliderFloat3("pos", state.camera.pos, -100.f, 100.f);
    ImGui::SliderFloat("near", &state.camera.near, 0.001f, 10.f);
    ImGui::SliderFloat("far", &state.camera.far, 1.0f, 10000.f);
    ImGui::SliderFloat("heading", &state.camera.heading, -180.0f, 180.f);
    ImGui::SliderFloat("pitch", &state.camera.pitch, -179.0f, 179.f);
    ImGui::End();

    // handle input
    handle_mouse (w, &gGuiInputState);

    if (glfwGetMouseButton(w, GLFW_MOUSE_BUTTON_RIGHT)) {
      if (gControlMode == ControlMode::ControlModeNone) {
        gControlMode = ControlMode::ControlModeFPS;
        Camera_CalcFromMatrix(&state.camera, &state.camera.mtxView[0]);
        glfwSetInputMode(w, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
      }
    } else {
      gControlMode = ControlMode::ControlModeNone;
      glfwSetInputMode(w, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
      Camera_Update(
          &state.camera,
          cur_width,
          cur_height,
          state.time.frame,
          0,
          0,
          nullptr);
    }

    if (gControlMode == ControlMode::ControlModeFPS) {
      float camera_accel[3] = {0.f, 0.f, 0.f};
      float accel_scale = 100.0;

      if (glfwGetKey(w, GLFW_KEY_LEFT_SHIFT)) {
        accel_scale *= 3.;
      } else if (glfwGetKey(w, GLFW_KEY_LEFT_CONTROL)) {
        accel_scale /= 3.;
      }

      if (glfwGetKey(w, GLFW_KEY_W)) {
        camera_accel[0] -= accel_scale;
      }
      if (glfwGetKey(w, GLFW_KEY_S)) {
        camera_accel[0] += accel_scale;
      }
      if (glfwGetKey(w, GLFW_KEY_C)) {
        camera_accel[1] -= accel_scale;
      }
      if (glfwGetKey(w, GLFW_KEY_SPACE)) {
        camera_accel[1] += accel_scale;
      }
      if (glfwGetKey(w, GLFW_KEY_A)) {
        camera_accel[2] -= accel_scale;
      }
      if (glfwGetKey(w, GLFW_KEY_D)) {
        camera_accel[2] += accel_scale;
      }

      Camera_Update(
          &state.camera,
          cur_width,
          cur_height,
          state.time.frame,
          gGuiInputState.mousedX,
          gGuiInputState.mousedY,
          camera_accel);
    }

    if (ImGui::BeginMainMenuBar()) {
      ImGui::Text("AnimTestbed");
      ImGui::Checkbox("ImGui Demo", &show_imgui_demo_window);
      ImGui::EndMainMenuBar();
    }


    draw_grid();
    skinned_mesh.DrawDebugUi();
    animation_controller.DrawDebugUi();

    animation_controller.Update(state.time.frame);
    animation_controller.Evaluate();

    sgl_defaults();
    sgl_matrix_mode_projection();
    sgl_load_matrix((const float*)&state.camera.mtxProj);
    sgl_matrix_mode_modelview();
    sgl_load_matrix((const float*)&state.camera.mtxView);
    skinned_mesh.DrawSkeleton();

    // 3. Show the ImGui test window. Most of the sample code is in ImGui::ShowDemoWindow()
    if (show_imgui_demo_window) {
      ImGui::SetNextWindowPos(ImVec2(460, 20), ImGuiCond_FirstUseEver);
      ImGui::ShowDemoWindow();
    }


    // the sokol_gfx draw pass
    sg_begin_default_pass(&pass_action, cur_width, cur_height);
    sgl_draw();
    ImGui::Render();
    draw_imgui(ImGui::GetDrawData());
    sg_end_pass();
    sg_commit();
    glfwSwapBuffers(w);
    glfwPollEvents();
  }

//  state.ozz = nullptr;

  /* cleanup */
  ImGui::DestroyContext();
  sgl_shutdown();
  sg_shutdown();
  glfwTerminate();
  return 0;
}

bool LoadSkeleton(const char* _filename, ozz::animation::Skeleton* _skeleton) {
  assert(_filename && _skeleton);
  ozz::log::Out() << "Loading skeleton archive " << _filename << "."
                  << std::endl;
  ozz::io::File file(_filename, "rb");
  if (!file.opened()) {
    ozz::log::Err() << "Failed to open skeleton file " << _filename << "."
                    << std::endl;
    return false;
  }
  ozz::io::IArchive archive(&file);
  if (!archive.TestTag<ozz::animation::Skeleton>()) {
    ozz::log::Err() << "Failed to load skeleton instance from file "
                    << _filename << "." << std::endl;
    return false;
  }

  // Once the tag is validated, reading cannot fail.
  archive >> *_skeleton;

  return true;
}

bool LoadAnimation(
    const char* _filename,
    ozz::animation::Animation* _animation) {
  assert(_filename && _animation);
  ozz::log::Out() << "Loading animation archive: " << _filename << "."
                  << std::endl;
  ozz::io::File file(_filename, "rb");
  if (!file.opened()) {
    ozz::log::Err() << "Failed to open animation file " << _filename << "."
                    << std::endl;
    return false;
  }
  ozz::io::IArchive archive(&file);
  if (!archive.TestTag<ozz::animation::Animation>()) {
    ozz::log::Err() << "Failed to load animation instance from file "
                    << _filename << "." << std::endl;
    return false;
  }

  // Once the tag is validated, reading cannot fail.
  archive >> *_animation;

  return true;
}

static void load_skeleton(void) {
  //  const char* skeleton_file = "../media/skeleton.ozz";
  const char* skeleton_file = "../media/MixamoYBot-skeleton.ozz";
  if (!LoadSkeleton(skeleton_file, &state.ozz->skeleton)) {
    std::cerr << "Could not load skeleton " << skeleton_file << std::endl;
    return;
  }

  const int num_soa_joints = state.ozz->skeleton.num_soa_joints();
  const int num_joints = state.ozz->skeleton.num_joints();
  state.ozz->local_matrices.resize(num_soa_joints);
  state.ozz->model_matrices.resize(num_joints);
  state.ozz->cache.Resize(num_joints);
  state.loaded.skeleton = true;
  std::cout << "Successfully loaded " << skeleton_file
            << " (soa: " << num_soa_joints << ", joints: " << num_joints << ")"
            << std::endl;
}

static void load_animation(void) {
  //  const char* anim_file = "../media/animation1.ozz";
  const char* anim_file = "../media/RunningSlow-loop.ozz";
  if (!LoadAnimation(anim_file, &state.ozz->animation)) {
    std::cerr << "Could not load animation " << anim_file << std::endl;
    return;
  }

  state.loaded.animation = true;
  std::cout << "Successfully loade " << anim_file << std::endl;
}

static void draw_vec(const ozz::math::SimdFloat4& vec) {
  sgl_v3f(ozz::math::GetX(vec), ozz::math::GetY(vec), ozz::math::GetZ(vec));
}

static void draw_line(
    const ozz::math::SimdFloat4& v0,
    const ozz::math::SimdFloat4& v1) {
  draw_vec(v0);
  draw_vec(v1);
}


static void draw_grid(void) {
  sgl_defaults();
  sgl_matrix_mode_projection();
  sgl_load_matrix((const float*)&state.camera.mtxProj);
  sgl_matrix_mode_modelview();
  sgl_load_matrix((const float*)&state.camera.mtxView);

  const int grid_size = 10;

  sgl_begin_lines();
  sgl_c3f(0.4f, 0.4f, 0.4f);
  for (int i = -grid_size; i <= grid_size; i++) {
    if (i == 0) {
      continue;
    }
    ozz::math::SimdFloat4 p0 = ozz::math::simd_float4::Load(i * 1.0f, 0.f, -grid_size * 1.0f, 1.f);
    ozz::math::SimdFloat4 p1 = ozz::math::simd_float4::Load(i * 1.0f, 0.f, grid_size * 1.0f, 1.f);
    draw_line(p0, p1);

    p0 = ozz::math::simd_float4::Load(-grid_size * 1.0f, 0.f, i * 1.0f, 1.f);
    p1 = ozz::math::simd_float4::Load(grid_size * 1.0f, 0.f, i * 1.0f, 1.f);
    draw_line(p0, p1);
  }
  sgl_c3f (0.7f, 0.4f, 0.2f);
  ozz::math::SimdFloat4 p0 = ozz::math::simd_float4::Load(0, 0.f, -grid_size * 1.0f, 1.f);
  ozz::math::SimdFloat4 p1 = ozz::math::simd_float4::Load(0, 0.f, grid_size * 1.0f, 1.f);
  draw_line(p0, p1);

  sgl_c3f (0.2f, 0.4f, 0.7f);
  p0 = ozz::math::simd_float4::Load(-grid_size * 1.0f, 0.f, 0.f, 1.f);
  p1 = ozz::math::simd_float4::Load(grid_size * 1.0f, 0.f, 0.f, 1.f);
  draw_line(p0, p1);

  sgl_end();
}


// draw ImGui draw lists via sokol-gfx
void draw_imgui(ImDrawData* draw_data) {
  assert(draw_data);
  if (draw_data->CmdListsCount == 0) {
    return;
  }

  // render the command list
  sg_apply_pipeline(pip);
  vs_params_t vs_params;
  vs_params.disp_size.x = ImGui::GetIO().DisplaySize.x;
  vs_params.disp_size.y = ImGui::GetIO().DisplaySize.y;
  sg_apply_uniforms(SG_SHADERSTAGE_VS, 0, SG_RANGE(vs_params));
  for (int cl_index = 0; cl_index < draw_data->CmdListsCount; cl_index++) {
    const ImDrawList* cl = draw_data->CmdLists[cl_index];

    // append vertices and indices to buffers, record start offsets in resource binding struct
    const uint32_t vtx_size = cl->VtxBuffer.size() * sizeof(ImDrawVert);
    const uint32_t idx_size = cl->IdxBuffer.size() * sizeof(ImDrawIdx);
    const uint32_t vb_offset = sg_append_buffer(
        bind.vertex_buffers[0],
        {&cl->VtxBuffer.front(), vtx_size});
    const uint32_t ib_offset =
        sg_append_buffer(bind.index_buffer, {&cl->IdxBuffer.front(), idx_size});
    /* don't render anything if the buffer is in overflow state (this is also
            checked internally in sokol_gfx, draw calls that attempt from
            overflowed buffers will be silently dropped)
        */
    if (sg_query_buffer_overflow(bind.vertex_buffers[0])
        || sg_query_buffer_overflow(bind.index_buffer)) {
      continue;
    }

    bind.vertex_buffer_offsets[0] = vb_offset;
    bind.index_buffer_offset = ib_offset;
    sg_apply_bindings(&bind);

    int base_element = 0;
    for (const ImDrawCmd& pcmd : cl->CmdBuffer) {
      if (pcmd.UserCallback) {
        pcmd.UserCallback(cl, &pcmd);
      } else {
        const int scissor_x = (int)(pcmd.ClipRect.x);
        const int scissor_y = (int)(pcmd.ClipRect.y);
        const int scissor_w = (int)(pcmd.ClipRect.z - pcmd.ClipRect.x);
        const int scissor_h = (int)(pcmd.ClipRect.w - pcmd.ClipRect.y);
        sg_apply_scissor_rect(scissor_x, scissor_y, scissor_w, scissor_h, true);
        sg_draw(base_element, pcmd.ElemCount, 1);
      }
      base_element += pcmd.ElemCount;
    }
  }
}
