#ifndef SRENDER_H
#define SRENDER_H

#ifdef __cplusplus
extern "C" {
#endif

#include <stdbool.h>

#include "vectorial/simd4x4f.h"

typedef struct srndr srndr;
typedef struct srview srview;
typedef struct srcmdbuf srcmdbuf;

typedef enum {
  SRndrCmdTypeFrame = 0,
  SRndrCmdTypeGrid,
  SRndrCmdTypeCube,
  SRndrCmdTypeSphere,
  SrndrCmdTypePoint,
} SRndrCmdType;

typedef struct srcmd {
  simd4x4f mat;
  simd4f color;
  SRndrCmdType type;
} srcmd;

//
// Renderer
//
srndr* srndr_create();
void srndr_destroy(srndr* sr);

//
// View
//
srview* srview_create();
void srview_destroy(srview* sv);

void srview_set_proj(srview* sv, simd4x4f proj);
void srview_set_view(srview* sv, simd4x4f view);
void srview_set_size(srview* sv, int width, int height);
void srview_get_output_texture(srview* sv, GLuint* texture);

//
// Command Buffer and Commands
//
void srcmd_clear(srcmd* cmd);

srcmdbuf* srcmdbuf_create(unsigned int size_max);
void srcmdbuf_clear(srcmdbuf* cmdbuf);
bool srcmdbuf_add(srcmdbuf* cmdbuf, srcmd* cmd);
srcmd* srcmd_create(srcmdbuf* cmdbuf);

void srndr_render(srndr* srndr, srview* sview, srcmdbuf* scmdbuf);

//
// srender Implementation
//

#ifdef SRENDER_IMPLEMENTATION

#include <assert.h>
#include <string.h>

#include "utils.h"

typedef struct srndr {
  struct {
    simd4f* pos;
    simd4f* color;
    GLuint num_points;
    GLuint points_vbo_id;
  } point_buf;
} srndr;

typedef struct srview {
  GLuint width;
  GLuint height;

  GLuint mFrameBufferId;
  GLuint mColorTexture;
  GLuint mDepthTexture;

  simd4x4f proj;
  simd4x4f view;
} srview;

typedef struct srcmdbuf {
  int nbufsize;
  int ncmds;
  srcmd* cmds;
} srcmdbuf;

//
// Vertex Data
//
typedef union srvrtxdata {
  struct {
    float x, y, z, w;
    float nx, ny, nz;
    float s, t;
    GLubyte r, g, b, a;
  };
  struct {
    float pos[4];
    float n[4];
    float uv[2];
    GLubyte color[4];
  };
} srvrtxdata;

//
// Vertex Buffer Object for all debug objects (coord frame, grid, cube, ...)
//
const GLint DEBUG_VBO_SIZE = 1024;
GLuint gDebugShapesVBOId = 0;
GLuint gDebugShapesVAId = 0;

//
// Simple Mesh Data
//
typedef struct srmeshdata {
  int nvertices;
  int nindices;

  GLuint vao_id;
  GLuint vb_id;
  GLuint idb_id;

  GLuint count;
  GLenum mode;
} srmeshdata;

static srmeshdata gCoordFrameMesh = {0};
static srmeshdata gGridMesh = {0};
static srmeshdata gCubeMesh = {0};
static srmeshdata gUVSphereMesh = {0};

typedef struct srshader {
  const char* vert_fname;
  const char* frag_fname;

  GLuint program_id;
  GLuint vert_shader_id;
  GLuint frag_shader_id;
} srshader;

static srshader gDefaultShader =
    {"default_vert.glsl", "default_frag.glsl", 0, 0, 0};

bool srshader_compile(GLuint shader_id, const char* shader_src) {
  glShaderSource(shader_id, 1, &shader_src, 0);
  glCompileShader(shader_id);

  // Check Vertex Shader
  GLint result = GL_FALSE;
  int log_length = 0;
  glGetShaderiv(shader_id, GL_COMPILE_STATUS, &result);
  glGetShaderiv(shader_id, GL_INFO_LOG_LENGTH, &log_length);
  if (log_length > 0) {
    char* err_msg = calloc(log_length, sizeof(char));
    assert(err_msg);
    glGetShaderInfoLog(shader_id, log_length, NULL, err_msg);
    gLog("%s", err_msg);
    free(err_msg);

    return false;
  }

  return true;
}

bool srshader_load(
    srshader* shader,
    const char* vert_src,
    const char* frag_src) {
  shader->vert_shader_id = glCreateShader(GL_VERTEX_SHADER);
  if (!srshader_compile(shader->vert_shader_id, vert_src)) {
    gLog("Error compiling vertex shader!");
    return false;
  }

  shader->frag_shader_id = glCreateShader(GL_FRAGMENT_SHADER);
  if (!srshader_compile(shader->frag_shader_id, frag_src)) {
    gLog("Error compiling fragment shader!");
    return false;
  }

  shader->program_id = glCreateProgram();
  glAttachShader(shader->program_id, shader->vert_shader_id);
  glAttachShader(shader->program_id, shader->frag_shader_id);

  // Bind attribute locations
  glBindAttribLocation(shader->program_id, 0, "inCoord");
  glBindAttribLocation(shader->program_id, 1, "inNormal");
  glBindAttribLocation(shader->program_id, 2, "inUV");
  glBindAttribLocation(shader->program_id, 3, "inColor");

  glLinkProgram(shader->program_id);

  GLint result = GL_FALSE;
  int log_length = 0;
  glGetProgramiv(shader->program_id, GL_LINK_STATUS, &result);
  glGetProgramiv(shader->program_id, GL_INFO_LOG_LENGTH, &log_length);
  if (log_length > 0) {
    char* err_msg = calloc(log_length, sizeof(char));
    assert(err_msg);
    glGetProgramInfoLog(shader->program_id, log_length, NULL, err_msg);
    gLog("%s", err_msg);
    free(err_msg);
    gLog("Error linking shader program!");

    return false;
  }

  return true;
}

bool try_read_file(const char* dir, const char* filename, char** buf) {
  assert(*buf == NULL);

  int path_len = strlen(dir) + strlen(filename) + 1;
  char* path_buffer = calloc(path_len, sizeof(char));
  snprintf(path_buffer, path_len, "%s%s", dir, filename);
  long file_length = 0;

  FILE* file = NULL;
  file = fopen(path_buffer, "rb");
  if (file) {
    fseek(file, 0, SEEK_END);
    file_length = ftell(file);
    fseek(file, 0, SEEK_SET);
    *buf = calloc(file_length, sizeof(char));
    assert(*buf);
    fread(*buf, 1, file_length, file);
    fclose(file);

    return true;
  }

  return false;
}

void init_shaders() {
  static const char* shader_search_paths[] = {
      "./data/shaders/",
      "../data/shaders/",
      NULL};

  bool found = false;
  int search_index = 0;
  char* frag_buffer = NULL;
  char* vert_buffer = NULL;

  while (shader_search_paths[search_index] != NULL) {
    const char* dir = shader_search_paths[search_index];
    static char path_buf[1024];
    long length;

    if (vert_buffer == NULL) {
      try_read_file(dir, gDefaultShader.vert_fname, &vert_buffer);
    }
    if (frag_buffer == NULL) {
      try_read_file(dir, gDefaultShader.frag_fname, &frag_buffer);
    }

    if ((vert_buffer != NULL) && (frag_buffer != NULL)) {
      srshader_load(&gDefaultShader, vert_buffer, frag_buffer);

      free(vert_buffer);
      vert_buffer = NULL;
      free(frag_buffer);
      frag_buffer = NULL;
      break;
    }

    search_index++;
  }

  if (gDefaultShader.program_id == 0) {
    gLog("Error: could not load default shader!");
  }
}

void init_debug_meshes() {
  assert(gCoordFrameMesh.nvertices == 0);

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

  glGenBuffers(1, &gDebugShapesVBOId);
  glBindBuffer(GL_ARRAY_BUFFER, gDebugShapesVBOId);
  glBufferData(
      GL_ARRAY_BUFFER,
      sizeof(union srvrtxdata) * DEBUG_VBO_SIZE,
      NULL,
      GL_STATIC_DRAW);

  GLintptr dbg_vbuf_offset = 0;

  glEnableVertexAttribArray(0);
  glVertexAttribPointer(
      0,
      4,
      GL_FLOAT,
      GL_FALSE,
      (sizeof(srvrtxdata)),
      (void*)0);

  glEnableVertexAttribArray(1);
  glVertexAttribPointer(
      1,
      3,
      GL_FLOAT,
      GL_FALSE,
      (sizeof(srvrtxdata)),
      (void*)(sizeof(float) * 4));

  // Attribute 2: texture coordinates
  glEnableVertexAttribArray(2);
  glVertexAttribPointer(
      2,
      2,
      GL_FLOAT,
      GL_FALSE,
      (sizeof(srvrtxdata)),
      (void*)(sizeof(float) * 7));

  // Attribute 3: color
  glEnableVertexAttribArray(3);
  glVertexAttribPointer(
      3,
      4,
      GL_UNSIGNED_BYTE,
      GL_TRUE,
      (sizeof(srvrtxdata)),
      (void*)(sizeof(float) * 9));

  //
  // Coordinate Frame
  //
  gCoordFrameMesh.vao_id = gDebugShapesVAId;
  gCoordFrameMesh.vb_id = gDebugShapesVBOId;
  gCoordFrameMesh.nvertices = 6;

  srvrtxdata coord_frame_vertices[] = {
      {0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 255, 0, 0, 255},
      {1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 255, 0, 0, 255},

      {0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0, 255, 0, 255},
      {0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0, 255, 0, 255},

      {0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0, 0, 255, 255},
      {0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0, 0, 255, 255}};
  GLuint coord_frame_indices[] = {0, 1, 2, 3, 4, 5};

  glBufferSubData(
      GL_ARRAY_BUFFER,
      dbg_vbuf_offset,
      sizeof(coord_frame_vertices),
      coord_frame_vertices);
  dbg_vbuf_offset = sizeof(coord_frame_vertices);

  glGenBuffers(1, &gCoordFrameMesh.idb_id);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gCoordFrameMesh.idb_id);
  glBufferData(
      GL_ELEMENT_ARRAY_BUFFER,
      sizeof(coord_frame_indices),
      coord_frame_indices,
      GL_STATIC_DRAW);

  gCoordFrameMesh.mode = GL_LINES;
  gCoordFrameMesh.count = 6;

  //
  // Coordinate Frame
  //
  gGridMesh.vao_id = gDebugShapesVAId;
  gGridMesh.vb_id = gDebugShapesVBOId;
  gGridMesh.nvertices = 44;

  srvrtxdata grid_vertices[44];
  srvrtxdata grid_def_vertex =
      {0.f, 0.f, 0.f, 1.f, 0.f, 1.f, 0.f, 0.f, 0.f, 255, 255, 255, 255};

  // lines along parallel to z axis
  for (int i = 0; i <= 10; i++) {
    memcpy(&grid_vertices[2 * i], &grid_def_vertex, sizeof(srvrtxdata));
    memcpy(&grid_vertices[2 * i + 1], &grid_def_vertex, sizeof(srvrtxdata));
    grid_vertices[2 * i].pos[0] = -5.0f + i * 1.0f;
    grid_vertices[2 * i].pos[2] = 5.0f;
    grid_vertices[2 * i + 1].pos[0] = -5.0f + i * 1.0f;
    grid_vertices[2 * i + 1].pos[2] = -5.0f;
    if (i == 5) {
      grid_vertices[2 * i].g = 0;
      grid_vertices[2 * i].r = 0;
      grid_vertices[2 * i + 1].g = 0;
      grid_vertices[2 * i + 1].r = 0;
    }
  }
  // lines parallel to x axis
  for (int i = 11; i <= 21; i++) {
    memcpy(&grid_vertices[2 * i], &grid_def_vertex, sizeof(srvrtxdata));
    memcpy(&grid_vertices[2 * i + 1], &grid_def_vertex, sizeof(srvrtxdata));
    grid_vertices[2 * i].pos[0] = -5.0f;
    grid_vertices[2 * i].pos[2] = -5.0f + (i - 11) * 1.0f;
    grid_vertices[2 * i + 1].pos[0] = 5.0f;
    grid_vertices[2 * i + 1].pos[2] = -5.0f + (i - 11) * 1.0f;

    if (i == 16) {
      grid_vertices[2 * i].g = 0;
      grid_vertices[2 * i].b = 0;
      grid_vertices[2 * i + 1].g = 0;
      grid_vertices[2 * i + 1].b = 0;
    }
  }

  glBindBuffer(GL_ARRAY_BUFFER, gDebugShapesVBOId);
  glBufferSubData(
      GL_ARRAY_BUFFER,
      dbg_vbuf_offset,
      sizeof(srvrtxdata) * 44,
      grid_vertices);
  dbg_vbuf_offset += sizeof(srvrtxdata) * 44;

  GLuint grid_indices[44];
  for (int i = 0; i < 22; i++) {
    grid_indices[2 * i] = 2 * i + 6;
    grid_indices[2 * i + 1] = (2 * i + 1) + 6;
  }

  glGenBuffers(1, &gGridMesh.idb_id);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gGridMesh.idb_id);
  glBufferData(
      GL_ELEMENT_ARRAY_BUFFER,
      sizeof(grid_indices),
      grid_indices,
      GL_STATIC_DRAW);

  gGridMesh.mode = GL_LINES;
  gGridMesh.count = 44;

  //
  // Cube
  //
  gCubeMesh.vao_id = gDebugShapesVAId;
  gCubeMesh.vb_id = gDebugShapesVBOId;
  gCubeMesh.nvertices = 24;

  // clang-format off
  srvrtxdata cube_vertices[24] = {
      // +x
      { 0.5f, -0.5f,  0.5f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 255, 0, 0, 255},
      { 0.5f, -0.5f, -0.5f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 255, 0, 0, 255},
      { 0.5f,  0.5f, -0.5f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 255, 0, 0, 255},
      { 0.5f,  0.5f,  0.5f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 255, 0, 0, 255},

      // +y
      {-0.5f,  0.5f,  0.5f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0, 255, 0, 255},
      { 0.5f,  0.5f,  0.5f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0, 255, 0, 255},
      { 0.5f,  0.5f, -0.5f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0, 255, 0, 255},
      {-0.5f,  0.5f, -0.5f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0, 255, 0, 255},

      // +z
      {-0.5f, -0.5f,  0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0, 0, 255, 255},
      { 0.5f, -0.5f,  0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0, 0, 255, 255},
      { 0.5f,  0.5f,  0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0, 0, 255, 255},
      {-0.5f,  0.5f,  0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0, 0, 255, 255},

      // -x
      {-0.5f, -0.5f, -0.5f, 1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 128, 0, 0, 255},
      {-0.5f, -0.5f,  0.5f, 1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 128, 0, 0, 255},
      {-0.5f,  0.5f,  0.5f, 1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 128, 0, 0, 255},
      {-0.5f,  0.5f, -0.5f, 1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 128, 0, 0, 255},

      // -y
      { 0.5f, -0.5f,  0.5f, 1.0f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f, 0, 128, 0, 255},
      {-0.5f, -0.5f,  0.5f, 1.0f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f, 0, 128, 0, 255},
      {-0.5f, -0.5f, -0.5f, 1.0f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f, 0, 128, 0, 255},
      { 0.5f, -0.5f, -0.5f, 1.0f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f, 0, 128, 0, 255},

      // -z
      { 0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, 0, 0, 128, 255},
      {-0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, 0, 0, 128, 255},
      {-0.5f,  0.5f, -0.5f, 1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, 0, 0, 128, 255},
      { 0.5f,  0.5f, -0.5f, 1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, 0, 0, 128, 255}
  };
  // clang-format on

  glBindBuffer(GL_ARRAY_BUFFER, gDebugShapesVBOId);
  glBufferSubData(
      GL_ARRAY_BUFFER,
      dbg_vbuf_offset,
      sizeof(srvrtxdata) * 24,
      cube_vertices);
  dbg_vbuf_offset += sizeof(srvrtxdata) * 24;

  GLuint cube_indices[] = {
      // clang-format off
     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
      // clang-format on
  };

  for (int i = 0; i < 36; i++) {
    cube_indices[i] += gCoordFrameMesh.nvertices + gGridMesh.nvertices;
  }

  glGenBuffers(1, &gCubeMesh.idb_id);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gCubeMesh.idb_id);
  glBufferData(
      GL_ELEMENT_ARRAY_BUFFER,
      sizeof(cube_indices),
      cube_indices,
      GL_STATIC_DRAW);

  gCubeMesh.mode = GL_TRIANGLES;
  gCubeMesh.count = 36;

  //
  // UVSphere
  //
  gUVSphereMesh.vao_id = gDebugShapesVAId;
  gUVSphereMesh.vb_id = gDebugShapesVBOId;

  // loosely based on http://www.songho.ca/opengl/gl_sphere.html
  int stack_count = 24;
  int sector_count = 24;
  gUVSphereMesh.nvertices = (stack_count + 1) * (sector_count + 1);
  gLog("UVsphere vertices: %d", gUVSphereMesh.nvertices);

  srvrtxdata* uvsphere_vertices =
      calloc(sizeof(srvrtxdata), gUVSphereMesh.nvertices);

  float stack_step = M_PI / stack_count;
  float sector_step = 2 * M_PI / sector_count;

  int vert_index = 0;
  for (int i = 0; i < stack_count + 1; i++) {
    float phi = -M_PI * 0.5 + i * stack_step;
    float y = sin(phi);

    for (int j = 0; j < sector_count + 1; j++) {
      float theta = -M_PI + j * sector_step;
      float x = cos(theta) * cos(phi);
      float z = -sin(theta) * cos(phi);
//      gLog ("idx: %d # phi: %f, theta %f, p = %f, %f, %f", vert_index, phi * 180 / M_PI, theta * 180 / M_PI, x, y, z);

      srvrtxdata* vertex = &uvsphere_vertices[vert_index++];
      vertex->x = x * 0.5;
      vertex->y = y * 0.5;
      vertex->z = z * 0.5;
      vertex->w = 1.0f;

      vertex->nx = x;
      vertex->ny = y;
      vertex->nz = z;

      vertex->s = (theta + M_PI) / (2.f * M_PI);
      vertex->t = (phi + M_PI * 0.5) / M_PI;

      vertex->r = 192;
      vertex->g = 192;
      vertex->b = 192;
      vertex->a = 255;
    }
  }

  glBindBuffer(GL_ARRAY_BUFFER, gDebugShapesVBOId);
  glBufferSubData(
      GL_ARRAY_BUFFER,
      dbg_vbuf_offset,
      sizeof(srvrtxdata) * gUVSphereMesh.nvertices,
      uvsphere_vertices);
  dbg_vbuf_offset += sizeof(srvrtxdata) * gUVSphereMesh.nvertices;
  free(uvsphere_vertices);

  gUVSphereMesh.nindices =
      stack_count * sector_count * 3 * 2 - sector_count * 2 * 3;
  GLuint* uvsphere_indices = calloc(sizeof(GLuint), gUVSphereMesh.nindices);

  int indices_index = 0;
  for (int i = 0; i < stack_count; i++) {
    GLuint k1 = i * (sector_count + 1);
    GLuint k2 = k1 + sector_count + 1;
    for (int j = 0; j < sector_count; ++j, ++k1, ++k2) {
      if (i != 0) {
        uvsphere_indices[indices_index++] = k1;
        uvsphere_indices[indices_index++] = k1 + 1;
        uvsphere_indices[indices_index++] = k2 + 1;
      }

      if (i != stack_count - 1) {
        uvsphere_indices[indices_index++] = k2 + 1;
        uvsphere_indices[indices_index++] = k2;
        uvsphere_indices[indices_index++] = k1;
      }
    }
  }

  for (int i = 0; i < gUVSphereMesh.nindices; i++) {
    uvsphere_indices[i] +=
        gCoordFrameMesh.nvertices + gGridMesh.nvertices + gCubeMesh.nvertices;
  }

  glGenBuffers(1, &gUVSphereMesh.idb_id);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gUVSphereMesh.idb_id);
  glBufferData(
      GL_ELEMENT_ARRAY_BUFFER,
      sizeof(GLuint) * gUVSphereMesh.nindices,
      uvsphere_indices,
      GL_STATIC_DRAW);

  free(uvsphere_indices);

  gUVSphereMesh.mode = GL_TRIANGLES;
  gUVSphereMesh.count = gUVSphereMesh.nindices;

  glBindVertexArray(0);

  gLog ("Debug Mesh Vertices: %d", dbg_vbuf_offset / sizeof(union srvrtxdata));
};

//
// Renderer
//
srndr* srndr_create() {
  srndr* result = calloc(1, sizeof(srndr));

  init_shaders();
  init_debug_meshes();

  return result;
}

void srndr_destroy(srndr* sr) { free(sr); }

//
// View
//
void srview_get_output_texture(srview* sv, GLuint* texture) {
  *texture = sv->mColorTexture;
}

void srview_set_proj(srview* sv, simd4x4f proj) { sv->proj = proj; }

void srview_set_view(srview* sv, simd4x4f view) { sv->view = view; }

void srview_update_framebuffer(srview* sv) {
  glGenFramebuffers(1, &sv->mFrameBufferId);
  glBindFramebuffer(GL_FRAMEBUFFER, sv->mFrameBufferId);

  // Color Texture
  glGenTextures(1, &sv->mColorTexture);
  glBindTexture(GL_TEXTURE_2D, sv->mColorTexture);

  glTexImage2D(
      GL_TEXTURE_2D,
      0,
      GL_RGB,
      sv->width,
      sv->height,
      0,
      GL_RGB,
      GL_UNSIGNED_BYTE,
      0);

  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

  glFramebufferTexture2D(
      GL_FRAMEBUFFER,
      GL_COLOR_ATTACHMENT0,
      GL_TEXTURE_2D,
      sv->mColorTexture,
      0);

  // Depth Texture
  glGenTextures(1, &sv->mDepthTexture);
  glBindTexture(GL_TEXTURE_2D, sv->mDepthTexture);

  glTexImage2D(
      GL_TEXTURE_2D,
      0,
      GL_DEPTH_COMPONENT,
      sv->width,
      sv->height,
      0,
      GL_DEPTH_COMPONENT,
      GL_FLOAT,
      0);

  // Set parameters so that we can set a shadow2DSampler
  glTexParameteri(
      GL_TEXTURE_2D,
      GL_TEXTURE_COMPARE_MODE,
      GL_COMPARE_REF_TO_TEXTURE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
  float border_color[] = {1.0f, 1.0f, 1.0f, 1.0f};
  glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border_color);

  glFramebufferTexture2D(
      GL_FRAMEBUFFER,
      GL_DEPTH_ATTACHMENT,
      GL_TEXTURE_2D,
      sv->mDepthTexture,
      0);
}

void srview_cleanup_framebuffer(srview* sv) {
  if (glIsFramebuffer(sv->mFrameBufferId)) {
    glDeleteFramebuffers(1, &sv->mFrameBufferId);
  }

  if (sv->mColorTexture != -1) {
    glDeleteTextures(1, &sv->mColorTexture);
    sv->mColorTexture = -1;
  }

  if (sv->mDepthTexture != -1) {
    glDeleteTextures(1, &sv->mDepthTexture);
    sv->mDepthTexture = -1;
  }
}

void srview_set_size(srview* sv, int width, int height) {
  if (sv->width == width && sv->height == height) {
    return;
  }

  sv->width = width;
  sv->height = height;

  srview_cleanup_framebuffer(sv);
  srview_update_framebuffer(sv);
}

srview* srview_create() {
  srview* result = calloc(1, sizeof(srview));

  return result;
}

void srview_destroy(srview* sv) {
  srview_cleanup_framebuffer(sv);

  free(sv);
}

void srndr_run_frame_command(const srcmd* cmd) {
  glBindVertexArray(gCoordFrameMesh.vao_id);

  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gCoordFrameMesh.idb_id);
  glDrawElements(
      gCoordFrameMesh.mode,
      gCoordFrameMesh.count,
      GL_UNSIGNED_INT,
      (void*)0);

  glBindBuffer(GL_ARRAY_BUFFER, 0);
  glBindVertexArray(0);
}

void srndr_run_grid_command(const srcmd* cmd) {
  glBindVertexArray(gGridMesh.vao_id);

  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gGridMesh.idb_id);
  glDrawElements(gGridMesh.mode, gGridMesh.count, GL_UNSIGNED_INT, (void*)0);

  glBindBuffer(GL_ARRAY_BUFFER, 0);
  glBindVertexArray(0);
}

void srndr_run_cube_command(const srcmd* cmd) {
  glBindVertexArray(gCubeMesh.vao_id);

  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gCubeMesh.idb_id);
  glDrawElements(gCubeMesh.mode, gCubeMesh.count, GL_UNSIGNED_INT, (void*)0);

  glBindBuffer(GL_ARRAY_BUFFER, 0);
  glBindVertexArray(0);
}

void srndr_run_sphere_command(const srcmd* cmd) {
  glBindVertexArray(gCubeMesh.vao_id);

  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gUVSphereMesh.idb_id);
  glDrawElements(
      gUVSphereMesh.mode,
      gUVSphereMesh.count,
      GL_UNSIGNED_INT,
      (void*)0);

  glBindBuffer(GL_ARRAY_BUFFER, 0);
  glBindVertexArray(0);
}

void srndr_render(srndr* srndr, srview* sview, srcmdbuf* scmdbuf) {
  glBindFramebuffer(GL_DRAW_FRAMEBUFFER, sview->mFrameBufferId);

  glViewport(0, 0, sview->width, sview->height);
  glClearColor(0.1f, 0.1f, 0.1f, 0.1f);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  glEnable(GL_DEPTH_TEST);
  glFrontFace(GL_CCW);
  glEnable(GL_CULL_FACE);
  glCullFace(GL_BACK);
//  glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

  glUseProgram(gDefaultShader.program_id);
  GLint view_mat_loc =
      glGetUniformLocation(gDefaultShader.program_id, "uViewMatrix");
  assert(view_mat_loc != -1);
  glUniformMatrix4fv(view_mat_loc, 1, GL_FALSE, &sview->view);

  GLint proj_mat_loc =
      glGetUniformLocation(gDefaultShader.program_id, "uProjectionMatrix");
  assert(proj_mat_loc != -1);
  glUniformMatrix4fv(proj_mat_loc, 1, GL_FALSE, &sview->proj);

  GLint model_mat_loc =
      glGetUniformLocation(gDefaultShader.program_id, "uModelMatrix");
  assert(model_mat_loc != -1);

  for (int i = 0; i < scmdbuf->ncmds; i++) {
    const srcmd* cmd = &scmdbuf->cmds[i];
    glUniformMatrix4fv(model_mat_loc, 1, GL_FALSE, &cmd->mat);

    switch (cmd->type) {
      case SRndrCmdTypeFrame:
        srndr_run_frame_command(cmd);
        break;
      case SRndrCmdTypeGrid:
        srndr_run_grid_command(cmd);
        break;
      case SRndrCmdTypeCube:
        srndr_run_cube_command(cmd);
        break;
      case SRndrCmdTypeSphere:
        srndr_run_sphere_command(cmd);
        break;
      default:
        gLog("Invalid command type %d at index %d", cmd->type, i);
        break;
    }
  }

  glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
}

//
// Render Commands
//
void srcmd_clear(srcmd* cmd) {
  cmd->color = simd4f_create(1.f, 1.f, 1.f, 1.f);
  cmd->type = SRndrCmdTypeFrame;
  simd4x4f_identity(&cmd->mat);
}

srcmdbuf* srcmdbuf_create(unsigned int size_max) {
  srcmdbuf* result = malloc(sizeof(srcmdbuf));
  result->nbufsize = size_max;
  result->ncmds = 0;
  result->cmds = calloc(sizeof(srcmd), size_max);

  return result;
}

void srcmdbuf_clear(srcmdbuf* cmdbuf) { cmdbuf->ncmds = 0; }

bool srcmdbuf_add(srcmdbuf* cmdbuf, srcmd* cmd) {
  srcmd* buf_cmd = srcmd_create(cmdbuf);

  if (!buf_cmd) {
    return false;
  }

  *buf_cmd = *cmd;
}

srcmd* srcmd_create(srcmdbuf* cmdbuf) {
  if (cmdbuf->ncmds == cmdbuf->nbufsize) {
    gLog("Warning: number of render commands maxed out!");
    return NULL;
  }
  return &(cmdbuf->cmds[cmdbuf->ncmds++]);
}

#endif  // SRENDER_IMPLEMENTATION

#ifdef __cplusplus
}
#endif

#endif  // SRENDER_H
