///|
/// Decode the supported glTF subset into the addon scene model.
///
/// This function accepts already-loaded bytes only. `binary` is the GLB BIN
/// chunk (or the single buffer supplied by an app); external JSON resources
/// are obtained through `resolve`.
pub fn load_gltf_scene(
  json : String,
  binary? : Bytes? = None,
  resolve? : GltfResourceResolver? = None,
) -> Scene raise GltfError {
  let root : GltfRoot = @json.from_json(@json.parse(json)) catch {
    err => raise InvalidJson(err.to_string())
  }
  if root.asset.version != "2.0" {
    raise UnsupportedAssetVersion(root.asset.version)
  }
  for extension in root.extensionsRequired.unwrap_or([]) {
    raise UnsupportedRequiredExtension(extension)
  }
  for extension in root.extensionsUsed.unwrap_or([]) {
    raise UnsupportedExtension(extension)
  }

  let buffers = root.buffers.unwrap_or([])
  let buffer_views = root.bufferViews.unwrap_or([])
  let accessors = root.accessors.unwrap_or([])
  let buffer_data : Array[Bytes?] = Array::make(buffers.length(), None)
  for index, buffer in buffers {
    match buffer.uri {
      None => buffer_data[index] = binary
      Some(uri) =>
        match decode_data_uri(uri) {
          Some(bytes) => buffer_data[index] = Some(bytes)
          None =>
            match resolve {
              None => raise ExternalResourceUnavailable(uri)
              Some(loader) =>
                match loader(uri) {
                  None => raise ExternalResourceUnavailable(uri)
                  Some(bytes) => buffer_data[index] = Some(bytes)
                }
            }
        }
    }
    match buffer_data[index] {
      None => raise MissingBuffer(index)
      Some(bytes) =>
        if bytes.length() < buffer.byteLength {
          raise MissingBuffer(index)
        }
    }
  }

  let scene = Scene::empty()
  let sampler_json = root.samplers.unwrap_or([])
  let sampler_ids : Array[SamplerId?] = Array::make(sampler_json.length(), None)
  for index, sampler in sampler_json {
    let mag_filter = filter_from_gltf(sampler.magFilter, false)
    let min_filter = filter_from_gltf(sampler.minFilter, true)
    let wrap_s = wrap_from_gltf(sampler.wrapS)
    let wrap_t = wrap_from_gltf(sampler.wrapT)
    sampler_ids[index] = Some(
      scene.add_sampler(
        Sampler::new(mag_filter~, min_filter~, wrap_s~, wrap_t~),
      ),
    )
  }
  let images = root.images.unwrap_or([])
  let texture_json = root.textures.unwrap_or([])
  let texture_ids : Array[TextureId?] = Array::make(texture_json.length(), None)
  for texture_index, texture in texture_json {
    let source_index = match texture.source {
      None => raise InvalidTexture(texture_index)
      Some(value) => value
    }
    if source_index < 0 || source_index >= images.length() {
      raise InvalidTexture(source_index)
    }
    let image = images[source_index]
    let sampler = match texture.sampler {
      None => None
      Some(value) =>
        if value >= 0 && value < sampler_ids.length() {
          sampler_ids[value]
        } else {
          raise InvalidSampler(value)
        }
    }
    let texture_id = match image.uri {
      Some(uri) =>
        scene.add_texture(Texture::uri(uri, mime_type=image.mimeType, sampler~))
      None =>
        match image.bufferView {
          None => raise InvalidTexture(source_index)
          Some(view_index) => {
            let bytes = read_buffer_view(view_index, buffer_views, buffer_data)
            scene.add_texture(
              Texture::embedded(bytes, mime_type=image.mimeType, sampler~),
            )
          }
        }
    }
    texture_ids[texture_index] = Some(texture_id)
  }

  let material_json = root.materials.unwrap_or([])
  let material_ids : Array[MaterialId?] = Array::make(
    material_json.length(),
    None,
  )
  for index, material in material_json {
    let pbr = material.pbrMetallicRoughness
    let base_color = match pbr {
      None => Vec4::new(x=1.0, y=1.0, z=1.0, w=1.0)
      Some(value) =>
        vec4_from_array(
          value.baseColorFactor,
          Vec4::new(x=1.0, y=1.0, z=1.0, w=1.0),
        )
    }
    let metallic = match pbr {
      None => 1.0
      Some(value) => value.metallicFactor.unwrap_or(1.0)
    }
    let roughness = match pbr {
      None => 1.0
      Some(value) => value.roughnessFactor.unwrap_or(1.0)
    }
    let base_color_texture = match pbr {
      None => None
      Some(value) =>
        match value.baseColorTexture {
          None => None
          Some(info) =>
            if info.index >= 0 && info.index < texture_ids.length() {
              texture_ids[info.index]
            } else {
              raise InvalidTexture(info.index)
            }
        }
    }
    let normal_texture = texture_info_id(material.normalTexture, texture_ids)
    let occlusion_texture = texture_info_id(
      material.occlusionTexture,
      texture_ids,
    )
    let emissive_texture = texture_info_id(
      material.emissiveTexture,
      texture_ids,
    )
    let emissive_factor = vec3_from_array(material.emissiveFactor, Vec3::zero())
    let alpha_mode = match material.alphaMode.unwrap_or("OPAQUE") {
      "MASK" => AlphaMode::Mask
      "BLEND" => Blend
      _ => Opaque
    }
    material_ids[index] = Some(
      scene.add_material(
        Material::pbr(
          base_color~,
          metallic~,
          roughness~,
          base_color_texture~,
          normal_texture~,
          occlusion_texture~,
          emissive_texture~,
          emissive_factor~,
          alpha_mode~,
          alpha_cutoff=material.alphaCutoff.unwrap_or(0.5),
          double_sided=material.doubleSided.unwrap_or(false),
        ),
      ),
    )
  }

  let mesh_json = root.meshes.unwrap_or([])
  for mesh in mesh_json {
    let primitives : Array[Primitive] = []
    for primitive in mesh.primitives {
      let mode = primitive.mode.unwrap_or(4)
      if mode != 4 {
        raise UnsupportedPrimitiveMode(mode)
      }
      let position_accessor = match primitive.attributes.get("POSITION") {
        None => raise MissingAttribute("POSITION")
        Some(value) => value
      }
      let positions = decode_vec3_accessor(
        position_accessor, accessors, buffer_views, buffer_data,
      )
      let normals = match primitive.attributes.get("NORMAL") {
        None => Array::make(positions.length(), Vec3::new(x=0.0, y=1.0, z=0.0))
        Some(value) =>
          decode_vec3_accessor(value, accessors, buffer_views, buffer_data)
      }
      let texcoords = match primitive.attributes.get("TEXCOORD_0") {
        None => Array::make(positions.length(), Vec2::zero())
        Some(value) =>
          decode_vec2_accessor(value, accessors, buffer_views, buffer_data)
      }
      let joints = match primitive.attributes.get("JOINTS_0") {
        None => Array::make(positions.length(), (0, 0, 0, 0))
        Some(value) =>
          decode_joints_accessor(value, accessors, buffer_views, buffer_data)
      }
      let weights = match primitive.attributes.get("WEIGHTS_0") {
        None => Array::make(positions.length(), Vec4::zero())
        Some(value) =>
          decode_vec4_accessor(value, accessors, buffer_views, buffer_data)
      }
      if normals.length() != positions.length() ||
        texcoords.length() != positions.length() ||
        joints.length() != positions.length() ||
        weights.length() != positions.length() {
        raise InvalidAccessor(position_accessor)
      }
      let vertices : Array[Vertex] = []
      for index in 0.. Array::make(positions.length(), Vec3::zero())
          Some(value) =>
            decode_vec3_accessor(value, accessors, buffer_views, buffer_data)
        }
        let normals_delta = match target.get("NORMAL") {
          None => Array::make(positions.length(), Vec3::zero())
          Some(value) =>
            decode_vec3_accessor(value, accessors, buffer_views, buffer_data)
        }
        let tangents_delta = match target.get("TANGENT") {
          None => Array::make(positions.length(), Vec3::zero())
          Some(value) =>
            decode_vec3_accessor(value, accessors, buffer_views, buffer_data)
        }
        if positions_delta.length() != positions.length() ||
          normals_delta.length() != positions.length() ||
          tangents_delta.length() != positions.length() {
          raise InvalidAccessor(position_accessor)
        }
        let deltas : Array[VertexDelta] = []
        for vertex_index in 0.. sequential_indices(vertices.length())
        Some(value) =>
          decode_indices_accessor(value, accessors, buffer_views, buffer_data)
      }
      let material = match primitive.material {
        None => None
        Some(value) =>
          if value >= 0 && value < material_ids.length() {
            material_ids[value]
          } else {
            raise InvalidAccessor(value)
          }
      }
      primitives.push(
        Primitive::new(vertices~, indices~, material~, morph_targets~),
      )
    }
    ignore(
      scene.add_mesh(
        Mesh::new(
          name=mesh.name.unwrap_or(""),
          primitives~,
          weights=mesh.weights.unwrap_or([]),
        ),
      ),
    )
  }

  let nodes = root.nodes.unwrap_or([])
  let skin_json = root.skins.unwrap_or([])
  for skin in skin_json {
    let joints : Array[NodeId] = []
    for joint in skin.joints {
      if joint < 0 || joint >= nodes.length() {
        raise InvalidNode(joint)
      }
      joints.push(NodeId::new(joint))
    }
    let inverse_bind_matrices = match skin.inverseBindMatrices {
      None => []
      Some(value) =>
        decode_mat4_accessor(value, accessors, buffer_views, buffer_data)
    }
    if inverse_bind_matrices.length() != 0 &&
      inverse_bind_matrices.length() != joints.length() {
      raise InvalidSkin(0)
    }
    let skeleton = match skin.skeleton {
      None => None
      Some(value) =>
        if value >= 0 && value < nodes.length() {
          Some(NodeId::new(value))
        } else {
          raise InvalidNode(value)
        }
    }
    ignore(
      scene.add_skin(
        Skin::new(
          name=skin.name.unwrap_or(""),
          joints~,
          inverse_bind_matrices~,
          skeleton~,
        ),
      ),
    )
  }
  let parents : Array[Int?] = Array::make(nodes.length(), None)
  for parent_index, node in nodes {
    for child in node.children.unwrap_or([]) {
      if child < 0 || child >= parents.length() {
        raise InvalidNode(child)
      }
      match parents[child] {
        Some(previous) if previous != parent_index => raise InvalidNode(child)
        _ => parents[child] = Some(parent_index)
      }
    }
  }
  for index in 0..= 0 {
      if seen.contains(current) {
        raise InvalidNodeCycle(index)
      }
      seen.push(current)
      current = match parents[current] {
        Some(parent) => parent
        None => -1
      }
    }
  }
  for index, node in nodes {
    let mesh = match node.mesh {
      None => None
      Some(value) =>
        if value >= 0 && value < mesh_json.length() {
          Some(MeshId::new(value))
        } else {
          raise InvalidAccessor(value)
        }
    }
    let parent = parents[index].map(value => NodeId::new(value))
    let camera = match node.camera {
      None => None
      Some(value) =>
        if value >= 0 && value < root.cameras.unwrap_or([]).length() {
          Some(value)
        } else {
          raise InvalidCamera(value)
        }
    }
    let skin = match node.skin {
      None => None
      Some(value) =>
        if value >= 0 && value < skin_json.length() {
          Some(value)
        } else {
          raise InvalidSkin(value)
        }
    }
    ignore(
      scene.add_node(
        Node::new(
          name=node.name.unwrap_or(""),
          parent~,
          local_transform=node_transform(node),
          mesh~,
          camera~,
          skin~,
          weights=node.weights,
        ),
      ),
    )
  }

  let camera_json = root.cameras.unwrap_or([])
  for camera in camera_json {
    let projection = match camera.camera_type {
      "perspective" =>
        match camera.perspective {
          None => raise InvalidCamera(0)
          Some(value) =>
            CameraProjection::Perspective(
              value.yfov,
              value.aspectRatio.unwrap_or(0.0),
              value.znear,
              value.zfar.unwrap_or(0.0),
            )
        }
      "orthographic" =>
        match camera.orthographic {
          None => raise InvalidCamera(0)
          Some(value) =>
            Orthographic(value.xmag, value.ymag, value.znear, value.zfar)
        }
      _ => raise InvalidCamera(0)
    }
    ignore(
      scene.add_camera(Camera::new(name=camera.name.unwrap_or(""), projection~)),
    )
  }
  let scene_json = root.scenes.unwrap_or([])
  for definition in scene_json {
    let roots : Array[NodeId] = []
    for node in definition.nodes.unwrap_or([]) {
      if node < 0 || node >= nodes.length() {
        raise InvalidScene(node)
      }
      roots.push(NodeId::new(node))
    }
    ignore(
      scene.add_scene_definition(
        SceneDefinition::new(name=definition.name.unwrap_or(""), roots~),
      ),
    )
  }
  match root.scene {
    None => ()
    Some(value) =>
      if value < 0 || value >= scene_json.length() {
        raise InvalidScene(value)
      } else {
        scene.set_default_scene(Some(value))
      }
  }

  let animation_json = root.animations.unwrap_or([])
  for animation in animation_json {
    let channels : Array[AnimationChannel] = []
    for channel in animation.channels {
      let node_index = match channel.target.node {
        Some(value) => value
        None => raise InvalidAnimation(0)
      }
      if node_index < 0 || node_index >= nodes.length() {
        raise InvalidAnimation(node_index)
      }
      if channel.sampler < 0 || channel.sampler >= animation.samplers.length() {
        raise InvalidAnimation(channel.sampler)
      }
      let sampler = animation.samplers[channel.sampler]
      let path = match channel.target.path {
        "translation" => AnimationPath::Translation
        "rotation" => Rotation
        "scale" => Scale
        "weights" => Weights
        value => raise UnsupportedAnimationPath(value)
      }
      let components = match path {
        Rotation => 4
        Weights => 1
        _ => 3
      }
      let times = decode_accessor_values(
        sampler.input,
        accessors,
        buffer_views,
        buffer_data,
        1,
      )
      let outputs = decode_accessor_values(
        sampler.output,
        accessors,
        buffer_views,
        buffer_data,
        components,
      )
      if times.length() != outputs.length() &&
        sampler.interpolation.unwrap_or("LINEAR") != "CUBICSPLINE" {
        raise InvalidAnimation(channel.sampler)
      }
      let interpolation = match sampler.interpolation.unwrap_or("LINEAR") {
        "STEP" => AnimationInterpolation::Step
        "CUBICSPLINE" => CubicSpline
        _ => Linear
      }
      let keyframes : Array[AnimationKeyframe] = []
      let key_count = times.length()
      for index in 0..= outputs.length() {
          raise InvalidAnimation(channel.sampler)
        }
        let incoming = if interpolation == CubicSpline &&
          index * 3 < outputs.length() {
          outputs[index * 3]
        } else {
          []
        }
        let outgoing = if interpolation == CubicSpline &&
          index * 3 + 2 < outputs.length() {
          outputs[index * 3 + 2]
        } else {
          []
        }
        keyframes.push(
          AnimationKeyframe::new(
            time=times[index][0],
            value=outputs[value_index],
            in_tangent=incoming,
            out_tangent=outgoing,
          ),
        )
      }
      channels.push(
        AnimationChannel::new(
          node=NodeId::new(node_index),
          path~,
          interpolation~,
          keyframes~,
        ),
      )
    }
    ignore(
      scene.add_animation(
        AnimationClip::new(name=animation.name.unwrap_or(""), channels~),
      ),
    )
  }
  scene
}

///|
fn texture_info_id(
  info : GltfTextureInfo?,
  texture_ids : Array[TextureId?],
) -> TextureId? raise GltfError {
  match info {
    None => None
    Some(value) =>
      if value.index >= 0 && value.index < texture_ids.length() {
        texture_ids[value.index]
      } else {
        raise InvalidTexture(value.index)
      }
  }
}

///|
fn node_transform(node : GltfNodeJson) -> Mat4 {
  match node.matrix {
    Some(values) =>
      if values.length() == 16 {
        // glTF matrices are column-major; the public Mat4 is row-major.
        Mat4::from_values([
          values[0],
          values[4],
          values[8],
          values[12],
          values[1],
          values[5],
          values[9],
          values[13],
          values[2],
          values[6],
          values[10],
          values[14],
          values[3],
          values[7],
          values[11],
          values[15],
        ])
      } else {
        trs_transform(node)
      }
    None => trs_transform(node)
  }
}

///|
fn trs_transform(node : GltfNodeJson) -> Mat4 {
  let translation = vec3_from_array(node.translation, Vec3::zero())
  let scale = vec3_from_array(node.scale, Vec3::new(x=1.0, y=1.0, z=1.0))
  let rotation = match node.rotation {
    Some(values) =>
      if values.length() == 4 {
        Quat::new(x=values[0], y=values[1], z=values[2], w=values[3])
      } else {
        Quat::identity()
      }
    None => Quat::identity()
  }
  Mat4::translation(translation)
  .mul(rotation.to_mat4())
  .mul(
    Mat4::from_values([
      scale.x,
      0.0,
      0.0,
      0.0,
      0.0,
      scale.y,
      0.0,
      0.0,
      0.0,
      0.0,
      scale.z,
      0.0,
      0.0,
      0.0,
      0.0,
      1.0,
    ]),
  )
}

///|
fn vec3_from_array(values : Array[Double]?, fallback : Vec3) -> Vec3 {
  match values {
    Some(items) =>
      if items.length() >= 3 {
        Vec3::new(x=items[0], y=items[1], z=items[2])
      } else {
        fallback
      }
    None => fallback
  }
}

///|
fn vec4_from_array(values : Array[Double]?, fallback : Vec4) -> Vec4 {
  match values {
    Some(items) =>
      if items.length() >= 4 {
        Vec4::new(x=items[0], y=items[1], z=items[2], w=items[3])
      } else {
        fallback
      }
    None => fallback
  }
}

///|
fn sequential_indices(length : Int) -> Array[Int] {
  let result : Array[Int] = []
  for index in 0.. Bytes? {
  if !uri.has_prefix("data:") {
    return None
  }
  let mut comma = -1
  for index in 0.. return None })
}

///|
fn filter_from_gltf(
  value : Int?,
  mipmap : Bool,
) -> SamplerFilter raise GltfError {
  match value {
    None => if mipmap { LinearMipmapLinear } else { Linear }
    Some(9728) => Nearest
    Some(9729) => Linear
    Some(9984) => NearestMipmapNearest
    Some(9985) => LinearMipmapNearest
    Some(9986) => NearestMipmapLinear
    Some(9987) => LinearMipmapLinear
    Some(_) => raise InvalidSampler(0)
  }
}

///|
fn wrap_from_gltf(value : Int?) -> SamplerWrap raise GltfError {
  match value {
    None => Repeat
    Some(10497) => Repeat
    Some(33648) => MirroredRepeat
    Some(33071) => ClampToEdge
    Some(_) => raise InvalidSampler(0)
  }
}

///|
fn decode_vec3_accessor(
  accessor_index : Int,
  accessors : Array[GltfAccessorJson],
  views : Array[GltfBufferView],
  buffers : Array[Bytes?],
) -> Array[Vec3] raise GltfError {
  let accessor = accessor_at(accessor_index, accessors)
  if accessor.accessor_kind != "VEC3" {
    raise UnsupportedAccessorType(accessor.accessor_kind)
  }
  let values = decode_accessor_values(
    accessor_index, accessors, views, buffers, 3,
  )
  let result : Array[Vec3] = []
  for item in values {
    result.push(Vec3::new(x=item[0], y=item[1], z=item[2]))
  }
  result
}

///|
fn decode_vec2_accessor(
  accessor_index : Int,
  accessors : Array[GltfAccessorJson],
  views : Array[GltfBufferView],
  buffers : Array[Bytes?],
) -> Array[Vec2] raise GltfError {
  let accessor = accessor_at(accessor_index, accessors)
  if accessor.accessor_kind != "VEC2" {
    raise UnsupportedAccessorType(accessor.accessor_kind)
  }
  let values = decode_accessor_values(
    accessor_index, accessors, views, buffers, 2,
  )
  let result : Array[Vec2] = []
  for item in values {
    result.push(Vec2::new(x=item[0], y=item[1]))
  }
  result
}

///|
fn decode_vec4_accessor(
  accessor_index : Int,
  accessors : Array[GltfAccessorJson],
  views : Array[GltfBufferView],
  buffers : Array[Bytes?],
) -> Array[Vec4] raise GltfError {
  let accessor = accessor_at(accessor_index, accessors)
  if accessor.accessor_kind != "VEC4" {
    raise UnsupportedAccessorType(accessor.accessor_kind)
  }
  decode_accessor_values(accessor_index, accessors, views, buffers, 4).map(item => {
    Vec4::new(x=item[0], y=item[1], z=item[2], w=item[3])
  })
}

///|
fn decode_joints_accessor(
  accessor_index : Int,
  accessors : Array[GltfAccessorJson],
  views : Array[GltfBufferView],
  buffers : Array[Bytes?],
) -> Array[(Int, Int, Int, Int)] raise GltfError {
  let accessor = accessor_at(accessor_index, accessors)
  if accessor.accessor_kind != "VEC4" ||
    (accessor.componentType != 5121 && accessor.componentType != 5123) {
    raise UnsupportedAccessorType(accessor.accessor_kind)
  }
  decode_accessor_values(accessor_index, accessors, views, buffers, 4).map(item => {
    (item[0].to_int(), item[1].to_int(), item[2].to_int(), item[3].to_int())
  })
}

///|
fn decode_mat4_accessor(
  accessor_index : Int,
  accessors : Array[GltfAccessorJson],
  views : Array[GltfBufferView],
  buffers : Array[Bytes?],
) -> Array[Mat4] raise GltfError {
  let accessor = accessor_at(accessor_index, accessors)
  if accessor.accessor_kind != "MAT4" || accessor.componentType != 5126 {
    raise UnsupportedAccessorType(accessor.accessor_kind)
  }
  decode_accessor_values(accessor_index, accessors, views, buffers, 16).map(item => {
    Mat4::from_values([
      item[0],
      item[4],
      item[8],
      item[12],
      item[1],
      item[5],
      item[9],
      item[13],
      item[2],
      item[6],
      item[10],
      item[14],
      item[3],
      item[7],
      item[11],
      item[15],
    ])
  })
}

///|
fn decode_indices_accessor(
  accessor_index : Int,
  accessors : Array[GltfAccessorJson],
  views : Array[GltfBufferView],
  buffers : Array[Bytes?],
) -> Array[Int] raise GltfError {
  let accessor = accessor_at(accessor_index, accessors)
  if accessor.accessor_kind != "SCALAR" {
    raise UnsupportedAccessorType(accessor.accessor_kind)
  }
  if accessor.componentType != 5121 &&
    accessor.componentType != 5123 &&
    accessor.componentType != 5125 {
    raise UnsupportedComponentType(accessor.componentType)
  }
  let values = decode_accessor_values(
    accessor_index, accessors, views, buffers, 1,
  )
  values.map(item => item[0].to_int())
}

///|
fn decode_accessor_values(
  accessor_index : Int,
  accessors : Array[GltfAccessorJson],
  views : Array[GltfBufferView],
  buffers : Array[Bytes?],
  components : Int,
) -> Array[Array[Double]] raise GltfError {
  let accessor = accessor_at(accessor_index, accessors)
  let component_size = component_size_for(accessor.componentType)
  if component_size == 0 {
    raise UnsupportedComponentType(accessor.componentType)
  }
  let element_size = component_size * components
  if accessor.count < 0 {
    raise InvalidAccessor(accessor_index)
  }
  let result : Array[Array[Double]] = Array::make(
    accessor.count,
    Array::make(components, 0.0),
  )
  match accessor.bufferView {
    None =>
      match accessor.sparse {
        None => raise InvalidAccessor(accessor_index)
        Some(_) => ()
      }
    Some(view_index) => {
      if view_index < 0 || view_index >= views.length() {
        raise MissingBufferView(view_index)
      }
      let view = views[view_index]
      if view.buffer < 0 || view.buffer >= buffers.length() {
        raise MissingBuffer(view.buffer)
      }
      let bytes = match buffers[view.buffer] {
        None => raise MissingBuffer(view.buffer)
        Some(value) => value
      }
      let stride = view.byteStride.unwrap_or(element_size)
      if stride < element_size {
        raise InvalidAccessor(accessor_index)
      }
      let view_start = view.byteOffset.unwrap_or(0)
      let start = view_start + accessor.byteOffset.unwrap_or(0)
      let required = if accessor.count == 0 {
        start
      } else {
        start + (accessor.count - 1) * stride + element_size
      }
      if start < 0 ||
        required > bytes.length() ||
        required > view_start + view.byteLength {
        raise InvalidAccessor(accessor_index)
      }
      for index in 0.. ()
    Some(sparse) => {
      if sparse.count < 0 || sparse.count > accessor.count {
        raise InvalidAccessor(accessor_index)
      }
      if sparse.indices.componentType != 5121 &&
        sparse.indices.componentType != 5123 &&
        sparse.indices.componentType != 5125 {
        raise UnsupportedComponentType(sparse.indices.componentType)
      }
      let index_bytes = read_buffer_view(
        sparse.indices.bufferView,
        views,
        buffers,
      )
      let value_bytes = read_buffer_view(
        sparse.values.bufferView,
        views,
        buffers,
      )
      let index_size = component_size_for(sparse.indices.componentType)
      let values_start = sparse.values.byteOffset.unwrap_or(0)
      let indices_start = sparse.indices.byteOffset.unwrap_or(0)
      let values_required = values_start + sparse.count * element_size
      let indices_required = indices_start + sparse.count * index_size
      if values_start < 0 ||
        indices_start < 0 ||
        values_required > value_bytes.length() ||
        indices_required > index_bytes.length() {
        raise InvalidAccessor(accessor_index)
      }
      for sparse_index in 0..= accessor.count {
          raise InvalidAccessor(accessor_index)
        }
        let values : Array[Double] = []
        let base = values_start + sparse_index * element_size
        for component in 0.. GltfAccessorJson raise GltfError {
  if index < 0 || index >= accessors.length() {
    raise MissingAccessor(index)
  }
  accessors[index]
}

///|
fn component_size_for(component_type : Int) -> Int {
  if component_type == 5120 || component_type == 5121 {
    1
  } else if component_type == 5122 || component_type == 5123 {
    2
  } else if component_type == 5125 || component_type == 5126 {
    4
  } else {
    0
  }
}

///|
fn read_component(
  bytes : Bytes,
  offset : Int,
  component_type : Int,
  normalized : Bool,
) -> Double {
  if component_type == 5126 {
    Float::reinterpret_from_uint(read_u32_le_unsigned(bytes, offset)).to_double()
  } else if component_type == 5121 {
    let value = bytes[offset].to_int().to_double()
    if normalized {
      value / 255.0
    } else {
      value
    }
  } else if component_type == 5123 {
    let value = read_u16_le(bytes, offset).to_double()
    if normalized {
      value / 65535.0
    } else {
      value
    }
  } else if component_type == 5125 {
    read_u32_le_unsigned(bytes, offset).to_double()
  } else if component_type == 5120 {
    let raw = bytes[offset].to_int()
    let value = if raw > 127 { raw - 256 } else { raw }
    if normalized {
      (value.to_double() / 127.0).max(-1.0)
    } else {
      value.to_double()
    }
  } else if component_type == 5122 {
    let raw = read_u16_le(bytes, offset)
    let value = if raw > 32767 { raw - 65536 } else { raw }
    if normalized {
      (value.to_double() / 32767.0).max(-1.0)
    } else {
      value.to_double()
    }
  } else {
    0.0
  }
}

///|
fn read_u16_le(bytes : Bytes, offset : Int) -> Int {
  bytes[offset].to_int() | (bytes[offset + 1].to_int() << 8)
}

///|
fn read_u32_le_unsigned(bytes : Bytes, offset : Int) -> UInt {
  bytes[offset].to_uint() |
  (bytes[offset + 1].to_uint() << 8) |
  (bytes[offset + 2].to_uint() << 16) |
  (bytes[offset + 3].to_uint() << 24)
}

///|
fn read_buffer_view(
  index : Int,
  views : Array[GltfBufferView],
  buffers : Array[Bytes?],
) -> Bytes raise GltfError {
  if index < 0 || index >= views.length() {
    raise MissingBufferView(index)
  }
  let view = views[index]
  if view.buffer < 0 || view.buffer >= buffers.length() {
    raise MissingBuffer(view.buffer)
  }
  let bytes = match buffers[view.buffer] {
    None => raise MissingBuffer(view.buffer)
    Some(value) => value
  }
  let start = view.byteOffset.unwrap_or(0)
  let end = start + view.byteLength
  if start < 0 || end > bytes.length() {
    raise InvalidAccessor(index)
  }
  bytes[start:end].to_owned()
}