#include "renderer.h"

u32 gl_shader_program(char* vs, char* fs)
{
  int status;
  char info_log[512];
  
  
  // =============
  // vertex shader
  u32 vertex_shader = glCreateShader(GL_VERTEX_SHADER);
  glShaderSource(vertex_shader, 1, &vs, NULL);
  glCompileShader(vertex_shader);
  
  glGetShaderiv(vertex_shader, GL_COMPILE_STATUS, &status);
  if (status == 0)
  {
    glGetShaderInfoLog(vertex_shader, 512, NULL, info_log);
    printf("== ERROR: Vertex Shader Compilation Failed ==\n");
    printf("%s\n", info_log);
  }
  
  
  // ===============
  // fragment shader
  u32 fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
  glShaderSource(fragment_shader, 1, &fs, NULL);
  glCompileShader(fragment_shader);
  
  glGetShaderiv(fragment_shader, GL_COMPILE_STATUS, &status);
  if (status == 0)
  {
    glGetShaderInfoLog(fragment_shader, 512, NULL, info_log);
    printf("== ERROR: Fragment Shader Compilation Failed ==\n");
    printf("%s\n", info_log);
  }
  
  
  // ==============
  // shader program
  u32 shader_program = glCreateProgram();
  
  glAttachShader(shader_program, vertex_shader);
  glAttachShader(shader_program, fragment_shader);
  glLinkProgram(shader_program);
  
  glGetProgramiv(shader_program, GL_LINK_STATUS, &status);
  if(status == 0)
  {
    glGetProgramInfoLog(shader_program, 512, NULL, info_log);
    printf("== ERROR: Shader Program Linking Failed\n");
    printf("%s\n", info_log);
  }
  
  glDeleteShader(vertex_shader);
  glDeleteShader(fragment_shader);
  
  return shader_program;
}

u32 gl_shader_program_from_path(const char* vspath, const char* fspath)
{
  size_t read_count;
  char* vs = (char*)SDL_LoadFile(vspath, &read_count);
  if (read_count == 0)
  {
    printf("Error! Failed to read vertex shader file at path %s\n", vspath);
    return 0;
  }
  
  char* fs = (char*)SDL_LoadFile(fspath, &read_count);
  if (read_count == 0)
  {
    printf("Error! Failed to read fragment shader file at path %s\n", vspath);
    return 0;
  }
  
  u32 shader_program = gl_shader_program(vs, fs);

  SDL_free(vs);
  SDL_free(fs);
  return shader_program;
}

u32 gl_setup_colored_quad(u32 sp)
{
  // @todo: make this use index buffer maybe?
  r32 vertices[] = {
    -1.0f, -1.0f,  0.0f,  // bottom-left
    1.0f, -1.0f,  0.0f,  // bottom-right
    1.0f,  1.0f,  0.0f,  // top-right
    1.0f,  1.0f,  0.0f,  // top-right
    -1.0f,  1.0f,  0.0f,  // top-left
    -1.0f, -1.0f,  0.0f,  // bottom-left
  };
  u32 vao, vbo;
  glGenVertexArrays(1, &vao);
  glGenBuffers(1, &vbo);
  
  glBindVertexArray(vao);
  glBindBuffer(GL_ARRAY_BUFFER, vbo);
  glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), &vertices, GL_STATIC_DRAW);
  glEnableVertexAttribArray(0);
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(r32), (void*)0);
  
  glBindVertexArray(0);
  
  // now return or store the vao, vbo state somewhere
  return vao;
}

void gl_draw_colored_quad(
  GLRenderer* renderer,
  Vec3 position,
  Vec2 size,
  Vec3 color
) {
  glEnable(GL_DEPTH_TEST);
  glUseProgram(renderer->cq_sp);
  if (renderer->cq_init == 0)
  {
    glUniformMatrix4fv(
      glGetUniformLocation(renderer->cq_sp, "Projection"), 
      1, GL_FALSE, (renderer->cam_proj).buffer
    );
    renderer->cq_init = 1;
  }
  // setting quad size
  Mat4 model = diag4m(1.0);
  Mat4 scale = scaling_matrix4m(size.x, size.y, 0.0f);
  model = multiply4m(scale, model);
  // setting quad position
  Mat4 translation = translation_matrix4m(position.x, position.y, position.z);
  model = multiply4m(translation, model);
  // setting color
  glUniform3fv(glGetUniformLocation(renderer->cq_sp, "Color"), 1, color.data);
  
  glUniformMatrix4fv(
    glGetUniformLocation(renderer->cq_sp, "Model"), 
    1, GL_FALSE, model.buffer
  );

  glUniformMatrix4fv(
    glGetUniformLocation(renderer->cq_sp, "View"), 
    1, GL_FALSE, (renderer->cam_view).buffer
  );
  
  glBindVertexArray(renderer->cq_vao);
  glDrawArrays(GL_TRIANGLES, 0, 6);
}

void gl_setup_colored_quad_optimized(
  GLRenderer* renderer,
  u32 sp
) {
  // @todo: make this use index buffer maybe?
  glGenVertexArrays(1, &renderer->cq_batch_vao);
  glGenBuffers(1, &renderer->cq_batch_vbo);
  
  glBindVertexArray(renderer->cq_batch_vao);
  glBindBuffer(GL_ARRAY_BUFFER, renderer->cq_batch_vbo);
  glBufferData(
    GL_ARRAY_BUFFER, (
      renderer->cq_pos_batch.capacity + renderer->cq_color_batch.capacity
    ) * sizeof(r32), NULL, GL_DYNAMIC_DRAW
  );

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

  glEnableVertexAttribArray(1);
  glVertexAttribPointer(
    1, 3, GL_FLOAT, GL_FALSE, 
    3 * sizeof(r32), (void*)(renderer->cq_pos_batch.capacity*sizeof(r32))
  );
  
  glBindVertexArray(0);
}

void gl_draw_colored_quad_optimized(
  GLRenderer* renderer,
  Vec3 position,
  Vec2 size,
  Vec3 color
) {
  Vec4 vertices[6] = {
    Vec4{-1.0f, -1.0f,  0.0f, 1.0f},// bottom-left
    Vec4{ 1.0f, -1.0f,  0.0f, 1.0f},// bottom-right
    Vec4{ 1.0f,  1.0f,  0.0f, 1.0f},// top-right
    Vec4{ 1.0f,  1.0f,  0.0f, 1.0f},// top-right
    Vec4{-1.0f,  1.0f,  0.0f, 1.0f},// top-left
    Vec4{-1.0f, -1.0f,  0.0f, 1.0f} // bottom-left
  };

  // setting quad size
  Mat4 model = diag4m(1.0);
  // @note: this is necessary because clip space is {-1,-1} to {1,1}
  // So, by scaling with the original size we get scaling range of {-32, -32}, {32, 32}
  // doubling our size.
  Mat4 scale = scaling_matrix4m(size.x/2.0f, size.y/2.0f, 0.0f);
  model = multiply4m(scale, model);
  // setting quad position
  Mat4 translation = translation_matrix4m(position.x, position.y, position.z);
  model = multiply4m(translation, model);

  Vec4 model_pos;
  model_pos = multiply4mv(model, vertices[0]);
  vertices[0] = model_pos;
  model_pos = multiply4mv(model, vertices[1]);
  vertices[1] = model_pos;
  model_pos = multiply4mv(model, vertices[2]);
  vertices[2] = model_pos;
  model_pos = multiply4mv(model, vertices[3]);
  vertices[3] = model_pos;
  model_pos = multiply4mv(model, vertices[4]);
  vertices[4] = model_pos;
  model_pos = multiply4mv(model, vertices[5]);
  vertices[5] = model_pos;
  
  array_insert(&renderer->cq_pos_batch, vertices[0].data, 4);
  array_insert(&renderer->cq_pos_batch, vertices[1].data, 4);
  array_insert(&renderer->cq_pos_batch, vertices[2].data, 4);
  array_insert(&renderer->cq_pos_batch, vertices[3].data, 4);
  array_insert(&renderer->cq_pos_batch, vertices[4].data, 4);
  array_insert(&renderer->cq_pos_batch, vertices[5].data, 4);

  // initialise color to be per vertex to allow batching
  array_insert(&renderer->cq_color_batch, color.data, 3);
  array_insert(&renderer->cq_color_batch, color.data, 3);
  array_insert(&renderer->cq_color_batch, color.data, 3);
  array_insert(&renderer->cq_color_batch, color.data, 3);
  array_insert(&renderer->cq_color_batch, color.data, 3);
  array_insert(&renderer->cq_color_batch, color.data, 3);

  renderer->cq_batch_count++;

  if(renderer->cq_batch_count == BATCH_SIZE) {
    gl_cq_flush(renderer);
  }
}

void gl_cq_flush(GLRenderer* renderer) {
  glUseProgram(renderer->cq_batch_sp);
  glEnable(GL_DEPTH_TEST);

  glUniformMatrix4fv(
    glGetUniformLocation(renderer->cq_batch_sp, "View"),
    1, GL_FALSE, (renderer->cam_view).buffer
  );

  glUniformMatrix4fv(
    glGetUniformLocation(renderer->cq_batch_sp, "Projection"), 
    1, GL_FALSE, (renderer->cam_proj).buffer
  );

  glBindBuffer(GL_ARRAY_BUFFER, renderer->cq_batch_vbo);

  // fill batch data
  // position batch
  glBufferSubData(
    GL_ARRAY_BUFFER, 
    0, 
    renderer->cq_pos_batch.capacity*sizeof(r32), 
    (void*)renderer->cq_pos_batch.buffer
  );

  // color batch
  glBufferSubData(
    GL_ARRAY_BUFFER, 
    renderer->cq_pos_batch.capacity*sizeof(r32), 
    renderer->cq_color_batch.capacity*sizeof(r32), 
    (void*)renderer->cq_color_batch.buffer
  );

  glBindVertexArray(renderer->cq_batch_vao);
  glDrawArrays(GL_TRIANGLES, 0, renderer->cq_batch_count*6);

  array_clear(&renderer->cq_pos_batch);
  array_clear(&renderer->cq_color_batch);
  renderer->cq_batch_count = 0;
}

void gl_setup_line(GLRenderer* renderer, u32 sp) {
    glGenVertexArrays(1, &renderer->line_vao);
    glGenBuffers(1, &renderer->line_vbo);

    glBindVertexArray(renderer->line_vao);
    glBindBuffer(GL_ARRAY_BUFFER, renderer->line_vbo);
    glBufferData(
	    GL_ARRAY_BUFFER, (
		renderer->line_pos_batch.capacity + 
		renderer->line_color_batch.capacity
		) * sizeof(r32), NULL, GL_DYNAMIC_DRAW
	    );

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

    glEnableVertexAttribArray(1);
    glVertexAttribPointer(
	    1, 3, GL_FLOAT, GL_FALSE, 
	    3 * sizeof(r32), (void*)(
		renderer->line_pos_batch.capacity*sizeof(r32)
		)
	    );

    glBindVertexArray(0);
}

void gl_draw_line(
	GLRenderer *renderer,
	Vec3 start,
	Vec3 end,
	Vec3 color
	) {

    Vec4 vertices[2] = {
	Vec4{start.x, start.y, start.z, 1.0f},
	Vec4{end.x, end.y, end.z, 1.0f}
    };

    array_insert(&renderer->line_pos_batch, vertices[0].data, 4);
    array_insert(&renderer->line_pos_batch, vertices[1].data, 4);
    array_insert(&renderer->line_color_batch, color.data, 3);
    array_insert(&renderer->line_color_batch, color.data, 3);

    renderer->line_batch_count++;
    if(renderer->line_batch_count == BATCH_SIZE) {
	gl_line_flush(renderer);
    }
}

void gl_line_flush(GLRenderer *renderer) {
    glUseProgram(renderer->line_sp);
    glEnable(GL_DEPTH_TEST);

    glUniformMatrix4fv(
	    glGetUniformLocation(renderer->line_sp, "View"),
	    1, GL_FALSE, (renderer->cam_view).buffer
	    );
    glUniformMatrix4fv(
	    glGetUniformLocation(renderer->line_sp, "Projection"),
	    1, GL_FALSE, (renderer->cam_proj).buffer
	    );
    glBindBuffer(GL_ARRAY_BUFFER, renderer->line_vbo);

    // fill batch data
    // position batch
    glBufferSubData(
	    GL_ARRAY_BUFFER,
	    0,
	    renderer->line_pos_batch.capacity*sizeof(r32),
	    (void*)renderer->line_pos_batch.buffer
	    );
    glBufferSubData(
	    GL_ARRAY_BUFFER,
	    renderer->line_pos_batch.capacity*sizeof(r32),
	    renderer->line_color_batch.capacity*sizeof(r32),
	    (void*)renderer->line_color_batch.buffer
	    );

    glBindVertexArray(renderer->line_vao);
    glDrawArrays(GL_LINES, 0, renderer->line_batch_count*2);

    array_clear(&renderer->line_pos_batch);
    array_clear(&renderer->line_color_batch);
    renderer->line_batch_count = 0;
}

void gl_setup_text(TextState *uistate) {
    uistate->scale = stbtt_ScaleForPixelHeight(&uistate->font, uistate->pixel_size);
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

    glGenTextures(1, &(uistate->texture_atlas_id));
    glBindTexture(GL_TEXTURE_2D_ARRAY, uistate->texture_atlas_id);

    // generate texture
    glTexImage3D(
            GL_TEXTURE_2D_ARRAY,
            0,
            GL_R8,
            uistate->pixel_size,
            uistate->pixel_size,
            128,
            0,
            GL_RED,
            GL_UNSIGNED_BYTE,
            0);

    // font vmetrics
    s32 ascent, descent, linegap = 0;
    stbtt_GetFontVMetrics(&uistate->font, &ascent, &descent, &linegap);
    uistate->ascent = ascent;
    uistate->descent = descent;
    uistate->linegap = linegap;

    // font bounding box
    s32 x0, y0, x1, y1 = 0;
    stbtt_GetFontBoundingBox(&uistate->font, &x0, &y0, &x1, &y1);
    uistate->bbox0 = IVec2{x0, y0};
    uistate->bbox1 = IVec2{x1, y1};

    // generate bitmaps
    u32 pixel_size = uistate->pixel_size;
    unsigned char *bitmap_buffer = (unsigned char*)calloc(pixel_size * pixel_size, sizeof(unsigned char));
    for (u32 c = 0; c < 128; c++)
    {
        s32 advance, lsb = 0;
        stbtt_GetCodepointHMetrics(&uistate->font, c, &advance, &lsb);
	s32 bx0, bx1, by0, by1 = 0;
	stbtt_GetCodepointBitmapBox(
		&uistate->font, c,
		uistate->scale, uistate->scale,
		&bx0, &by0,
		&bx1, &by1
		);

	s32 width = bx1 - bx0;
	s32 height = by1 - by0;

        stbtt_MakeCodepointBitmap(
                &uistate->font, 
		bitmap_buffer,
		width,
		height,
		width,
                uistate->scale, 
                uistate->scale,
                c);

        glTexSubImage3D(
		GL_TEXTURE_2D_ARRAY,
		0,
		0, 0, // x, y offset
		int(c),
		width,
          	height,
          	1,
          	GL_RED,
          	GL_UNSIGNED_BYTE,
          	bitmap_buffer
		);
        // set texture options
        glTexParameteri(
		GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE
	);
        glTexParameteri(
                GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE
                );
        glTexParameteri(
                GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR
                );
        glTexParameteri(
                GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR
                );

        TextChar tc;
        tc.size = Vec2{
            (r32)width, (r32)height
        };
	tc.bbox0 = Vec2{
	    (r32)bx0, (r32)by0
	};
	tc.bbox1 = Vec2{
	    (r32)bx1, (r32)by1
	};
        tc.advance = advance;
	tc.lsb = (s32)lsb;
        uistate->char_map[c] = tc;
    }

    glBindTexture(GL_TEXTURE_2D_ARRAY, 0);

    r32 vertices[] = {
        0.0f, 1.0f,
        0.0f, 0.0f,
        1.0f, 1.0f,
        1.0f, 0.0f
    };

    glGenVertexArrays(1, &(uistate->vao));
    glGenBuffers(1, &(uistate->vbo));

    glBindVertexArray(uistate->vao);
    glBindBuffer(GL_ARRAY_BUFFER, uistate->vbo);
    glBufferData(
            GL_ARRAY_BUFFER, 
	    sizeof(vertices), 
	    vertices, 
	    GL_STATIC_DRAW);

    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, 0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
}

void gl_render_text(
	GLRenderer *renderer, 
	char *text,
	Vec2 position, 
	Vec3 color, 
	r32 font_size) {
    // render_text
    glDisable(GL_DEPTH_TEST);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    glUseProgram(renderer->ui_text.sp);
    if (renderer->ui_cam_update) {

	glUniformMatrix4fv(
		glGetUniformLocation(
		    renderer->ui_text.sp, "View"),
		1, GL_FALSE, renderer->ui_cam_view.buffer);

	glUniformMatrix4fv(
		glGetUniformLocation(
		    renderer->ui_text.sp, "Projection"),
		1, GL_FALSE, renderer->cam_proj.buffer);

	renderer->ui_cam_update = 0;
    }
    glUniform3fv(
	    glGetUniformLocation(
		renderer->ui_text.sp, "TextColor"),
	    1, color.data);
    glBindVertexArray(renderer->ui_text.vao);
    glBindTexture(
	    GL_TEXTURE_2D_ARRAY, 
	    renderer->ui_text.texture_atlas_id);
    glBindBuffer(GL_ARRAY_BUFFER, renderer->ui_text.vbo);
    glActiveTexture(GL_TEXTURE0);

    u32 running_index = 0;
    r32 startx = position.x;
    r32 starty = position.y;
    r32 linex = startx;
    r32 liney = starty;
    r32 render_scale = font_size/(r32)renderer->ui_text.pixel_size;
    r32 font_scale = renderer->ui_text.scale*render_scale;
    memset(renderer->ui_text.transforms, 0, sizeof(Mat4)*renderer->ui_text.chunk_size);
    memset(renderer->ui_text.char_indexes, 0, sizeof(s32)*renderer->ui_text.chunk_size);

    char *char_iter = text;
    r32 baseline = -renderer->ui_text.bbox0.y*font_scale - font_size;
    while (*char_iter != '\0') {
	TextChar render_char = renderer->ui_text.char_map[*char_iter];
	if (*char_iter == ' ') {
	    linex += (font_scale * render_char.advance);
	    char_iter++;
	    continue;
	}
	if (*char_iter == '\t') {
	    linex += (font_scale * render_char.advance);
	    char_iter++;
	    continue;
	}
	if (*char_iter == '\n') {
	    linex = startx;
	    liney = liney - font_scale * (renderer->ui_text.ascent - renderer->ui_text.descent + renderer->ui_text.linegap);
	    char_iter++;
	    continue;
	}
	r32 xpos = linex + (font_scale * render_char.lsb);
	r32 ypos = liney + (baseline - render_scale*render_char.bbox0.y);

	Mat4 sc = scaling_matrix4m(font_size, font_size, 1.0f);
	Mat4 tr = translation_matrix4m(xpos, ypos, 0);
	Mat4 model = multiply4m(tr, sc);
	renderer->ui_text.transforms[running_index] = model;
	renderer->ui_text.char_indexes[running_index] = 
	    int(*char_iter);

	linex += (font_scale * render_char.advance);
	char prev_char = *char_iter;
	char_iter++;
	char curr_char = *char_iter;

	if (curr_char) {
	    r32 kern = font_scale * stbtt_GetCodepointKernAdvance(&renderer->ui_text.font, prev_char, curr_char);
	    linex += kern;
	}
	running_index++;
	if (running_index >= renderer->ui_text.chunk_size) {
	    gl_text_flush(renderer, running_index);
	    running_index = 0;
	}
    }
    gl_text_flush(renderer, running_index);
}

void gl_text_flush(GLRenderer *renderer, u32 render_count) {
    s32 transform_loc = glGetUniformLocation(
	    renderer->ui_text.sp, "LetterTransforms");
    glUniformMatrix4fv(
	    transform_loc, 
	    render_count,
	    GL_FALSE, 
	    &(renderer->ui_text.transforms[0].buffer[0])
	    );

    s32 texture_map_loc = glGetUniformLocation(
	    renderer->ui_text.sp, "TextureMap");
    glUniform1iv(
	    texture_map_loc, 
	    render_count, 
	    renderer->ui_text.char_indexes);

    glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, render_count);

    memset(renderer->ui_text.transforms, 0, sizeof(Mat4)*renderer->ui_text.chunk_size);
    memset(renderer->ui_text.char_indexes, 0, sizeof(s32)*renderer->ui_text.chunk_size);
}