#include <emscripten/html5_webgl.h>
#include <emscripten/html5.h>
#include <webgl/webgl2.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>

GLuint compile_shader(GLenum shaderType, const char *src) {
  GLuint shader = glCreateShader(shaderType);
  glShaderSource(shader, 1, &src, NULL);
  glCompileShader(shader);
  return shader;
}

GLuint create_program(GLuint vertexShader, GLuint fragmentShader) {
  GLuint program = glCreateProgram();
  glAttachShader(program, vertexShader);
  glAttachShader(program, fragmentShader);
  glBindAttribLocation(program, 0, "apos");
  glBindAttribLocation(program, 1, "acolor");
  glLinkProgram(program);
  return program;
}

int main() {
  EmscriptenWebGLContextAttributes attrs;
  emscripten_webgl_init_context_attributes(&attrs);
  attrs.majorVersion = 2;
  attrs.proxyContextToMainThread = EMSCRIPTEN_WEBGL_CONTEXT_PROXY_FALLBACK;
  attrs.renderViaOffscreenBackBuffer = EM_TRUE;
  EMSCRIPTEN_WEBGL_CONTEXT_HANDLE context = emscripten_webgl_create_context("#canvas", &attrs);
  emscripten_webgl_make_context_current(context);

  // Test emscripten_webgl_get_supported_extensions() API
  char *extensions = emscripten_webgl_get_supported_extensions();
  assert(extensions);
  assert(strlen(extensions) > 0);
  assert(strstr(extensions, "WEBGL") != 0);
  free(extensions);

  // Test emscripten_webgl_get_parameter_d() API
  assert(emscripten_webgl_get_parameter_d(GL_BLUE_BITS) == 8);

  // Test emscripten_webgl_get_parameter_o() API
  GLuint buf;
  glGenBuffers(1, &buf);
  assert(buf != 0);
  glBindBuffer(GL_ARRAY_BUFFER, buf);
  assert(emscripten_webgl_get_parameter_o(GL_ARRAY_BUFFER_BINDING) == buf);

  // Test emscripten_webgl_get_parameter_utf8() API
  char *version = emscripten_webgl_get_parameter_utf8(GL_VERSION);
  assert(version);
  assert(strstr(version, "WebGL") != 0);
  free(version);

  // Test emscripten_webgl_get_parameter_i64v API
  int64_t components = 0;
  emscripten_webgl_get_parameter_i64v(GL_MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS, &components);
  assert(components > 0);

  // Test emscripten_webgl_get_parameter_v API with truncated read length
  glViewport(1, 2, 3, 4);
  int viewport[4] = { 0, 0, 0, 0 };
  int numElements = emscripten_webgl_get_parameter_v(GL_VIEWPORT, viewport, 3, EMSCRIPTEN_WEBGL_PARAM_TYPE_INT);
  assert(numElements == 4);
  assert(viewport[0] == 1);
  assert(viewport[1] == 2);
  assert(viewport[2] == 3);
  assert(viewport[3] == 0);

  // Test emscripten_webgl_get_parameter_v API with full read length
  numElements = emscripten_webgl_get_parameter_v(GL_VIEWPORT, viewport, 4, EMSCRIPTEN_WEBGL_PARAM_TYPE_INT);
  assert(numElements == 4);
  assert(viewport[0] == 1);
  assert(viewport[1] == 2);
  assert(viewport[2] == 3);
  assert(viewport[3] == 4);

  // Test emscripten_webgl_get_vertex_attrib_d() API
  glEnableVertexAttribArray(0);
  double enabled = emscripten_webgl_get_vertex_attrib_d(0, GL_VERTEX_ATTRIB_ARRAY_ENABLED);
  assert(enabled == 1.0);

  // Test emscripten_webgl_get_vertex_attrib_v() API with truncated read length
  glVertexAttrib4f(0, 0.2f, 0.4f, 0.6f, 0.8f);
  float data[4] = { 0, 0, 0, 0 };
  numElements = emscripten_webgl_get_vertex_attrib_v(0, GL_CURRENT_VERTEX_ATTRIB, data, 2, EMSCRIPTEN_WEBGL_PARAM_TYPE_FLOAT);
  assert(numElements == 4);
  assert(data[0] == 0.2f);
  assert(data[1] == 0.4f);
  assert(data[2] == 0.0f);
  assert(data[3] == 0.0f);

  // Test emscripten_webgl_get_vertex_attrib_v() API with full read length
  numElements = emscripten_webgl_get_vertex_attrib_v(0, GL_CURRENT_VERTEX_ATTRIB, data, 4, EMSCRIPTEN_WEBGL_PARAM_TYPE_FLOAT);
  assert(numElements == 4);
  assert(data[0] == 0.2f);
  assert(data[1] == 0.4f);
  assert(data[2] == 0.6f);
  assert(data[3] == 0.8f);

  // Test emscripten_webgl_get_vertex_attrib_o() API
  glBufferData(GL_ARRAY_BUFFER, 4, data, GL_STATIC_DRAW);
  glVertexAttribPointer(0, 1, GL_FLOAT, GL_FALSE, 0, 0);
  assert(emscripten_webgl_get_vertex_attrib_o(0, GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING) == buf);

  static const char vertex_shader[] =
    "#version 100\n"
    "attribute vec4 apos;"
    "attribute vec4 acolor;"
    "uniform float vsUniformFloat;"
    "varying vec4 color;"
    "void main() {"
      "color = acolor * vsUniformFloat;"
      "gl_Position = apos;"
    "}";
  GLuint vs = compile_shader(GL_VERTEX_SHADER, vertex_shader);

  // Test emscripten_webgl_get_shader_parameter_d() API
  assert(emscripten_webgl_get_shader_parameter_d(vs, GL_DELETE_STATUS) == 0);
  assert(emscripten_webgl_get_shader_parameter_d(vs, GL_COMPILE_STATUS) == 1);
  assert(emscripten_webgl_get_shader_parameter_d(vs, GL_SHADER_TYPE) == GL_VERTEX_SHADER);

  // Test emscripten_webgl_get_shader_source_utf8() API
  char *src = emscripten_webgl_get_shader_source_utf8(vs);
  assert(src);
  assert(strstr(src, "attribute") != 0);
  free(src);

  static const char failing_shader[] =
    "#version 100\n"
    "attribute vec4 apos;"
    "attr_error_ibute vec4 acolor;"
    "varying vec4 color;"
    "void main() {"
      "color = acolor;"
      "gl_Position = apos;"
    "}";
  GLuint vs2 = compile_shader(GL_VERTEX_SHADER, failing_shader);

  // Test emscripten_webgl_get_shader_info_log_utf8() API
  src = emscripten_webgl_get_shader_info_log_utf8(vs2);
  assert(src);
  assert(strstr(src, "attr_error_ibute") != 0);
  free(src);

  static const char fragment_shader[] =
    "#version 100\n"
    "precision lowp float;"
    "varying vec4 color;"
    "uniform vec4 psUniformVec;"
    "void main() {"
      "gl_FragColor = color + psUniformVec;"
    "}";
  GLuint fs = compile_shader(GL_FRAGMENT_SHADER, fragment_shader);

  // Test emscripten_webgl_get_shader_parameter_d() API
  assert(emscripten_webgl_get_shader_parameter_d(fs, GL_DELETE_STATUS) == 0);
  assert(emscripten_webgl_get_shader_parameter_d(fs, GL_COMPILE_STATUS) == 1);
  assert(emscripten_webgl_get_shader_parameter_d(fs, GL_SHADER_TYPE) == GL_FRAGMENT_SHADER);

  // Test emscripten_webgl_get_shader_source_utf8() API
  src = emscripten_webgl_get_shader_source_utf8(fs);
  assert(src);
  assert(strstr(src, "varying") != 0);
  free(src);

  GLuint program = create_program(vs, fs);

  // Test emscripten_webgl_get_program_parameter_d() API
  assert(emscripten_webgl_get_program_parameter_d(program, GL_ACTIVE_ATTRIBUTES) == 2);

  // Test emscripten_webgl_get_program_info_log_utf8() API
  GLuint program2 = create_program(vs2, fs);
  char *programLog = emscripten_webgl_get_program_info_log_utf8(program2);
  assert(programLog);
  // Firefox says "Must have a compiled vertex shader attached."
  // Chrome says "invalid shaders"
  // -> test that word "shader" is present
  assert(strstr(programLog, "shader") != 0);
  free(programLog);

  glUseProgram(program);
  glUniform1f(glGetUniformLocation(program, "vsUniformFloat"), 2.f);
  glUniform4f(glGetUniformLocation(program, "psUniformVec"), 1.f, 2.f, 3.f, 4.f);

  // Test emscripten_webgl_get_uniform_d() API
  double vsUniformFloat = emscripten_webgl_get_uniform_d(program, glGetUniformLocation(program, "vsUniformFloat"));
  assert(vsUniformFloat == 2.f);

  float psUniformVec[4] = { 0, 0, 0, 0 };

  // Test emscripten_webgl_get_uniform_v() API truncated read
  numElements = emscripten_webgl_get_uniform_v(program, glGetUniformLocation(program, "psUniformVec"), psUniformVec, 3, EMSCRIPTEN_WEBGL_PARAM_TYPE_FLOAT);
  assert(numElements == 4);
  assert(psUniformVec[0] == 1.0f);
  assert(psUniformVec[1] == 2.0f);
  assert(psUniformVec[2] == 3.0f);
  assert(psUniformVec[3] == 0.0f);

  // Test emscripten_webgl_get_uniform_v() API full read
  numElements = emscripten_webgl_get_uniform_v(program, glGetUniformLocation(program, "psUniformVec"), psUniformVec, 4, EMSCRIPTEN_WEBGL_PARAM_TYPE_FLOAT);
  assert(numElements == 4);
  assert(psUniformVec[0] == 1.0f);
  assert(psUniformVec[1] == 2.0f);
  assert(psUniformVec[2] == 3.0f);
  assert(psUniformVec[3] == 4.0f);

  return 0;
}
