///|
priv struct Mesh {
  positions : Array[Double]
  colors : Array[Double]
  uvs : Array[Double]
  vertex_count : Int
}

///|
priv struct ChunkMesh {
  solid : Mesh
  cutout : Mesh
  trans : Mesh
}

///|
fn build_mesh_range(
  world : World,
  x_start : Int,
  x_end : Int,
  z_start : Int,
  z_end : Int,
  y_start : Int,
  y_end : Int,
) -> ChunkMesh {
  let solid_positions : Array[Double] = []
  let solid_colors : Array[Double] = []
  let solid_uvs : Array[Double] = []
  let cutout_positions : Array[Double] = []
  let cutout_colors : Array[Double] = []
  let cutout_uvs : Array[Double] = []
  let trans_positions : Array[Double] = []
  let trans_colors : Array[Double] = []
  let trans_uvs : Array[Double] = []
  for z = z_start; z < z_end; z = z + 1 {
    for y = y_start; y < y_end; y = y + 1 {
      for x = x_start; x < x_end; x = x + 1 {
        let block = world.get_range(x, y, z, y_start, y_end)
        if !block_is_air(block) {
          let is_trans = block_is_transparent(block)
          let is_cutout = block_is_cutout(block)
          let positions = if is_trans {
            trans_positions
          } else if is_cutout {
            cutout_positions
          } else {
            solid_positions
          }
          let colors = if is_trans {
            trans_colors
          } else if is_cutout {
            cutout_colors
          } else {
            solid_colors
          }
          let uvs = if is_trans {
            trans_uvs
          } else if is_cutout {
            cutout_uvs
          } else {
            solid_uvs
          }
          let east = world.get_range(x + 1, y, z, y_start, y_end)
          if face_visible(block, east) {
            append_face_east(positions, colors, uvs, x, y, z, block)
          }
          let west = world.get_range(x - 1, y, z, y_start, y_end)
          if face_visible(block, west) {
            append_face_west(positions, colors, uvs, x, y, z, block)
          }
          let up = world.get_range(x, y + 1, z, y_start, y_end)
          if face_visible(block, up) {
            append_face_up(positions, colors, uvs, x, y, z, block)
          }
          let down = world.get_range(x, y - 1, z, y_start, y_end)
          if face_visible(block, down) {
            append_face_down(positions, colors, uvs, x, y, z, block)
          }
          let south = world.get_range(x, y, z + 1, y_start, y_end)
          if face_visible(block, south) {
            append_face_south(positions, colors, uvs, x, y, z, block)
          }
          let north = world.get_range(x, y, z - 1, y_start, y_end)
          if face_visible(block, north) {
            append_face_north(positions, colors, uvs, x, y, z, block)
          }
        }
      }
    }
  }
  let solid = {
    positions: solid_positions,
    colors: solid_colors,
    uvs: solid_uvs,
    vertex_count: solid_positions.length() / 3,
  }
  let cutout = {
    positions: cutout_positions,
    colors: cutout_colors,
    uvs: cutout_uvs,
    vertex_count: cutout_positions.length() / 3,
  }
  let trans = {
    positions: trans_positions,
    colors: trans_colors,
    uvs: trans_uvs,
    vertex_count: trans_positions.length() / 3,
  }
  { solid, cutout, trans }
}

///|
fn build_chunk_mesh(
  world : World,
  cx : Int,
  cz : Int,
  chunk_size : Int,
  y_start : Int,
  y_end : Int,
) -> ChunkMesh {
  let x_start = cx * chunk_size
  let z_start = cz * chunk_size
  build_mesh_range(
    world,
    x_start,
    x_start + chunk_size,
    z_start,
    z_start + chunk_size,
    y_start,
    y_end,
  )
}

///|
fn build_chunk_surface_mesh(
  world : World,
  cx : Int,
  cz : Int,
  chunk_size : Int,
) -> ChunkMesh {
  let solid_positions : Array[Double] = []
  let solid_colors : Array[Double] = []
  let solid_uvs : Array[Double] = []
  let trans_positions : Array[Double] = []
  let trans_colors : Array[Double] = []
  let trans_uvs : Array[Double] = []
  let total = chunk_size * chunk_size
  let heights = Array::make(total, 0)
  let blocks = Array::make(total, Block::Air)
  let side_blocks = Array::make(total, Block::Air)
  fn surface_height(world : World, gx : Int, gz : Int) -> Int {
    let info = world.column_info(gx, gz)
    let h = info.height
    if h <= world.sea_level {
      world.sea_level
    } else {
      h
    }
  }

  fn emit_east(
    block : Block,
    x : Double,
    z0 : Double,
    z1 : Double,
    y0 : Double,
    y1 : Double,
  ) -> Unit {
    let use_trans = block_is_transparent(block)
    let positions = if use_trans { trans_positions } else { solid_positions }
    let colors = if use_trans { trans_colors } else { solid_colors }
    let uvs = if use_trans { trans_uvs } else { solid_uvs }
    append_face_east_rect(positions, colors, uvs, x, z0, z1, y0, y1, block)
  }

  fn emit_west(
    block : Block,
    x : Double,
    z0 : Double,
    z1 : Double,
    y0 : Double,
    y1 : Double,
  ) -> Unit {
    let use_trans = block_is_transparent(block)
    let positions = if use_trans { trans_positions } else { solid_positions }
    let colors = if use_trans { trans_colors } else { solid_colors }
    let uvs = if use_trans { trans_uvs } else { solid_uvs }
    append_face_west_rect(positions, colors, uvs, x, z0, z1, y0, y1, block)
  }

  fn emit_south(
    block : Block,
    x0 : Double,
    x1 : Double,
    z : Double,
    y0 : Double,
    y1 : Double,
  ) -> Unit {
    let use_trans = block_is_transparent(block)
    let positions = if use_trans { trans_positions } else { solid_positions }
    let colors = if use_trans { trans_colors } else { solid_colors }
    let uvs = if use_trans { trans_uvs } else { solid_uvs }
    append_face_south_rect(positions, colors, uvs, x0, x1, z, y0, y1, block)
  }

  fn emit_north(
    block : Block,
    x0 : Double,
    x1 : Double,
    z : Double,
    y0 : Double,
    y1 : Double,
  ) -> Unit {
    let use_trans = block_is_transparent(block)
    let positions = if use_trans { trans_positions } else { solid_positions }
    let colors = if use_trans { trans_colors } else { solid_colors }
    let uvs = if use_trans { trans_uvs } else { solid_uvs }
    append_face_north_rect(positions, colors, uvs, x0, x1, z, y0, y1, block)
  }

  for z = 0; z < chunk_size; z = z + 1 {
    for x = 0; x < chunk_size; x = x + 1 {
      let gx = cx * chunk_size + x
      let gz = cz * chunk_size + z
      let info = world.column_info(gx, gz)
      let h = info.height
      let (top_block, mid_block) = surface_blocks(world, gx, gz, info)
      let idx = x + z * chunk_size
      if h <= world.sea_level {
        heights[idx] = world.sea_level
        blocks[idx] = Block::Water
        side_blocks[idx] = Block::Water
      } else {
        heights[idx] = h
        blocks[idx] = top_block
        side_blocks[idx] = mid_block
      }
    }
  }
  let used = Array::make(total, false)
  for z = 0; z < chunk_size; z = z + 1 {
    for x = 0; x < chunk_size; x = x + 1 {
      let idx = x + z * chunk_size
      if used[idx] {
        continue
      }
      let block = blocks[idx]
      if block_is_air(block) {
        continue
      }
      let height = heights[idx]
      let mut w = 1
      while x + w < chunk_size {
        let idx2 = x + w + z * chunk_size
        if used[idx2] || blocks[idx2] != block || heights[idx2] != height {
          break
        }
        w = w + 1
      }
      let mut d = 1
      while z + d < chunk_size {
        let mut ok = true
        for dx = 0; dx < w; dx = dx + 1 {
          let idx2 = x + dx + (z + d) * chunk_size
          if used[idx2] || blocks[idx2] != block || heights[idx2] != height {
            ok = false
            break
          }
        }
        if !ok {
          break
        }
        d = d + 1
      }
      for dz = 0; dz < d; dz = dz + 1 {
        for dx = 0; dx < w; dx = dx + 1 {
          used[x + dx + (z + dz) * chunk_size] = true
        }
      }
      let x0 = (cx * chunk_size + x).to_double()
      let x1 = (cx * chunk_size + x + w).to_double()
      let z0 = (cz * chunk_size + z).to_double()
      let z1 = (cz * chunk_size + z + d).to_double()
      let y = height.to_double()
      if block_is_transparent(block) {
        append_face_up_rect(
          trans_positions, trans_colors, trans_uvs, x0, x1, z0, z1, y, block,
        )
      } else {
        append_face_up_rect(
          solid_positions, solid_colors, solid_uvs, x0, x1, z0, z1, y, block,
        )
      }
    }
  }
  for z = 0; z < chunk_size; z = z + 1 {
    for x = 0; x < chunk_size; x = x + 1 {
      let idx = x + z * chunk_size
      let block = blocks[idx]
      if block_is_air(block) {
        continue
      }
      let height = heights[idx]
      let side_block = side_blocks[idx]
      let band = if block == Block::Grass {
        1
      } else if block == Block::Snow {
        2
      } else {
        0
      }
      let gx = cx * chunk_size + x
      let gz = cz * chunk_size + z
      let h_east = if x + 1 < chunk_size {
        let idx2 = x + 1 + z * chunk_size
        heights[idx2]
      } else {
        surface_height(world, gx + 1, gz)
      }
      if height > h_east {
        let x0 = (gx + 1).to_double()
        let z0 = gz.to_double()
        let z1 = (gz + 1).to_double()
        let diff = height - h_east
        if band > 0 && diff > band {
          let y_split = (height - band).to_double()
          emit_east(side_block, x0, z0, z1, h_east.to_double(), y_split)
          emit_east(block, x0, z0, z1, y_split, height.to_double())
        } else {
          let face_block = if band > 0 { block } else { side_block }
          emit_east(
            face_block,
            x0,
            z0,
            z1,
            h_east.to_double(),
            height.to_double(),
          )
        }
      }
      let h_west = if x > 0 {
        let idx2 = x - 1 + z * chunk_size
        heights[idx2]
      } else {
        surface_height(world, gx - 1, gz)
      }
      if height > h_west {
        let x0 = gx.to_double()
        let z0 = gz.to_double()
        let z1 = (gz + 1).to_double()
        let diff = height - h_west
        if band > 0 && diff > band {
          let y_split = (height - band).to_double()
          emit_west(side_block, x0, z0, z1, h_west.to_double(), y_split)
          emit_west(block, x0, z0, z1, y_split, height.to_double())
        } else {
          let face_block = if band > 0 { block } else { side_block }
          emit_west(
            face_block,
            x0,
            z0,
            z1,
            h_west.to_double(),
            height.to_double(),
          )
        }
      }
      let h_south = if z + 1 < chunk_size {
        let idx2 = x + (z + 1) * chunk_size
        heights[idx2]
      } else {
        surface_height(world, gx, gz + 1)
      }
      if height > h_south {
        let x0 = gx.to_double()
        let x1 = (gx + 1).to_double()
        let z0 = (gz + 1).to_double()
        let diff = height - h_south
        if band > 0 && diff > band {
          let y_split = (height - band).to_double()
          emit_south(side_block, x0, x1, z0, h_south.to_double(), y_split)
          emit_south(block, x0, x1, z0, y_split, height.to_double())
        } else {
          let face_block = if band > 0 { block } else { side_block }
          emit_south(
            face_block,
            x0,
            x1,
            z0,
            h_south.to_double(),
            height.to_double(),
          )
        }
      }
      let h_north = if z > 0 {
        let idx2 = x + (z - 1) * chunk_size
        heights[idx2]
      } else {
        surface_height(world, gx, gz - 1)
      }
      if height > h_north {
        let x0 = gx.to_double()
        let x1 = (gx + 1).to_double()
        let z0 = gz.to_double()
        let diff = height - h_north
        if band > 0 && diff > band {
          let y_split = (height - band).to_double()
          emit_north(side_block, x0, x1, z0, h_north.to_double(), y_split)
          emit_north(block, x0, x1, z0, y_split, height.to_double())
        } else {
          let face_block = if band > 0 { block } else { side_block }
          emit_north(
            face_block,
            x0,
            x1,
            z0,
            h_north.to_double(),
            height.to_double(),
          )
        }
      }
    }
  }
  let solid = {
    positions: solid_positions,
    colors: solid_colors,
    uvs: solid_uvs,
    vertex_count: solid_positions.length() / 3,
  }
  let cutout = { positions: [], colors: [], uvs: [], vertex_count: 0 }
  let trans = {
    positions: trans_positions,
    colors: trans_colors,
    uvs: trans_uvs,
    vertex_count: trans_positions.length() / 3,
  }
  { solid, cutout, trans }
}

///|
fn face_visible(block : Block, neighbor : Block) -> Bool {
  if block_is_air(block) {
    false
  } else if block_is_transparent(block) {
    block_is_air(neighbor)
  } else if block_is_cutout(block) {
    !block_is_cutout(neighbor)
  } else {
    block_is_air(neighbor) || block_is_see_through(neighbor)
  }
}

///|
fn face_shade(face : Int) -> Double {
  match face {
    0 => 0.85
    1 => 0.85
    2 => 1.0
    3 => 0.5
    4 => 0.8
    5 => 0.8
    _ => 1.0
  }
}

///|
fn block_tint(block : Block, face : Int) -> Vec3 {
  let shade = face_shade(face)
  match block {
    Block::Air => vec3(0.0, 0.0, 0.0)
    _ => vec3(shade, shade, shade)
  }
}

///|
fn tile_for_face(block : Block, face : Int) -> (Int, Int) {
  match block {
    Block::Grass =>
      if face == 2 {
        (0, 0)
      } else if face == 3 {
        (2, 0)
      } else {
        (1, 0)
      }
    Block::Dirt => (2, 0)
    Block::Stone => (3, 0)
    Block::Sand => (0, 1)
    Block::Water => (1, 1)
    Block::Wood => if face == 2 || face == 3 { (2, 1) } else { (3, 1) }
    Block::Leaves => (0, 2)
    Block::Bedrock => (1, 2)
    Block::Planks => (2, 2)
    Block::Brick => (3, 2)
    Block::Cobblestone => (0, 3)
    Block::Gravel => (1, 3)
    Block::Clay => (2, 3)
    Block::Snow => (3, 3)
    Block::Air => (0, 0)
  }
}

///|
fn push_vertex_uv(
  positions : Array[Double],
  colors : Array[Double],
  uvs : Array[Double],
  x : Double,
  y : Double,
  z : Double,
  color : Vec3,
  u : Double,
  v : Double,
) -> Unit {
  positions.push(x)
  positions.push(y)
  positions.push(z)
  colors.push(color.x)
  colors.push(color.y)
  colors.push(color.z)
  uvs.push(u)
  uvs.push(v)
}

///|
fn push_quad(
  positions : Array[Double],
  colors : Array[Double],
  uvs : Array[Double],
  x0 : Double,
  y0 : Double,
  z0 : Double,
  x1 : Double,
  y1 : Double,
  z1 : Double,
  x2 : Double,
  y2 : Double,
  z2 : Double,
  x3 : Double,
  y3 : Double,
  z3 : Double,
  color : Vec3,
  u0 : Double,
  v0 : Double,
  u1 : Double,
  v1 : Double,
) -> Unit {
  push_vertex_uv(positions, colors, uvs, x0, y0, z0, color, u0, v0)
  push_vertex_uv(positions, colors, uvs, x1, y1, z1, color, u1, v0)
  push_vertex_uv(positions, colors, uvs, x2, y2, z2, color, u1, v1)
  push_vertex_uv(positions, colors, uvs, x0, y0, z0, color, u0, v0)
  push_vertex_uv(positions, colors, uvs, x2, y2, z2, color, u1, v1)
  push_vertex_uv(positions, colors, uvs, x3, y3, z3, color, u0, v1)
}

///|
fn tile_uv(tile_x : Int, tile_y : Int) -> (Double, Double, Double, Double) {
  let cols = 4.0
  let rows = 4.0
  let du = 1.0 / cols
  let dv = 1.0 / rows
  let u0 = tile_x.to_double() * du
  let v1 = 1.0 - tile_y.to_double() * dv
  let v0 = v1 - dv
  (u0, v0, u0 + du, v1)
}

///|
fn append_face_east(
  positions : Array[Double],
  colors : Array[Double],
  uvs : Array[Double],
  x : Int,
  y : Int,
  z : Int,
  block : Block,
) -> Unit {
  let c = block_tint(block, 0)
  let (tx, ty) = tile_for_face(block, 0)
  let (u0, v0, u1, v1) = tile_uv(tx, ty)
  let x0 = x.to_double() + 1.0
  let y0 = y.to_double()
  let y1 = y.to_double() + 1.0
  let z0 = z.to_double()
  let z1 = z.to_double() + 1.0
  push_quad(
    positions, colors, uvs, x0, y0, z0, x0, y0, z1, x0, y1, z1, x0, y1, z0, c, u0,
    v0, u1, v1,
  )
}

///|
fn append_face_west(
  positions : Array[Double],
  colors : Array[Double],
  uvs : Array[Double],
  x : Int,
  y : Int,
  z : Int,
  block : Block,
) -> Unit {
  let c = block_tint(block, 1)
  let (tx, ty) = tile_for_face(block, 1)
  let (u0, v0, u1, v1) = tile_uv(tx, ty)
  let x0 = x.to_double()
  let y0 = y.to_double()
  let y1 = y.to_double() + 1.0
  let z0 = z.to_double()
  let z1 = z.to_double() + 1.0
  push_quad(
    positions, colors, uvs, x0, y0, z1, x0, y0, z0, x0, y1, z0, x0, y1, z1, c, u0,
    v0, u1, v1,
  )
}

///|
fn append_face_up(
  positions : Array[Double],
  colors : Array[Double],
  uvs : Array[Double],
  x : Int,
  y : Int,
  z : Int,
  block : Block,
) -> Unit {
  let c = block_tint(block, 2)
  let (tx, ty) = tile_for_face(block, 2)
  let (u0, v0, u1, v1) = tile_uv(tx, ty)
  let x0 = x.to_double()
  let x1 = x.to_double() + 1.0
  let y1 = y.to_double() + 1.0
  let z0 = z.to_double()
  let z1 = z.to_double() + 1.0
  push_quad(
    positions, colors, uvs, x0, y1, z0, x0, y1, z1, x1, y1, z1, x1, y1, z0, c, u0,
    v0, u1, v1,
  )
}

///|
fn append_face_up_rect(
  positions : Array[Double],
  colors : Array[Double],
  uvs : Array[Double],
  x0 : Double,
  x1 : Double,
  z0 : Double,
  z1 : Double,
  y : Double,
  block : Block,
) -> Unit {
  let c = block_tint(block, 2)
  let (tx, ty) = tile_for_face(block, 2)
  let (u0, v0, u1, v1) = tile_uv(tx, ty)
  push_quad(
    positions, colors, uvs, x0, y, z0, x0, y, z1, x1, y, z1, x1, y, z0, c, u0, v0,
    u1, v1,
  )
}

///|
fn append_face_east_rect(
  positions : Array[Double],
  colors : Array[Double],
  uvs : Array[Double],
  x : Double,
  z0 : Double,
  z1 : Double,
  y0 : Double,
  y1 : Double,
  block : Block,
) -> Unit {
  let c = block_tint(block, 0)
  let (tx, ty) = tile_for_face(block, 0)
  let (u0, v0, u1, v1) = tile_uv(tx, ty)
  push_quad(
    positions, colors, uvs, x, y0, z0, x, y0, z1, x, y1, z1, x, y1, z0, c, u0, v0,
    u1, v1,
  )
}

///|
fn append_face_west_rect(
  positions : Array[Double],
  colors : Array[Double],
  uvs : Array[Double],
  x : Double,
  z0 : Double,
  z1 : Double,
  y0 : Double,
  y1 : Double,
  block : Block,
) -> Unit {
  let c = block_tint(block, 1)
  let (tx, ty) = tile_for_face(block, 1)
  let (u0, v0, u1, v1) = tile_uv(tx, ty)
  push_quad(
    positions, colors, uvs, x, y0, z1, x, y0, z0, x, y1, z0, x, y1, z1, c, u0, v0,
    u1, v1,
  )
}

///|
fn append_face_south_rect(
  positions : Array[Double],
  colors : Array[Double],
  uvs : Array[Double],
  x0 : Double,
  x1 : Double,
  z : Double,
  y0 : Double,
  y1 : Double,
  block : Block,
) -> Unit {
  let c = block_tint(block, 4)
  let (tx, ty) = tile_for_face(block, 4)
  let (u0, v0, u1, v1) = tile_uv(tx, ty)
  push_quad(
    positions, colors, uvs, x0, y0, z, x1, y0, z, x1, y1, z, x0, y1, z, c, u0, v0,
    u1, v1,
  )
}

///|
fn append_face_north_rect(
  positions : Array[Double],
  colors : Array[Double],
  uvs : Array[Double],
  x0 : Double,
  x1 : Double,
  z : Double,
  y0 : Double,
  y1 : Double,
  block : Block,
) -> Unit {
  let c = block_tint(block, 5)
  let (tx, ty) = tile_for_face(block, 5)
  let (u0, v0, u1, v1) = tile_uv(tx, ty)
  push_quad(
    positions, colors, uvs, x1, y0, z, x0, y0, z, x0, y1, z, x1, y1, z, c, u0, v0,
    u1, v1,
  )
}

///|
fn append_face_down(
  positions : Array[Double],
  colors : Array[Double],
  uvs : Array[Double],
  x : Int,
  y : Int,
  z : Int,
  block : Block,
) -> Unit {
  let c = block_tint(block, 3)
  let (tx, ty) = tile_for_face(block, 3)
  let (u0, v0, u1, v1) = tile_uv(tx, ty)
  let x0 = x.to_double()
  let x1 = x.to_double() + 1.0
  let y0 = y.to_double()
  let z0 = z.to_double()
  let z1 = z.to_double() + 1.0
  push_quad(
    positions, colors, uvs, x0, y0, z0, x1, y0, z0, x1, y0, z1, x0, y0, z1, c, u0,
    v0, u1, v1,
  )
}

///|
fn append_face_south(
  positions : Array[Double],
  colors : Array[Double],
  uvs : Array[Double],
  x : Int,
  y : Int,
  z : Int,
  block : Block,
) -> Unit {
  let c = block_tint(block, 4)
  let (tx, ty) = tile_for_face(block, 4)
  let (u0, v0, u1, v1) = tile_uv(tx, ty)
  let x0 = x.to_double()
  let x1 = x.to_double() + 1.0
  let y0 = y.to_double()
  let y1 = y.to_double() + 1.0
  let z1 = z.to_double() + 1.0
  push_quad(
    positions, colors, uvs, x0, y0, z1, x1, y0, z1, x1, y1, z1, x0, y1, z1, c, u0,
    v0, u1, v1,
  )
}

///|
fn append_face_north(
  positions : Array[Double],
  colors : Array[Double],
  uvs : Array[Double],
  x : Int,
  y : Int,
  z : Int,
  block : Block,
) -> Unit {
  let c = block_tint(block, 5)
  let (tx, ty) = tile_for_face(block, 5)
  let (u0, v0, u1, v1) = tile_uv(tx, ty)
  let x0 = x.to_double()
  let x1 = x.to_double() + 1.0
  let y0 = y.to_double()
  let y1 = y.to_double() + 1.0
  let z0 = z.to_double()
  push_quad(
    positions, colors, uvs, x1, y0, z0, x0, y0, z0, x0, y1, z0, x1, y1, z0, c, u0,
    v0, u1, v1,
  )
}