///|
priv struct Renderer {
  canvas : JsValue
  gl : WebGL
  program : WebGLProgram
  loc_pos : Int
  loc_color : Int
  loc_uv : Int
  loc_vp : WebGLUniformLocation
  loc_tex : WebGLUniformLocation
  loc_tint : WebGLUniformLocation
  loc_tint_strength : WebGLUniformLocation
  texture : WebGLTexture
  particle_buffer_pos : WebGLBuffer
  particle_buffer_color : WebGLBuffer
  particle_buffer_uv : WebGLBuffer
  mut particle_vertex_count : Int
  mut chunks : Array[RenderChunk]
  mut width : Int
  mut height : Int
}

///|
priv struct RenderChunk {
  buffer_pos : WebGLBuffer
  buffer_color : WebGLBuffer
  buffer_uv : WebGLBuffer
  vertex_count : Int
  buffer_pos_cutout : WebGLBuffer
  buffer_color_cutout : WebGLBuffer
  buffer_uv_cutout : WebGLBuffer
  vertex_count_cutout : Int
  buffer_pos_trans : WebGLBuffer
  buffer_color_trans : WebGLBuffer
  buffer_uv_trans : WebGLBuffer
  vertex_count_trans : Int
}

///|
fn renderer_init(canvas : JsValue) -> Renderer {
  let gl = gl_create_context(canvas)
  let vs = compile_shader(gl, gl_vertex_shader, vertex_shader_source())
  let fs = compile_shader(gl, gl_fragment_shader, fragment_shader_source())
  let program = link_program(gl, vs, fs)
  let loc_pos = gl_get_attrib_location(gl, program, "a_pos")
  let loc_color = gl_get_attrib_location(gl, program, "a_color")
  let loc_uv = gl_get_attrib_location(gl, program, "a_uv")
  let loc_vp = gl_get_uniform_location(gl, program, "u_vp")
  let loc_tex = gl_get_uniform_location(gl, program, "u_tex")
  let loc_tint = gl_get_uniform_location(gl, program, "u_tint")
  let loc_tint_strength = gl_get_uniform_location(
    gl, program, "u_tint_strength",
  )
  let texture = gl_create_texture(gl)
  let atlas = dom_create_atlas()
  gl_active_texture(gl, gl_texture0)
  gl_bind_texture(gl, gl_texture_2d, texture)
  gl_pixel_storei(gl, gl_unpack_flip_y_webgl, 1)
  let ok = gl_tex_image_2d_canvas(
    gl, gl_texture_2d, 0, gl_rgba, gl_rgba, gl_unsigned_byte, atlas,
  )
  if !ok {
    js_error("Failed to upload texture atlas")
  }
  gl_tex_parameteri(gl, gl_texture_2d, gl_texture_min_filter, gl_nearest)
  gl_tex_parameteri(gl, gl_texture_2d, gl_texture_mag_filter, gl_nearest)
  gl_tex_parameteri(gl, gl_texture_2d, gl_texture_wrap_s, gl_clamp_to_edge)
  gl_tex_parameteri(gl, gl_texture_2d, gl_texture_wrap_t, gl_clamp_to_edge)
  gl_enable(gl, gl_depth_test)
  gl_enable(gl, gl_blend)
  gl_blend_func(gl, gl_src_alpha, gl_one_minus_src_alpha)
  gl_clear_color(gl, 0.52, 0.75, 0.95, 1.0)
  gl_use_program(gl, program)
  gl_uniform1i(gl, loc_tex, 0)
  gl_uniform3f(gl, loc_tint, 0.0, 0.0, 0.0)
  gl_uniform1f(gl, loc_tint_strength, 0.0)
  let particle_buffer_pos = gl_create_buffer(gl)
  let particle_buffer_color = gl_create_buffer(gl)
  let particle_buffer_uv = gl_create_buffer(gl)
  {
    canvas,
    gl,
    program,
    loc_pos,
    loc_color,
    loc_uv,
    loc_vp,
    loc_tex,
    loc_tint,
    loc_tint_strength,
    texture,
    particle_buffer_pos,
    particle_buffer_color,
    particle_buffer_uv,
    particle_vertex_count: 0,
    chunks: [],
    width: 0,
    height: 0,
  }
}

///|
fn Renderer::resize(self : Renderer, width : Int, height : Int) -> Unit {
  if width != self.width || height != self.height {
    self.width = width
    self.height = height
    gl_viewport(self.gl, 0, 0, width, height)
  }
}

///|
fn Renderer::upload_chunk_meshes(
  self : Renderer,
  meshes : Array[ChunkMesh],
) -> Unit {
  self.chunks = []
  for i = 0; i < meshes.length(); i = i + 1 {
    let mesh = meshes[i]
    let solid = mesh.solid
    let cutout = mesh.cutout
    let trans = mesh.trans
    let buffer_pos = gl_create_buffer(self.gl)
    let buffer_color = gl_create_buffer(self.gl)
    let buffer_uv = gl_create_buffer(self.gl)
    gl_bind_buffer(self.gl, gl_array_buffer, buffer_pos)
    gl_buffer_data_f32(
      self.gl,
      gl_array_buffer,
      solid.positions,
      gl_static_draw,
    )
    gl_bind_buffer(self.gl, gl_array_buffer, buffer_color)
    gl_buffer_data_f32(self.gl, gl_array_buffer, solid.colors, gl_static_draw)
    gl_bind_buffer(self.gl, gl_array_buffer, buffer_uv)
    gl_buffer_data_f32(self.gl, gl_array_buffer, solid.uvs, gl_static_draw)
    let buffer_pos_cutout = gl_create_buffer(self.gl)
    let buffer_color_cutout = gl_create_buffer(self.gl)
    let buffer_uv_cutout = gl_create_buffer(self.gl)
    gl_bind_buffer(self.gl, gl_array_buffer, buffer_pos_cutout)
    gl_buffer_data_f32(
      self.gl,
      gl_array_buffer,
      cutout.positions,
      gl_static_draw,
    )
    gl_bind_buffer(self.gl, gl_array_buffer, buffer_color_cutout)
    gl_buffer_data_f32(self.gl, gl_array_buffer, cutout.colors, gl_static_draw)
    gl_bind_buffer(self.gl, gl_array_buffer, buffer_uv_cutout)
    gl_buffer_data_f32(self.gl, gl_array_buffer, cutout.uvs, gl_static_draw)
    let buffer_pos_trans = gl_create_buffer(self.gl)
    let buffer_color_trans = gl_create_buffer(self.gl)
    let buffer_uv_trans = gl_create_buffer(self.gl)
    gl_bind_buffer(self.gl, gl_array_buffer, buffer_pos_trans)
    gl_buffer_data_f32(
      self.gl,
      gl_array_buffer,
      trans.positions,
      gl_static_draw,
    )
    gl_bind_buffer(self.gl, gl_array_buffer, buffer_color_trans)
    gl_buffer_data_f32(self.gl, gl_array_buffer, trans.colors, gl_static_draw)
    gl_bind_buffer(self.gl, gl_array_buffer, buffer_uv_trans)
    gl_buffer_data_f32(self.gl, gl_array_buffer, trans.uvs, gl_static_draw)
    self.chunks.push({
      buffer_pos,
      buffer_color,
      buffer_uv,
      vertex_count: solid.vertex_count,
      buffer_pos_cutout,
      buffer_color_cutout,
      buffer_uv_cutout,
      vertex_count_cutout: cutout.vertex_count,
      buffer_pos_trans,
      buffer_color_trans,
      buffer_uv_trans,
      vertex_count_trans: trans.vertex_count,
    })
  }
}

///|
fn Renderer::update_chunk(
  self : Renderer,
  index : Int,
  mesh : ChunkMesh,
) -> Unit {
  if index < 0 || index >= self.chunks.length() {
    return
  }
  let chunk = self.chunks[index]
  let solid = mesh.solid
  let cutout = mesh.cutout
  let trans = mesh.trans
  gl_bind_buffer(self.gl, gl_array_buffer, chunk.buffer_pos)
  gl_buffer_data_f32(self.gl, gl_array_buffer, solid.positions, gl_static_draw)
  gl_bind_buffer(self.gl, gl_array_buffer, chunk.buffer_color)
  gl_buffer_data_f32(self.gl, gl_array_buffer, solid.colors, gl_static_draw)
  gl_bind_buffer(self.gl, gl_array_buffer, chunk.buffer_uv)
  gl_buffer_data_f32(self.gl, gl_array_buffer, solid.uvs, gl_static_draw)
  gl_bind_buffer(self.gl, gl_array_buffer, chunk.buffer_pos_cutout)
  gl_buffer_data_f32(self.gl, gl_array_buffer, cutout.positions, gl_static_draw)
  gl_bind_buffer(self.gl, gl_array_buffer, chunk.buffer_color_cutout)
  gl_buffer_data_f32(self.gl, gl_array_buffer, cutout.colors, gl_static_draw)
  gl_bind_buffer(self.gl, gl_array_buffer, chunk.buffer_uv_cutout)
  gl_buffer_data_f32(self.gl, gl_array_buffer, cutout.uvs, gl_static_draw)
  gl_bind_buffer(self.gl, gl_array_buffer, chunk.buffer_pos_trans)
  gl_buffer_data_f32(self.gl, gl_array_buffer, trans.positions, gl_static_draw)
  gl_bind_buffer(self.gl, gl_array_buffer, chunk.buffer_color_trans)
  gl_buffer_data_f32(self.gl, gl_array_buffer, trans.colors, gl_static_draw)
  gl_bind_buffer(self.gl, gl_array_buffer, chunk.buffer_uv_trans)
  gl_buffer_data_f32(self.gl, gl_array_buffer, trans.uvs, gl_static_draw)
  self.chunks[index] = {
    buffer_pos: chunk.buffer_pos,
    buffer_color: chunk.buffer_color,
    buffer_uv: chunk.buffer_uv,
    vertex_count: solid.vertex_count,
    buffer_pos_cutout: chunk.buffer_pos_cutout,
    buffer_color_cutout: chunk.buffer_color_cutout,
    buffer_uv_cutout: chunk.buffer_uv_cutout,
    vertex_count_cutout: cutout.vertex_count,
    buffer_pos_trans: chunk.buffer_pos_trans,
    buffer_color_trans: chunk.buffer_color_trans,
    buffer_uv_trans: chunk.buffer_uv_trans,
    vertex_count_trans: trans.vertex_count,
  }
}

///|
fn Renderer::upload_particles(self : Renderer, mesh : Mesh) -> Unit {
  gl_bind_buffer(self.gl, gl_array_buffer, self.particle_buffer_pos)
  gl_buffer_data_f32(self.gl, gl_array_buffer, mesh.positions, gl_static_draw)
  gl_bind_buffer(self.gl, gl_array_buffer, self.particle_buffer_color)
  gl_buffer_data_f32(self.gl, gl_array_buffer, mesh.colors, gl_static_draw)
  gl_bind_buffer(self.gl, gl_array_buffer, self.particle_buffer_uv)
  gl_buffer_data_f32(self.gl, gl_array_buffer, mesh.uvs, gl_static_draw)
  self.particle_vertex_count = mesh.vertex_count
}

///|
fn Renderer::clear_particles(self : Renderer) -> Unit {
  self.particle_vertex_count = 0
}

///|
fn Renderer::draw(
  self : Renderer,
  vp : Array[Double],
  tint : Vec3,
  tint_strength : Double,
) -> Unit {
  if tint_strength > 0.0 {
    gl_clear_color(self.gl, 0.05, 0.12, 0.22, 1.0)
  } else {
    gl_clear_color(self.gl, 0.52, 0.75, 0.95, 1.0)
  }
  gl_clear(self.gl, gl_color_buffer_bit | gl_depth_buffer_bit)
  gl_use_program(self.gl, self.program)
  gl_uniform_matrix4fv(self.gl, self.loc_vp, false, vp)
  gl_active_texture(self.gl, gl_texture0)
  gl_bind_texture(self.gl, gl_texture_2d, self.texture)
  gl_uniform1i(self.gl, self.loc_tex, 0)
  gl_uniform3f(self.gl, self.loc_tint, tint.x, tint.y, tint.z)
  gl_uniform1f(self.gl, self.loc_tint_strength, tint_strength)
  gl_enable_vertex_attrib_array(self.gl, self.loc_pos)
  gl_enable_vertex_attrib_array(self.gl, self.loc_color)
  gl_enable_vertex_attrib_array(self.gl, self.loc_uv)
  gl_depth_mask(self.gl, true)
  gl_depth_func(self.gl, gl_less)
  for i = 0; i < self.chunks.length(); i = i + 1 {
    let chunk = self.chunks[i]
    if chunk.vertex_count > 0 {
      gl_bind_buffer(self.gl, gl_array_buffer, chunk.buffer_pos)
      gl_vertex_attrib_pointer(self.gl, self.loc_pos, 3, gl_float, false, 0, 0)
      gl_bind_buffer(self.gl, gl_array_buffer, chunk.buffer_color)
      gl_vertex_attrib_pointer(
        self.gl,
        self.loc_color,
        3,
        gl_float,
        false,
        0,
        0,
      )
      gl_bind_buffer(self.gl, gl_array_buffer, chunk.buffer_uv)
      gl_vertex_attrib_pointer(self.gl, self.loc_uv, 2, gl_float, false, 0, 0)
      gl_draw_arrays(self.gl, gl_triangles, 0, chunk.vertex_count)
    }
  }
  for i = 0; i < self.chunks.length(); i = i + 1 {
    let chunk = self.chunks[i]
    if chunk.vertex_count_cutout > 0 {
      gl_bind_buffer(self.gl, gl_array_buffer, chunk.buffer_pos_cutout)
      gl_vertex_attrib_pointer(self.gl, self.loc_pos, 3, gl_float, false, 0, 0)
      gl_bind_buffer(self.gl, gl_array_buffer, chunk.buffer_color_cutout)
      gl_vertex_attrib_pointer(
        self.gl,
        self.loc_color,
        3,
        gl_float,
        false,
        0,
        0,
      )
      gl_bind_buffer(self.gl, gl_array_buffer, chunk.buffer_uv_cutout)
      gl_vertex_attrib_pointer(self.gl, self.loc_uv, 2, gl_float, false, 0, 0)
      gl_draw_arrays(self.gl, gl_triangles, 0, chunk.vertex_count_cutout)
    }
  }
  gl_depth_mask(self.gl, false)
  gl_depth_func(self.gl, gl_lequal)
  for i = 0; i < self.chunks.length(); i = i + 1 {
    let chunk = self.chunks[i]
    if chunk.vertex_count_trans > 0 {
      gl_bind_buffer(self.gl, gl_array_buffer, chunk.buffer_pos_trans)
      gl_vertex_attrib_pointer(self.gl, self.loc_pos, 3, gl_float, false, 0, 0)
      gl_bind_buffer(self.gl, gl_array_buffer, chunk.buffer_color_trans)
      gl_vertex_attrib_pointer(
        self.gl,
        self.loc_color,
        3,
        gl_float,
        false,
        0,
        0,
      )
      gl_bind_buffer(self.gl, gl_array_buffer, chunk.buffer_uv_trans)
      gl_vertex_attrib_pointer(self.gl, self.loc_uv, 2, gl_float, false, 0, 0)
      gl_draw_arrays(self.gl, gl_triangles, 0, chunk.vertex_count_trans)
    }
  }
  if self.particle_vertex_count > 0 {
    gl_bind_buffer(self.gl, gl_array_buffer, self.particle_buffer_pos)
    gl_vertex_attrib_pointer(self.gl, self.loc_pos, 3, gl_float, false, 0, 0)
    gl_bind_buffer(self.gl, gl_array_buffer, self.particle_buffer_color)
    gl_vertex_attrib_pointer(self.gl, self.loc_color, 3, gl_float, false, 0, 0)
    gl_bind_buffer(self.gl, gl_array_buffer, self.particle_buffer_uv)
    gl_vertex_attrib_pointer(self.gl, self.loc_uv, 2, gl_float, false, 0, 0)
    gl_draw_arrays(self.gl, gl_triangles, 0, self.particle_vertex_count)
  }
  gl_depth_mask(self.gl, true)
  gl_depth_func(self.gl, gl_less)
}

///|
fn compile_shader(
  gl : WebGL,
  shader_type : Int,
  source : String,
) -> WebGLShader {
  let shader = gl_create_shader(gl, shader_type)
  gl_shader_source(gl, shader, source)
  gl_compile_shader(gl, shader)
  if !gl_get_shader_parameter(gl, shader, gl_compile_status) {
    let log = gl_get_shader_info_log(gl, shader)
    js_error("Shader compile error:\\n\{log}")
  }
  shader
}

///|
fn link_program(gl : WebGL, vs : WebGLShader, fs : WebGLShader) -> WebGLProgram {
  let program = gl_create_program(gl)
  gl_attach_shader(gl, program, vs)
  gl_attach_shader(gl, program, fs)
  gl_link_program(gl, program)
  if !gl_get_program_parameter(gl, program, gl_link_status) {
    let log = gl_get_program_info_log(gl, program)
    js_error("Program link error:\\n\{log}")
  }
  program
}

///|
fn vertex_shader_source() -> String {
  let src : String =
    #|attribute vec3 a_pos;
    #|attribute vec3 a_color;
    #|attribute vec2 a_uv;
    #|uniform mat4 u_vp;
    #|varying vec3 v_color;
    #|varying vec2 v_uv;
    #|void main() {
    #|  gl_Position = u_vp * vec4(a_pos, 1.0);
    #|  v_color = a_color;
    #|  v_uv = a_uv;
    #|}
  src
}

///|
fn fragment_shader_source() -> String {
  let src : String =
    #|precision mediump float;
    #|varying vec3 v_color;
    #|varying vec2 v_uv;
    #|uniform sampler2D u_tex;
    #|uniform vec3 u_tint;
    #|uniform float u_tint_strength;
    #|void main() {
    #|  vec4 tex = texture2D(u_tex, v_uv);
    #|  if (tex.a < 0.1) discard;
    #|  vec3 base = tex.rgb * v_color;
    #|  vec3 mixed = mix(base, u_tint, u_tint_strength);
    #|  gl_FragColor = vec4(mixed, tex.a);
    #|}
  src
}