#include "graphics/graphics.h"
#include "graphics/buffer.h"
#include "graphics/canvas.h"
#include "graphics/material.h"
#include "graphics/mesh.h"
#include "graphics/shader.h"
#include "graphics/texture.h"
#include "data/rasterizer.h"
#include "event/event.h"
#include "math/math.h"
#include "core/maf.h"
#include "core/os.h"
#include "util.h"
#include <stdlib.h>
#include <string.h>
#include <math.h>

#define MAX_TRANSFORMS 64
#define MAX_BATCHES 4
#define MAX_DRAWS 256

typedef enum {
  STREAM_VERTEX,
  STREAM_DRAWID,
  STREAM_INDEX,
  STREAM_MODEL,
  STREAM_COLOR,
  STREAM_FRAME,
  MAX_STREAMS
} StreamType;

typedef enum {
  BATCH_POINTS,
  BATCH_LINES,
  BATCH_PLANE,
  BATCH_BOX,
  BATCH_ARC,
  BATCH_SPHERE,
  BATCH_CYLINDER,
  BATCH_SKYBOX,
  BATCH_TEXT,
  BATCH_FILL,
  BATCH_MESH
} BatchType;

typedef union {
  struct { DrawStyle style; } triangles;
  struct { DrawStyle style; } plane;
  struct { DrawStyle style; } box;
  struct { DrawStyle style; ArcMode mode; float r1; float r2; int segments; } arc;
  struct { float r1; float r2; bool capped; int segments; } cylinder;
  struct { int segments; } sphere;
  struct { float spread; } text;
  struct { float u; float v; float w; float h; } fill;
  struct { uint32_t rangeStart; uint32_t rangeCount; uint32_t instances; float* pose; } mesh;
} BatchParams;

typedef struct {
  BatchType type;
  BatchParams params;
  DrawMode topology;
  DefaultShader shader;
  Mesh* mesh;
  Pipeline* pipeline;
  Material* material;
  Texture* texture;
  mat4 transform;
  uint32_t vertexCount;
  uint32_t indexCount;
  float** vertices;
  uint16_t** indices;
  uint16_t* baseVertex;
  bool instanced;
} BatchRequest;

typedef struct {
  BatchType type;
  BatchParams params;
  DrawCommand draw;
  Material* material;
  mat4 transforms;
  Color* colors;
  uint32_t drawStart;
  uint32_t drawCount;
  bool indexed;
} Batch;

typedef struct {
  float viewMatrix[2][16];
  float projection[2][16];
} FrameData;

static struct {
  bool initialized;
  bool debug;
  int width;
  int height;
  Canvas* backbuffer;
  FrameData frameData;
  bool frameDataDirty;
  Canvas* defaultCanvas;
  Shader* defaultShaders[MAX_DEFAULT_SHADERS][2];
  Material* defaultMaterial;
  Font* defaultFont;
  TextureFilter defaultFilter;
  float transforms[MAX_TRANSFORMS][16];
  int transform;
  Color backgroundColor;
  Color linearBackgroundColor;
  Canvas* canvas;
  Color color;
  Color linearColor;
  Font* font;
  Pipeline pipeline;
  float pointSize;
  Shader* shader;
  Mesh* mesh;
  Mesh* instancedMesh;
  Buffer* identityBuffer;
  Buffer* buffers[MAX_STREAMS];
  uint32_t head[MAX_STREAMS];
  uint32_t tail[MAX_STREAMS];
  Batch batches[MAX_BATCHES];
  uint8_t batchCount;
} state;

static const uint32_t bufferCount[] = {
  [STREAM_VERTEX] = (1 << 16) - 1,
  [STREAM_DRAWID] = (1 << 16) - 1,
  [STREAM_INDEX] = 1 << 16,
#if defined(LOVR_WEBGL) // Work around bugs where big UBOs don't work
  [STREAM_MODEL] = MAX_DRAWS,
  [STREAM_COLOR] = MAX_DRAWS,
#else
  [STREAM_MODEL] = MAX_DRAWS * MAX_BATCHES,
  [STREAM_COLOR] = MAX_DRAWS * MAX_BATCHES,
#endif
  [STREAM_FRAME] = 4
};

static const size_t bufferStride[] = {
  [STREAM_VERTEX] = 8 * sizeof(float),
  [STREAM_DRAWID] = sizeof(uint8_t),
  [STREAM_INDEX] = sizeof(uint16_t),
  [STREAM_MODEL] = 16 * sizeof(float),
  [STREAM_COLOR] = 4 * sizeof(float),
  [STREAM_FRAME] = sizeof(FrameData)
};

static const BufferType bufferType[] = {
  [STREAM_VERTEX] = BUFFER_VERTEX,
  [STREAM_DRAWID] = BUFFER_GENERIC,
  [STREAM_INDEX] = BUFFER_INDEX,
  [STREAM_MODEL] = BUFFER_UNIFORM,
  [STREAM_COLOR] = BUFFER_UNIFORM,
  [STREAM_FRAME] = BUFFER_UNIFORM
};

static void gammaCorrect(Color* color) {
  color->r = lovrMathGammaToLinear(color->r);
  color->g = lovrMathGammaToLinear(color->g);
  color->b = lovrMathGammaToLinear(color->b);
}

static void onQuitRequest(void) {
  lovrEventPush((Event) { .type = EVENT_QUIT, .data.quit = { .exitCode = 0 } });
}

static void onResizeWindow(int width, int height) {
  state.width = width;
  state.height = height;
  lovrCanvasSetWidth(state.defaultCanvas, width);
  lovrCanvasSetHeight(state.defaultCanvas, height);
  lovrEventPush((Event) { .type = EVENT_RESIZE, .data.resize = { width, height } });
}

static void* lovrGraphicsMapBuffer(StreamType type, uint32_t count) {
  lovrAssert(count <= bufferCount[type], "Whoa there!  Tried to get %d elements from a buffer that only has %d elements.", count, bufferCount[type]);

  if (state.head[type] + count > bufferCount[type]) {
    lovrAssert(state.batchCount == 0, "Internal error: Batches still exist during Buffer reset");
    lovrBufferDiscard(state.buffers[type]);
    state.tail[type] = 0;
    state.head[type] = 0;
  }

  return lovrBufferMap(state.buffers[type], state.head[type] * bufferStride[type], true);
}

// Base

bool lovrGraphicsInit(bool debug) {
  state.debug = debug;
  return false; // See lovrGraphicsCreateWindow for actual initialization
}

void lovrGraphicsDestroy() {
  if (!state.initialized) return;
  lovrGraphicsSetShader(NULL);
  lovrGraphicsSetFont(NULL);
  lovrGraphicsSetCanvas(NULL);
  for (int i = 0; i < MAX_DEFAULT_SHADERS; i++) {
    lovrRelease(state.defaultShaders[i][false], lovrShaderDestroy);
    lovrRelease(state.defaultShaders[i][true], lovrShaderDestroy);
  }
  for (int i = 0; i < MAX_STREAMS; i++) {
    lovrRelease(state.buffers[i], lovrBufferDestroy);
  }
  lovrRelease(state.mesh, lovrMeshDestroy);
  lovrRelease(state.instancedMesh, lovrMeshDestroy);
  lovrRelease(state.identityBuffer, lovrBufferDestroy);
  lovrRelease(state.defaultMaterial, lovrMaterialDestroy);
  lovrRelease(state.defaultFont, lovrFontDestroy);
  lovrRelease(state.defaultCanvas, lovrCanvasDestroy);
  lovrGpuDestroy();
  memset(&state, 0, sizeof(state));
}

void lovrGraphicsPresent() {
  lovrGraphicsFlush();
  os_window_swap();
  lovrGpuPresent();
}

void lovrGraphicsCreateWindow(WindowFlags* flags) {
  os_window_config config = {
    .width = flags->width,
    .height = flags->height,
    .fullscreen = flags->fullscreen,
    .resizable = flags->resizable,
    .debug = state.debug,
    .vsync = flags->vsync,
    .msaa = flags->msaa,
    .title = flags->title,
    .icon.data = flags->icon.data,
    .icon.width = flags->icon.width,
    .icon.height = flags->icon.height
  };

  lovrAssert(!state.initialized, "Window is already created");
  lovrAssert(os_window_open(&config), "Could not create window");
  os_window_set_vsync(flags->vsync); // Force vsync in case lovr.headset changed it in a previous restart
  os_on_quit(onQuitRequest);
  os_on_resize(onResizeWindow);
  os_window_get_fbsize(&state.width, &state.height);
  lovrGpuInit(os_get_gl_proc_address, state.debug);

  state.defaultCanvas = lovrCanvasCreateFromHandle(state.width, state.height, (CanvasFlags) { .stereo = false }, 0, 0, 0, 1, true);
  state.backbuffer = state.defaultCanvas;

  for (int i = 0; i < MAX_STREAMS; i++) {
    state.buffers[i] = lovrBufferCreate(bufferCount[i] * bufferStride[i], NULL, bufferType[i], USAGE_STREAM, false);
  }

  // The identity buffer is used for autoinstanced meshes and instanced primitives and maps the
  // instance ID to a vertex attribute.  Its contents never change, so they are initialized here.
  state.identityBuffer = lovrBufferCreate(MAX_DRAWS * sizeof(uint8_t), NULL, BUFFER_VERTEX, USAGE_STATIC, false);
  uint8_t* id = lovrBufferMap(state.identityBuffer, 0, true);
  for (int i = 0; i < MAX_DRAWS; i++) id[i] = i;
  lovrBufferFlush(state.identityBuffer, 0, MAX_DRAWS);
  lovrBufferUnmap(state.identityBuffer);

  Buffer* vertexBuffer = state.buffers[STREAM_VERTEX];
  size_t stride = bufferStride[STREAM_VERTEX];

  MeshAttribute position = { .buffer = vertexBuffer, .offset = 0, .stride = stride, .type = F32, .components = 3 };
  MeshAttribute normal = { .buffer = vertexBuffer, .offset = 12, .stride = stride, .type = F32, .components = 3 };
  MeshAttribute texCoord = { .buffer = vertexBuffer, .offset = 24, .stride = stride, .type = F32, .components = 2 };
  MeshAttribute drawId = { .buffer = state.buffers[STREAM_DRAWID], .type = U8, .components = 1 };
  MeshAttribute identity = { .buffer = state.identityBuffer, .type = U8, .components = 1, .divisor = 1 };

  state.mesh = lovrMeshCreate(DRAW_TRIANGLES, NULL, 0);
  lovrMeshAttachAttribute(state.mesh, "lovrPosition", &position);
  lovrMeshAttachAttribute(state.mesh, "lovrNormal", &normal);
  lovrMeshAttachAttribute(state.mesh, "lovrTexCoord", &texCoord);
  lovrMeshAttachAttribute(state.mesh, "lovrDrawID", &drawId);

  state.instancedMesh = lovrMeshCreate(DRAW_TRIANGLES, NULL, 0);
  lovrMeshAttachAttribute(state.instancedMesh, "lovrPosition", &position);
  lovrMeshAttachAttribute(state.instancedMesh, "lovrNormal", &normal);
  lovrMeshAttachAttribute(state.instancedMesh, "lovrTexCoord", &texCoord);
  lovrMeshAttachAttribute(state.instancedMesh, "lovrDrawID", &identity);

  lovrGraphicsReset();
  state.initialized = true;
}

int lovrGraphicsGetWidth() {
  return state.width;
}

int lovrGraphicsGetHeight() {
  return state.height;
}

float lovrGraphicsGetPixelDensity() {
  int width, height, framebufferWidth, framebufferHeight;
  os_window_get_size(&width, &height);
  os_window_get_fbsize(&framebufferWidth, &framebufferHeight);
  if (width == 0 || framebufferWidth == 0) {
    return 0.f;
  } else {
    return (float) framebufferWidth / (float) width;
  }
}

void lovrGraphicsSetBackbuffer(Canvas* canvas, bool stereo, bool clear) {
  lovrGraphicsFlush();

  if (!canvas) {
    canvas = state.defaultCanvas;
    lovrCanvasSetStereo(canvas, stereo);
  }

  if (canvas != state.backbuffer) {
    lovrCanvasResolve(state.backbuffer);
    state.backbuffer = canvas;
  }

  if (clear) {
    lovrGpuClear(state.backbuffer, &state.linearBackgroundColor, &(float) { 1. }, &(int) { 0 });
  }
}

void lovrGraphicsGetViewMatrix(uint32_t index, float* viewMatrix) {
  lovrAssert(index < 2, "Invalid view index %d", index);
  mat4_init(viewMatrix, state.frameData.viewMatrix[index]);
}

void lovrGraphicsSetViewMatrix(uint32_t index, float* viewMatrix) {
  lovrAssert(index < 2, "Invalid view index %d", index);
  lovrGraphicsFlush();
  if (viewMatrix) {
    mat4_init(state.frameData.viewMatrix[index], viewMatrix);
  } else {
    mat4_identity(state.frameData.viewMatrix[index]);
  }
  state.frameDataDirty = true;
}

void lovrGraphicsGetProjection(uint32_t index, float* projection) {
  lovrAssert(index < 2, "Invalid view index %d", index);
  mat4_init(projection, state.frameData.projection[index]);
}

void lovrGraphicsSetProjection(uint32_t index, float* projection) {
  lovrAssert(index < 2, "Invalid view index %d", index);
  lovrGraphicsFlush();
  if (projection) {
    mat4_init(state.frameData.projection[index], projection);
  } else {
    float fov = 67.f * (float) M_PI / 180.f;
    float aspect = (float) state.width / state.height;
    mat4_perspective(state.frameData.projection[index], .01f, 100.f, fov, aspect);
  }
  state.frameDataDirty = true;
}

Buffer* lovrGraphicsGetIdentityBuffer() {
  return state.identityBuffer;
}

// State

void lovrGraphicsReset() {
  state.transform = 0;
  lovrGraphicsSetViewMatrix(0, NULL);
  lovrGraphicsSetViewMatrix(1, NULL);
  lovrGraphicsSetProjection(0, NULL);
  lovrGraphicsSetProjection(1, NULL);
  lovrGraphicsSetAlphaSampling(false);
  lovrGraphicsSetBackgroundColor((Color) { 0, 0, 0, 1 });
  lovrGraphicsSetBlendMode(BLEND_ALPHA, BLEND_ALPHA_MULTIPLY);
  lovrGraphicsSetCanvas(NULL);
  lovrGraphicsSetColor((Color) { 1, 1, 1, 1 });
  lovrGraphicsSetColorMask(true, true, true, true);
  lovrGraphicsSetCullingEnabled(false);
  lovrGraphicsSetDefaultFilter((TextureFilter) { .mode = FILTER_TRILINEAR });
  lovrGraphicsSetDepthTest(COMPARE_LEQUAL, true);
  lovrGraphicsSetFont(NULL);
  lovrGraphicsSetLineWidth(1.f);
  lovrGraphicsSetPointSize(1.f);
  lovrGraphicsSetShader(NULL);
  lovrGraphicsSetStencilTest(COMPARE_NONE, 0);
  lovrGraphicsSetWinding(WINDING_COUNTERCLOCKWISE);
  lovrGraphicsSetWireframe(false);
  lovrGraphicsOrigin();
}

bool lovrGraphicsGetAlphaSampling() {
  return state.pipeline.alphaSampling;
}

void lovrGraphicsSetAlphaSampling(bool sample) {
  state.pipeline.alphaSampling = sample;
}

Color lovrGraphicsGetBackgroundColor() {
  return state.backgroundColor;
}

void lovrGraphicsSetBackgroundColor(Color color) {
  state.backgroundColor = state.linearBackgroundColor = color;
#if !defined(LOVR_WEBGL)
  gammaCorrect(&state.linearBackgroundColor);
#endif
}

void lovrGraphicsGetBlendMode(BlendMode* mode, BlendAlphaMode* alphaMode) {
  *mode = state.pipeline.blendMode;
  *alphaMode = state.pipeline.blendAlphaMode;
}

void lovrGraphicsSetBlendMode(BlendMode mode, BlendAlphaMode alphaMode) {
  state.pipeline.blendMode = mode;
  state.pipeline.blendAlphaMode = alphaMode;
}

Canvas* lovrGraphicsGetCanvas() {
  return state.canvas;
}

void lovrGraphicsSetCanvas(Canvas* canvas) {
  if (state.canvas && canvas != state.canvas) {
    // The canvas must be flushed because if someone uses its textures to do a draw there is no way
    // to know that using that Texture requires the Canvas' batches to be flushed.
    lovrGraphicsFlushCanvas(state.canvas);
    lovrCanvasResolve(state.canvas);
  }

  lovrRetain(canvas);
  lovrRelease(state.canvas, lovrCanvasDestroy);
  state.canvas = canvas;
}

Color lovrGraphicsGetColor() {
  return state.color;
}

void lovrGraphicsSetColor(Color color) {
  state.color = state.linearColor = color;
  gammaCorrect(&state.linearColor);
}

void lovrGraphicsGetColorMask(bool* r, bool* g, bool* b, bool* a) {
  *r = state.pipeline.colorMask & 0x8;
  *g = state.pipeline.colorMask & 0x4;
  *b = state.pipeline.colorMask & 0x2;
  *a = state.pipeline.colorMask & 0x1;
}

void lovrGraphicsSetColorMask(bool r, bool g, bool b, bool a) {
  state.pipeline.colorMask = (r << 3) | (g << 2) | (b << 1) | a;
}

bool lovrGraphicsIsCullingEnabled() {
  return state.pipeline.culling;
}

void lovrGraphicsSetCullingEnabled(bool culling) {
  state.pipeline.culling = culling;
}

TextureFilter lovrGraphicsGetDefaultFilter() {
  return state.defaultFilter;
}

void lovrGraphicsSetDefaultFilter(TextureFilter filter) {
  state.defaultFilter = filter;
}

void lovrGraphicsGetDepthTest(CompareMode* mode, bool* write) {
  *mode = state.pipeline.depthTest;
  *write = state.pipeline.depthWrite;
}

void lovrGraphicsSetDepthTest(CompareMode mode, bool write) {
  state.pipeline.depthTest = mode;
  state.pipeline.depthWrite = write;
}

Font* lovrGraphicsGetFont() {
  if (!state.font) {
    if (!state.defaultFont) {
      Rasterizer* rasterizer = lovrRasterizerCreate(NULL, 32);
      state.defaultFont = lovrFontCreate(rasterizer, 1, 3.);
      lovrRelease(rasterizer, lovrRasterizerDestroy);
    }

    lovrGraphicsSetFont(state.defaultFont);
  }

  return state.font;
}

void lovrGraphicsSetFont(Font* font) {
  lovrRetain(font);
  lovrRelease(state.font, lovrFontDestroy);
  state.font = font;
}

float lovrGraphicsGetLineWidth() {
  return state.pipeline.lineWidth;
}

void lovrGraphicsSetLineWidth(float width) {
  state.pipeline.lineWidth = width;
}

float lovrGraphicsGetPointSize() {
  return state.pointSize;
}

void lovrGraphicsSetPointSize(float size) {
  state.pointSize = size;
}

Shader* lovrGraphicsGetShader() {
  return state.shader;
}

void lovrGraphicsSetShader(Shader* shader) {
  lovrAssert(!shader || lovrShaderGetType(shader) == SHADER_GRAPHICS, "Compute shaders can not be set as the active shader");
  lovrRetain(shader);
  lovrRelease(state.shader, lovrShaderDestroy);
  state.shader = shader;
}

void lovrGraphicsGetStencilTest(CompareMode* mode, int* value) {
  *mode = state.pipeline.stencilMode;
  *value = state.pipeline.stencilValue;
}

void lovrGraphicsSetStencilTest(CompareMode mode, int value) {
  state.pipeline.stencilMode = mode;
  state.pipeline.stencilValue = value;
}

Winding lovrGraphicsGetWinding() {
  return state.pipeline.winding;
}

void lovrGraphicsSetWinding(Winding winding) {
  state.pipeline.winding = winding;
}

bool lovrGraphicsIsWireframe() {
  return state.pipeline.wireframe;
}

void lovrGraphicsSetWireframe(bool wireframe) {
#ifdef LOVR_GL
  state.pipeline.wireframe = wireframe;
#endif
}

// Transforms

void lovrGraphicsPush() {
  lovrAssert(++state.transform < MAX_TRANSFORMS, "Unbalanced matrix stack (more pushes than pops?)");
  mat4_init(state.transforms[state.transform], state.transforms[state.transform - 1]);
}

void lovrGraphicsPop() {
  lovrAssert(--state.transform >= 0, "Unbalanced matrix stack (more pops than pushes?)");
}

void lovrGraphicsOrigin() {
  mat4_identity(state.transforms[state.transform]);
}

void lovrGraphicsTranslate(vec3 translation) {
  mat4_translate(state.transforms[state.transform], translation[0], translation[1], translation[2]);
}

void lovrGraphicsRotate(quat rotation) {
  mat4_rotateQuat(state.transforms[state.transform], rotation);
}

void lovrGraphicsScale(vec3 scale) {
  mat4_scale(state.transforms[state.transform], scale[0], scale[1], scale[2]);
}

void lovrGraphicsMatrixTransform(mat4 transform) {
  mat4_mul(state.transforms[state.transform], transform);
}

// Rendering

static void lovrGraphicsBatch(BatchRequest* req) {

  // Resolve objects
  Mesh* mesh = req->mesh ? req->mesh : (req->instanced ? state.instancedMesh : state.mesh);
  Canvas* canvas = state.canvas ? state.canvas : state.backbuffer;
  bool stereo = lovrCanvasIsStereo(canvas);
  Shader* shader = state.shader ? state.shader : (state.defaultShaders[req->shader][stereo] ? state.defaultShaders[req->shader][stereo] : (state.defaultShaders[req->shader][stereo] = lovrShaderCreateDefault(req->shader, NULL, 0, stereo)));
  Pipeline* pipeline = req->pipeline ? req->pipeline : &state.pipeline;
  Material* material = req->material ? req->material : (state.defaultMaterial ? state.defaultMaterial : (state.defaultMaterial = lovrMaterialCreate()));

  if (!req->material) {
    if (req->type == BATCH_SKYBOX && lovrTextureGetType(req->texture) == TEXTURE_CUBE) {
      lovrShaderSetTextures(shader, "lovrSkyboxTexture", &req->texture, 0, 1);
    } else {
      lovrMaterialSetTexture(material, TEXTURE_DIFFUSE, req->texture);
    }
  }

  if (lovrShaderHasUniform(shader, "lovrPose")) {
    if (req->type == BATCH_MESH && req->params.mesh.pose) {
      lovrShaderSetMatrices(shader, "lovrPose", req->params.mesh.pose, 0, MAX_BONES * 16);
    } else {
      lovrShaderSetMatrices(shader, "lovrPose", (float[]) MAT4_IDENTITY, 0, 16);
    }
  }

  // Try to find an existing batch to use
  Batch* batch = NULL;
  for (int i = state.batchCount - 1; i >= 0; i--) {
    if (req->type == BATCH_MESH && req->params.mesh.instances > 1) { break; }

    Batch* b = &state.batches[i];
    if (b->type != req->type) { goto next; }
    if (b->drawCount >= MAX_DRAWS) { goto next; }
    if (b->draw.mesh != mesh) { goto next; }
    if (b->draw.canvas != canvas) { goto next; }
    if (b->draw.shader != shader) { goto next; }
    if (b->material != material) { goto next; }
    if (memcmp(&b->draw.pipeline, pipeline, sizeof(Pipeline))) { goto next; }
    if (memcmp(&b->params, &req->params, sizeof(BatchParams))) { goto next; }
    batch = b;
    break;

next:
    // Draws can't be reordered when blending is on, depth test is off, or either of the batches
    // are streaming their vertices (since the vertices of a batch must be contiguous)
    if (b->draw.pipeline.blendMode != BLEND_NONE || pipeline->blendMode != BLEND_NONE) { break; }
    if (b->draw.pipeline.depthTest == COMPARE_NONE || pipeline->depthTest == COMPARE_NONE) { break; }
    if (!req->instanced) { break; }
  }

  // The final draw id isn't known until the batch is fully resolved and all the potential flushes
  // have occurred, so we have to do this weird thing where we map the draw id buffer early on but
  // write the ids much later.
  uint8_t* ids = NULL;

  // Figure out if a flush is necessary before mapping buffers for vertex data or UBOs.
  // - A flush is necessary if vertices are about to be written (during the first element of an
  //   instanced batch or any element of a stream batch) and any of the ranges go past the end.
  // - If a new batch is required but there isn't space for it, flush to make space.
  // - If a new batch is required, make sure there is space for the matrix/color UBO streams.
  // It's important to flush before mapping any streams, because flushing unmaps all streams.
  bool needFlush = false;
  bool hasVertices = req->vertexCount > 0 && (!req->instanced || !batch);
  bool hasIndices = hasVertices && req->indexCount > 0;
  needFlush = needFlush || (hasVertices && state.head[STREAM_VERTEX] + req->vertexCount > bufferCount[STREAM_VERTEX]);
  needFlush = needFlush || (hasVertices && state.head[STREAM_DRAWID] + req->vertexCount > bufferCount[STREAM_DRAWID]);
  needFlush = needFlush || (hasIndices && state.head[STREAM_INDEX] + req->indexCount > bufferCount[STREAM_INDEX]);
  needFlush = needFlush || (!batch && state.batchCount >= MAX_BATCHES);
  needFlush = needFlush || (!batch && state.head[STREAM_MODEL] + MAX_DRAWS > bufferCount[STREAM_MODEL]);
  needFlush = needFlush || (!batch && state.head[STREAM_COLOR] + MAX_DRAWS > bufferCount[STREAM_COLOR]);
  if (needFlush) lovrGraphicsFlush();

  if (req->vertexCount > 0 && (!req->instanced || !batch)) {
    *(req->vertices) = lovrGraphicsMapBuffer(STREAM_VERTEX, req->vertexCount);
    ids = lovrGraphicsMapBuffer(STREAM_DRAWID, req->vertexCount);

    if (req->indexCount > 0) {
      *(req->indices) = lovrGraphicsMapBuffer(STREAM_INDEX, req->indexCount);
      *(req->baseVertex) = state.head[STREAM_VERTEX];
    }
  }

  // Start a new batch
  if (!batch || state.batchCount == 0) {
    float* transforms = lovrGraphicsMapBuffer(STREAM_MODEL, MAX_DRAWS);
    Color* colors = lovrGraphicsMapBuffer(STREAM_COLOR, MAX_DRAWS);

    uint32_t rangeStart, rangeCount, instances;
    if (req->type == BATCH_MESH) {
      rangeStart = req->params.mesh.rangeStart;
      rangeCount = req->params.mesh.rangeCount;
      instances = req->instanced ? 0 : req->params.mesh.instances;
    } else {
      rangeStart = req->indexCount > 0 ? state.head[STREAM_INDEX] : state.head[STREAM_VERTEX];
      rangeCount = 0;
      instances = 0;
    }

    batch = &state.batches[state.batchCount++];
    *batch = (Batch) {
      .type = req->type,
      .params = req->params,
      .draw = {
        .mesh = mesh,
        .canvas = canvas,
        .shader = shader,
        .pipeline = *pipeline,
        .topology = req->topology,
        .rangeStart = rangeStart,
        .rangeCount = rangeCount,
        .instances = instances
      },
      .material = material,
      .transforms = transforms,
      .colors = colors,
      .drawStart = state.head[STREAM_MODEL],
      .indexed = req->indexCount > 0
    };

    state.head[STREAM_MODEL] += MAX_DRAWS;
    state.head[STREAM_COLOR] += MAX_DRAWS;
  }

  // Transform
  if (req->transform) {
    float transform[16];
    mat4_mul(mat4_init(transform, state.transforms[state.transform]), req->transform);
    memcpy(&batch->transforms[16 * batch->drawCount], transform, 16 * sizeof(float));
  } else {
    memcpy(&batch->transforms[16 * batch->drawCount], state.transforms[state.transform], 16 * sizeof(float));
  }

  // Color
  batch->colors[batch->drawCount] = state.linearColor;

  // Cursors
  if (!req->instanced || batch->drawCount == 0) {
    if (ids) {
      memset(ids, batch->drawCount, req->vertexCount * sizeof(uint8_t));
    }

    batch->draw.rangeCount += batch->indexed ? req->indexCount : req->vertexCount;
    state.head[STREAM_VERTEX] += req->vertexCount;
    state.head[STREAM_DRAWID] += req->vertexCount;
    state.head[STREAM_INDEX] += req->indexCount;
  }

  if (req->instanced) {
    batch->draw.instances++;
  }

  batch->drawCount++;
}

void lovrGraphicsFlush() {
  if (state.batchCount == 0) {
    return;
  }

  // Prevent infinite flushing >_>
  int batchCount = state.batchCount;
  state.batchCount = 0;

  if (state.frameDataDirty) {
    state.frameDataDirty = false;
    void* data = lovrGraphicsMapBuffer(STREAM_FRAME, 1);
    memcpy(data, &state.frameData, sizeof(FrameData));
    state.head[STREAM_FRAME]++;
  }

  // Flush buffers
  for (int i = 0; i < MAX_STREAMS; i++) {
    lovrBufferFlush(state.buffers[i], state.tail[i] * bufferStride[i], (state.head[i] - state.tail[i]) * bufferStride[i]);
    lovrBufferUnmap(state.buffers[i]);
    state.tail[i] = state.head[i];
  }

  for (int b = 0; b < batchCount; b++) {
    Batch* batch = &state.batches[b];

    // Uniforms
    lovrMaterialBind(batch->material, batch->draw.shader);
    lovrShaderSetBlock(batch->draw.shader, "lovrModelBlock", state.buffers[STREAM_MODEL], batch->drawStart * bufferStride[STREAM_MODEL], MAX_DRAWS * bufferStride[STREAM_MODEL], ACCESS_READ);
    lovrShaderSetBlock(batch->draw.shader, "lovrColorBlock", state.buffers[STREAM_COLOR], batch->drawStart * bufferStride[STREAM_COLOR], MAX_DRAWS * bufferStride[STREAM_COLOR], ACCESS_READ);
    lovrShaderSetBlock(batch->draw.shader, "lovrFrameBlock", state.buffers[STREAM_FRAME], (state.head[STREAM_FRAME] - 1) * bufferStride[STREAM_FRAME], bufferStride[STREAM_FRAME], ACCESS_READ);
    if (batch->type == BATCH_TEXT) {
      Texture* texture = lovrMaterialGetTexture(batch->material, TEXTURE_DIFFUSE);
      uint32_t width = lovrTextureGetWidth(texture, 0);
      uint32_t height = lovrTextureGetHeight(texture, 0);
      float range[2] = { batch->params.text.spread / width, batch->params.text.spread / height };
      lovrShaderSetFloats(batch->draw.shader, "lovrSdfRange", range, 0, 2);
    }
    if (batch->draw.topology == DRAW_POINTS) {
      lovrShaderSetFloats(batch->draw.shader, "lovrPointSize", &state.pointSize, 0, 1);
    }

    // Other bindings (TODO try to get rid of all this!)
    if (batch->type == BATCH_MESH) {
      lovrMeshSetAttributeEnabled(batch->draw.mesh, "lovrDrawID", batch->params.mesh.instances <= 1);
    } else {
      if (batch->draw.mesh == state.instancedMesh && batch->draw.instances <= 1) {
        batch->draw.mesh = state.mesh;
      }

      if (batch->indexed) {
        lovrMeshSetIndexBuffer(batch->draw.mesh, state.buffers[STREAM_INDEX], bufferCount[STREAM_INDEX], sizeof(uint16_t), 0);
      } else {
        lovrMeshSetIndexBuffer(batch->draw.mesh, NULL, 0, 0, 0);
      }
    }

    lovrGpuDraw(&batch->draw);
  }
}

void lovrGraphicsFlushCanvas(Canvas* canvas) {
  for (int i = state.batchCount - 1; i >= 0; i--) {
    if (state.batches[i].draw.canvas == canvas) {
      lovrGraphicsFlush();
      return;
    }
  }
}

void lovrGraphicsFlushShader(Shader* shader) {
  for (int i = state.batchCount - 1; i >= 0; i--) {
    if (state.batches[i].draw.shader == shader) {
      lovrGraphicsFlush();
      return;
    }
  }
}

void lovrGraphicsFlushMaterial(Material* material) {
  for (int i = state.batchCount - 1; i >= 0; i--) {
    if (state.batches[i].material == material) {
      lovrGraphicsFlush();
      return;
    }
  }
}

void lovrGraphicsFlushMesh(Mesh* mesh) {
  for (int i = state.batchCount - 1; i >= 0; i--) {
    if (state.batches[i].draw.mesh == mesh) {
      lovrGraphicsFlush();
      return;
    }
  }
}

void lovrGraphicsClear(Color* color, float* depth, int* stencil) {
#if !defined(LOVR_WEBGL)
  if (color) gammaCorrect(color);
#endif
  if (color || depth || stencil) lovrGraphicsFlush();
  lovrGpuClear(state.canvas ? state.canvas : state.backbuffer, color, depth, stencil);
}

void lovrGraphicsDiscard(bool color, bool depth, bool stencil) {
  if (color || depth || stencil) lovrGraphicsFlush();
  lovrGpuDiscard(state.canvas ? state.canvas : state.backbuffer, color, depth, stencil);
}

void lovrGraphicsPoints(uint32_t count, float** vertices) {
  lovrGraphicsBatch(&(BatchRequest) {
    .type = BATCH_POINTS,
    .topology = DRAW_POINTS,
    .vertexCount = count,
    .vertices = vertices
  });
}

void lovrGraphicsLine(uint32_t count, float** vertices) {
  uint32_t indexCount = count + 1;
  uint16_t* indices;
  uint16_t baseVertex;

  lovrGraphicsBatch(&(BatchRequest) {
    .type = BATCH_LINES,
    .topology = DRAW_LINE_STRIP,
    .vertexCount = count,
    .vertices = vertices,
    .indexCount = indexCount,
    .indices = &indices,
    .baseVertex = &baseVertex
  });

  indices[0] = 0xffff;
  for (uint32_t i = 1; i < indexCount; i++) {
    indices[i] = baseVertex + i - 1;
  }
}

void lovrGraphicsPlane(DrawStyle style, Material* material, mat4 transform, float u, float v, float w, float h) {
  float* vertices = NULL;
  uint16_t* indices = NULL;
  uint16_t baseVertex;

  lovrGraphicsBatch(&(BatchRequest) {
    .type = BATCH_PLANE,
    .params.plane.style = style,
    .topology = style == STYLE_LINE ? DRAW_LINE_LOOP : DRAW_TRIANGLES,
    .material = material,
    .transform = transform,
    .vertexCount = 4,
    .indexCount = style == STYLE_LINE ? 5 : 6,
    .vertices = &vertices,
    .indices = &indices,
    .baseVertex = &baseVertex
  });

  if (style == STYLE_LINE) {
    static float vertexData[] = {
      -.5f,  .5f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f,
       .5f,  .5f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f,
       .5f, -.5f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f,
      -.5f, -.5f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f
    };

    memcpy(vertices, vertexData, sizeof(vertexData));

    indices[0] = 0xffff;
    indices[1] = 0 + baseVertex;
    indices[2] = 1 + baseVertex;
    indices[3] = 2 + baseVertex;
    indices[4] = 3 + baseVertex;
  } else {
    float vertexData[] = {
      -.5f,  .5f, 0.f, 0.f, 0.f, -1.f, u, v + h,
      -.5f, -.5f, 0.f, 0.f, 0.f, -1.f, u, v,
       .5f,  .5f, 0.f, 0.f, 0.f, -1.f, u + w, v + h,
       .5f, -.5f, 0.f, 0.f, 0.f, -1.f, u + w, v
    };

    memcpy(vertices, vertexData, sizeof(vertexData));

    static uint16_t indexData[] = { 0, 1, 2, 2, 1, 3 };

    for (size_t i = 0; i < sizeof(indexData) / sizeof(indexData[0]); i++) {
      indices[i] = indexData[i] + baseVertex;
    }
  }
}

void lovrGraphicsBox(DrawStyle style, Material* material, mat4 transform) {
  float* vertices = NULL;
  uint16_t* indices = NULL;
  uint16_t baseVertex;

  lovrGraphicsBatch(&(BatchRequest) {
    .type = BATCH_BOX,
    .params.box.style = style,
    .topology = style == STYLE_LINE ? DRAW_LINES : DRAW_TRIANGLES,
    .material = material,
    .transform = transform,
    .vertexCount = style == STYLE_LINE ? 8 : 24,
    .indexCount = style == STYLE_LINE ? 24 : 36,
    .vertices = &vertices,
    .indices = &indices,
    .baseVertex = &baseVertex,
    .instanced = true
  });

  if (vertices) {
    if (style == STYLE_LINE) {
      static float vertexData[] = {
        -.5f,  .5f, -.5f, 0.f, 0.f, 0.f, 0.f, 0.f, // Front
         .5f,  .5f, -.5f, 0.f, 0.f, 0.f, 0.f, 0.f,
         .5f, -.5f, -.5f, 0.f, 0.f, 0.f, 0.f, 0.f,
        -.5f, -.5f, -.5f, 0.f, 0.f, 0.f, 0.f, 0.f,
        -.5f,  .5f,  .5f, 0.f, 0.f, 0.f, 0.f, 0.f, // Back
         .5f,  .5f,  .5f, 0.f, 0.f, 0.f, 0.f, 0.f,
         .5f, -.5f,  .5f, 0.f, 0.f, 0.f, 0.f, 0.f,
        -.5f, -.5f,  .5f, 0.f, 0.f, 0.f, 0.f, 0.f
      };

      memcpy(vertices, vertexData, sizeof(vertexData));

      static uint16_t indexData[] = {
        0, 1, 1, 2, 2, 3, 3, 0, // Front
        4, 5, 5, 6, 6, 7, 7, 4, // Back
        0, 4, 1, 5, 2, 6, 3, 7  // Connections
      };

      for (size_t i = 0; i < sizeof(indexData) / sizeof(indexData[0]); i++) {
        indices[i] = indexData[i] + baseVertex;
      }
    } else {
      static float vertexData[] = {
        -.5f, -.5f, -.5f,  0.f,  0.f, -1.f, 0.f, 0.f, // Front
        -.5f,  .5f, -.5f,  0.f,  0.f, -1.f, 0.f, 1.f,
         .5f, -.5f, -.5f,  0.f,  0.f, -1.f, 1.f, 0.f,
         .5f,  .5f, -.5f,  0.f,  0.f, -1.f, 1.f, 1.f,
         .5f,  .5f, -.5f,  1.f,  0.f,  0.f, 0.f, 1.f, // Right
         .5f,  .5f,  .5f,  1.f,  0.f,  0.f, 1.f, 1.f,
         .5f, -.5f, -.5f,  1.f,  0.f,  0.f, 0.f, 0.f,
         .5f, -.5f,  .5f,  1.f,  0.f,  0.f, 1.f, 0.f,
         .5f, -.5f,  .5f,  0.f,  0.f,  1.f, 0.f, 0.f, // Back
         .5f,  .5f,  .5f,  0.f,  0.f,  1.f, 0.f, 1.f,
        -.5f, -.5f,  .5f,  0.f,  0.f,  1.f, 1.f, 0.f,
        -.5f,  .5f,  .5f,  0.f,  0.f,  1.f, 1.f, 1.f,
        -.5f,  .5f,  .5f, -1.f,  0.f,  0.f, 0.f, 1.f, // Left
        -.5f,  .5f, -.5f, -1.f,  0.f,  0.f, 1.f, 1.f,
        -.5f, -.5f,  .5f, -1.f,  0.f,  0.f, 0.f, 0.f,
        -.5f, -.5f, -.5f, -1.f,  0.f,  0.f, 1.f, 0.f,
        -.5f, -.5f, -.5f,  0.f, -1.f,  0.f, 0.f, 0.f, // Bottom
         .5f, -.5f, -.5f,  0.f, -1.f,  0.f, 1.f, 0.f,
        -.5f, -.5f,  .5f,  0.f, -1.f,  0.f, 0.f, 1.f,
         .5f, -.5f,  .5f,  0.f, -1.f,  0.f, 1.f, 1.f,
        -.5f,  .5f, -.5f,  0.f,  1.f,  0.f, 0.f, 1.f, // Top
        -.5f,  .5f,  .5f,  0.f,  1.f,  0.f, 0.f, 0.f,
         .5f,  .5f, -.5f,  0.f,  1.f,  0.f, 1.f, 1.f,
         .5f,  .5f,  .5f,  0.f,  1.f,  0.f, 1.f, 0.f
      };

      memcpy(vertices, vertexData, sizeof(vertexData));

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

      for (size_t i = 0; i < sizeof(indexData) / sizeof(indexData[0]); i++) {
        indices[i] = indexData[i] + baseVertex;
      }
    }
  }
}

void lovrGraphicsArc(DrawStyle style, ArcMode mode, Material* material, mat4 transform, float r1, float r2, int segments) {
  bool hasCenterPoint = false;

  if (fabsf(r1 - r2) >= 2.f * (float) M_PI) {
    r1 = 0.f;
    r2 = 2.f * (float) M_PI;
  } else {
    hasCenterPoint = mode == ARC_MODE_PIE;
  }

  uint32_t vertexCount = segments + 1 + hasCenterPoint;
  float* vertices = NULL;

  lovrGraphicsBatch(&(BatchRequest) {
    .type = BATCH_ARC,
    .params.arc.r1 = r1,
    .params.arc.r2 = r2,
    .params.arc.mode = mode,
    .params.arc.style = style,
    .params.arc.segments = segments,
    .topology = style == STYLE_LINE ? (mode == ARC_MODE_OPEN ? DRAW_LINE_STRIP : DRAW_LINE_LOOP) : DRAW_TRIANGLE_FAN,
    .material = material,
    .transform = transform,
    .vertexCount = vertexCount,
    .vertices = &vertices,
    .instanced = true
  });

  if (vertices) {
    if (hasCenterPoint) {
      memcpy(vertices, ((float[]) { 0.f, 0.f, 0.f, 0.f, 0.f, 1.f, .5f, .5f }), 8 * sizeof(float));
      vertices += 8;
    }

    float theta = r1;
    float angleShift = (r2 - r1) / (float) segments;

    for (int i = 0; i <= segments; i++) {
      float x = cosf(theta);
      float y = sinf(theta);
      memcpy(vertices, ((float[]) { x, y, 0.f, 0.f, 0.f, 1.f, (1.f + x) * .5f, (1.f - y) * .5f }), 8 * sizeof(float));
      vertices += 8;
      theta += angleShift;
    }
  }
}

void lovrGraphicsCircle(DrawStyle style, Material* material, mat4 transform, int segments) {
  lovrGraphicsArc(style, ARC_MODE_OPEN, material, transform, 0, 2.f * (float) M_PI, segments);
}

void lovrGraphicsCylinder(Material* material, mat4 transform, float r1, float r2, bool capped, int segments) {
  float length = vec3_length((float[4]) { transform[8], transform[9], transform[10] });
  r1 /= length;
  r2 /= length;

  uint32_t vertexCount = ((capped && r1) * (segments + 2) + (capped && r2) * (segments + 2) + 2 * (segments + 1));
  uint32_t indexCount = 3 * segments * ((capped && r1) + (capped && r2) + 2);
  float* vertices = NULL;
  uint16_t* indices = NULL;
  uint16_t baseVertex;

  lovrGraphicsBatch(&(BatchRequest) {
    .type = BATCH_CYLINDER,
    .params.cylinder.r1 = r1,
    .params.cylinder.r2 = r2,
    .params.cylinder.capped = capped,
    .params.cylinder.segments = segments,
    .topology = DRAW_TRIANGLES,
    .material = material,
    .transform = transform,
    .vertexCount = vertexCount,
    .indexCount = indexCount,
    .vertices = &vertices,
    .indices = &indices,
    .baseVertex = &baseVertex,
    .instanced = true
  });

  if (vertices) {
    float* v = vertices;

    // Ring
    for (int i = 0; i <= segments; i++) {
      float t = (float) i / segments;
      float theta = t * (2 * M_PI);
      float X = cosf(theta);
      float Y = sinf(theta);
      memcpy(vertices, (float[16]) {
        r1 * X, r1 * Y, -.5f, X, Y, 0.f, 1.f - t, 1.f,
        r2 * X, r2 * Y,  .5f, X, Y, 0.f, 1.f - t, 0.f
      }, 16 * sizeof(float));
      vertices += 16;
    }

    // Top
    int top = (segments + 1) * 2 + baseVertex;
    if (capped && r1 != 0) {
      memcpy(vertices, (float[8]) { 0.f, 0.f, -.5f, 0.f, 0.f, -1.f, .5f, .5f }, 8 * sizeof(float));
      vertices += 8;
      for (int i = 0; i <= segments; i++) {
        int j = i * 2 * 8;
        float x = v[j + 0];
        float y = v[j + 1];
        float z = v[j + 2];
        float u = 1.f - (x / r1 * .5 + .5);
        float v = y / r1 * .5 + .5;
        memcpy(vertices, (float[8]) { x, y, z, 0.f, 0.f, -1.f, u, v }, 8 * sizeof(float));
        vertices += 8;
      }
    }

    // Bottom
    int bot = (segments + 1) * 2 + (1 + segments + 1) * (capped && r1 != 0) + baseVertex;
    if (capped && r2 != 0) {
      memcpy(vertices, (float[8]) { 0.f, 0.f, .5f, 0.f, 0.f, 1.f, .5f, .5f }, 8 * sizeof(float));
      vertices += 8;
      for (int i = 0; i <= segments; i++) {
        int j = i * 2 * 8 + 8;
        float x = v[j + 0];
        float y = v[j + 1];
        float z = v[j + 2];
        float u = x / r1 * .5 + .5;
        float v = y / r1 * .5 + .5;
        memcpy(vertices, (float[8]) { x, y, z, 0.f, 0.f, 1.f, u, v }, 8 * sizeof(float));
        vertices += 8;
      }
    }

    // Indices
    for (int i = 0; i < segments; i++) {
      int j = 2 * i + baseVertex;
      memcpy(indices, (uint16_t[6]) { j, j + 2, j + 1, j + 1, j + 2, j + 3 }, 6 * sizeof(uint16_t));
      indices += 6;

      if (capped && r1 != 0.f) {
        memcpy(indices, (uint16_t[3]) { top, top + i + 2, top + i + 1 }, 3 * sizeof(uint16_t));
        indices += 3;
      }

      if (capped && r2 != 0.f) {
        memcpy(indices, (uint16_t[3]) { bot, bot + i + 1, bot + i + 2 }, 3 * sizeof(uint16_t));
        indices += 3;
      }
    }
  }
}

void lovrGraphicsSphere(Material* material, mat4 transform, int segments) {
  float* vertices = NULL;
  uint16_t* indices = NULL;
  uint16_t baseVertex;

  lovrGraphicsBatch(&(BatchRequest) {
    .type = BATCH_SPHERE,
    .params.sphere.segments = segments,
    .topology = DRAW_TRIANGLES,
    .material = material,
    .transform = transform,
    .vertexCount = (segments + 1) * (segments + 1),
    .indexCount = segments * segments * 6,
    .vertices = &vertices,
    .indices = &indices,
    .baseVertex = &baseVertex,
    .instanced = true
  });

  if (vertices) {
    for (int i = 0; i <= segments; i++) {
      float v = i / (float) segments;
      float sinV = sinf(v * (float) M_PI);
      float cosV = cosf(v * (float) M_PI);
      for (int k = 0; k <= segments; k++) {
        float u = k / (float) segments;
        float x = sinf(u * 2.f * (float) M_PI) * sinV;
        float y = cosV;
        float z = -cosf(u * 2.f * (float) M_PI) * sinV;
        memcpy(vertices, ((float[8]) { x, y, z, x, y, z, u, 1.f - v }), 8 * sizeof(float));
        vertices += 8;
      }
    }

    for (int i = 0; i < segments; i++) {
      uint16_t offset0 = i * (segments + 1) + baseVertex;
      uint16_t offset1 = (i + 1) * (segments + 1) + baseVertex;
      for (int j = 0; j < segments; j++) {
        uint16_t i0 = offset0 + j;
        uint16_t i1 = offset1 + j;
        memcpy(indices, ((uint16_t[]) { i0, i0 + 1, i1, i1, i0 + 1, i1 + 1 }), 6 * sizeof(uint16_t));
        indices += 6;
      }
    }
  }
}

void lovrGraphicsSkybox(Texture* texture) {
  TextureType type = lovrTextureGetType(texture);
  lovrAssert(type == TEXTURE_CUBE || type == TEXTURE_2D, "Only 2D and cube textures can be used as skyboxes");

  Pipeline pipeline = state.pipeline;
  pipeline.winding = WINDING_COUNTERCLOCKWISE;

  float* vertices = NULL;

  lovrGraphicsBatch(&(BatchRequest) {
    .type = BATCH_SKYBOX,
    .topology = DRAW_TRIANGLE_STRIP,
    .shader = type == TEXTURE_CUBE ? SHADER_CUBE : SHADER_PANO,
    .pipeline = &pipeline,
    .texture = texture,
    .vertexCount = 4,
    .vertices = &vertices,
    .instanced = true
  });

  if (vertices) {
    static float vertexData[] = {
      -1.f,  1.f, 1.f, 0.f, 0.f, 0.f, 0.f, 0.f,
      -1.f, -1.f, 1.f, 0.f, 0.f, 0.f, 0.f, 0.f,
       1.f,  1.f, 1.f, 0.f, 0.f, 0.f, 0.f, 0.f,
       1.f, -1.f, 1.f, 0.f, 0.f, 0.f, 0.f, 0.f
    };

    memcpy(vertices, vertexData, sizeof(vertexData));
  }
}

void lovrGraphicsPrint(const char* str, size_t length, mat4 transform, float wrap, HorizontalAlign halign, VerticalAlign valign) {
  float width;
  float lastLineWidth;
  float height;
  uint32_t lineCount;
  uint32_t glyphCount;
  Font* font = lovrGraphicsGetFont();
  lovrFontMeasure(font, str, length, wrap, &width, &lastLineWidth, &height, &lineCount, &glyphCount);

  if (glyphCount == 0) {
    return;
  }

  float scale = 1.f / lovrFontGetPixelDensity(font);
  mat4_scale(transform, scale, scale, scale);
  mat4_translate(transform, 0.f, height * (valign / 2.f), 0.f);

  Pipeline pipeline = state.pipeline;
  pipeline.blendMode = pipeline.blendMode == BLEND_NONE ? BLEND_ALPHA : pipeline.blendMode;

  float* vertices;
  uint16_t* indices;
  uint16_t baseVertex;
  lovrGraphicsBatch(&(BatchRequest) {
    .type = BATCH_TEXT,
    .params.text.spread = lovrFontGetSpread(font),
    .topology = DRAW_TRIANGLES,
    .shader = SHADER_FONT,
    .pipeline = &pipeline,
    .transform = transform,
    .texture = lovrFontGetTexture(font),
    .vertexCount = glyphCount * 4,
    .indexCount = glyphCount * 6,
    .vertices = &vertices,
    .indices = &indices,
    .baseVertex = &baseVertex
  });

  lovrFontRender(font, str, length, wrap, halign, vertices, indices, baseVertex);
}

void lovrGraphicsFill(Texture* texture, float u, float v, float w, float h) {
  Pipeline pipeline = state.pipeline;
  pipeline.depthTest = COMPARE_NONE;
  pipeline.depthWrite = false;

  float* vertices = NULL;

  lovrGraphicsBatch(&(BatchRequest) {
    .type = BATCH_FILL,
    .params.fill = { .u = u, .v = v, .w = w, .h = h },
    .topology = DRAW_TRIANGLE_STRIP,
    .shader = SHADER_FILL,
    .texture = texture,
    .pipeline = &pipeline,
    .vertexCount = 4,
    .vertices = &vertices
  });

  if (vertices) {
    memcpy(vertices, (float[32]) {
      -1.f,  1.f, 0.f, 0.f, 0.f, 0.f, u, v + h,
      -1.f, -1.f, 0.f, 0.f, 0.f, 0.f, u, v,
       1.f,  1.f, 0.f, 0.f, 0.f, 0.f, u + w, v + h,
       1.f, -1.f, 0.f, 0.f, 0.f, 0.f, u + w, v
    }, 32 * sizeof(float));
  }
}

void lovrGraphicsDrawMesh(Mesh* mesh, mat4 transform, uint32_t instances, float* pose) {
  uint32_t vertexCount = lovrMeshGetVertexCount(mesh);
  uint32_t indexCount = lovrMeshGetIndexCount(mesh);
  uint32_t defaultCount = indexCount > 0 ? indexCount : vertexCount;
  uint32_t rangeStart, rangeCount;
  lovrMeshGetDrawRange(mesh, &rangeStart, &rangeCount);
  rangeCount = rangeCount > 0 ? rangeCount : defaultCount;
  DrawMode mode = lovrMeshGetDrawMode(mesh);
  Material* material = lovrMeshGetMaterial(mesh);

  lovrGraphicsBatch(&(BatchRequest) {
    .type = BATCH_MESH,
    .params.mesh.rangeStart = rangeStart,
    .params.mesh.rangeCount = rangeCount,
    .params.mesh.instances = instances,
    .params.mesh.pose = pose,
    .mesh = mesh,
    .topology = mode,
    .transform = transform,
    .material = material,
    .instanced = instances <= 1
  });
}
