///|
pub(all) struct MaterialSample {
  base_color : Color4
  metallic : Double
  roughness : Double
  emissive : Vec3
  alpha : Double
  normal_scale : Double
  occlusion_strength : Double
} derive(Debug, ToJson, Eq)

///|
pub(all) struct MaterialSampleStats {
  count : Int
  average_color : Color4
  average_metallic : Double
  average_roughness : Double
  transparent_count : Int
  metallic_count : Int
  emissive_count : Int
  valid : Bool
} derive(Debug, ToJson, Eq)

///|
pub(all) struct MaterialFeatureKey {
  has_base_texture : Bool
  has_normal_texture : Bool
  has_emissive_texture : Bool
  has_occlusion_texture : Bool
  alpha_mode : String
  double_sided : Bool
} derive(Debug, ToJson, Eq)

///|
pub fn material_sample(
  base_color : Color4,
  metallic : Double,
  roughness : Double,
  emissive : Vec3,
  alpha : Double,
) -> MaterialSample {
  {
    base_color,
    metallic,
    roughness,
    emissive,
    alpha,
    normal_scale: 1.0,
    occlusion_strength: 1.0,
  }
}

///|
pub fn material_clamp(
  value : Double,
  minimum : Double,
  maximum : Double,
) -> Double {
  if value < minimum {
    minimum
  } else if value > maximum {
    maximum
  } else {
    value
  }
}

///|
pub fn material_clamp_color(value : Color4) -> Color4 {
  {
    r: material_clamp(value.r, 0.0, 1.0),
    g: material_clamp(value.g, 0.0, 1.0),
    b: material_clamp(value.b, 0.0, 1.0),
    a: material_clamp(value.a, 0.0, 1.0),
  }
}

///|
pub fn material_sample_normalize(sample : MaterialSample) -> MaterialSample {
  {
    base_color: material_clamp_color(sample.base_color),
    metallic: material_clamp(sample.metallic, 0.0, 1.0),
    roughness: material_clamp(sample.roughness, 0.0, 1.0),
    emissive: sample.emissive,
    alpha: material_clamp(sample.alpha, 0.0, 1.0),
    normal_scale: material_clamp(sample.normal_scale, 0.0, 8.0),
    occlusion_strength: material_clamp(sample.occlusion_strength, 0.0, 1.0),
  }
}

///|
pub fn material_sample_lerp(
  left : MaterialSample,
  right : MaterialSample,
  factor : Double,
) -> MaterialSample {
  let t = material_clamp(factor, 0.0, 1.0)
  material_sample(
    {
      r: left.base_color.r + (right.base_color.r - left.base_color.r) * t,
      g: left.base_color.g + (right.base_color.g - left.base_color.g) * t,
      b: left.base_color.b + (right.base_color.b - left.base_color.b) * t,
      a: left.base_color.a + (right.base_color.a - left.base_color.a) * t,
    },
    left.metallic + (right.metallic - left.metallic) * t,
    left.roughness + (right.roughness - left.roughness) * t,
    {
      x: left.emissive.x + (right.emissive.x - left.emissive.x) * t,
      y: left.emissive.y + (right.emissive.y - left.emissive.y) * t,
      z: left.emissive.z + (right.emissive.z - left.emissive.z) * t,
    },
    left.alpha + (right.alpha - left.alpha) * t,
  )
}

///|
pub fn material_sample_energy(sample : MaterialSample) -> Double {
  let color = (sample.base_color.r + sample.base_color.g + sample.base_color.b) /
    3.0
  color *
  (1.0 - sample.metallic * 0.5) *
  (1.0 - sample.roughness * 0.25) *
  sample.alpha
}

///|
pub fn material_sample_is_transparent(
  sample : MaterialSample,
  threshold : Double,
) -> Bool {
  sample.alpha < threshold
}

///|
pub fn material_sample_is_metallic(
  sample : MaterialSample,
  threshold : Double,
) -> Bool {
  sample.metallic >= threshold
}

///|
pub fn material_sample_is_emissive(
  sample : MaterialSample,
  threshold : Double,
) -> Bool {
  topology_length_squared(sample.emissive) >= threshold * threshold
}

///|
pub fn material_sample_stats(
  samples : Array[MaterialSample],
) -> MaterialSampleStats {
  let count = samples.length()
  let mut color = { r: 0.0, g: 0.0, b: 0.0, a: 0.0 }
  let mut metallic = 0.0
  let mut roughness = 0.0
  let mut transparent = 0
  let mut metallic_count = 0
  let mut emissive = 0
  for sample in samples {
    color = {
      r: color.r + sample.base_color.r,
      g: color.g + sample.base_color.g,
      b: color.b + sample.base_color.b,
      a: color.a + sample.base_color.a,
    }
    metallic = metallic + sample.metallic
    roughness = roughness + sample.roughness
    if material_sample_is_transparent(sample, 0.999) {
      transparent = transparent + 1
    }
    if material_sample_is_metallic(sample, 0.5) {
      metallic_count = metallic_count + 1
    }
    if material_sample_is_emissive(sample, 0.001) {
      emissive = emissive + 1
    }
  }
  let divisor = if count == 0 { 1.0 } else { Double::from_int(count) }
  {
    count,
    average_color: {
      r: color.r / divisor,
      g: color.g / divisor,
      b: color.b / divisor,
      a: color.a / divisor,
    },
    average_metallic: metallic / divisor,
    average_roughness: roughness / divisor,
    transparent_count: transparent,
    metallic_count,
    emissive_count: emissive,
    valid: samples.all(fn(sample) {
      material_sample_normalize(sample) == sample
    }),
  }
}

///|
pub fn material_feature_key(material : GltfMaterial) -> MaterialFeatureKey {
  {
    has_base_texture: material.pbr_metallic_roughness.base_color_texture
    is Some(_),
    has_normal_texture: material.normal_texture is Some(_),
    has_emissive_texture: material.emissive_texture is Some(_),
    has_occlusion_texture: material.occlusion_texture is Some(_),
    alpha_mode: alpha_mode_name(material.alpha_mode),
    double_sided: material.double_sided,
  }
}

///|
pub fn material_feature_key_string(key : MaterialFeatureKey) -> String {
  "base=\{key.has_base_texture};normal=\{key.has_normal_texture};emissive=\{key.has_emissive_texture};occlusion=\{key.has_occlusion_texture};alpha=\{key.alpha_mode};double=\{key.double_sided}"
}

///|
pub fn material_feature_key_equal(
  left : MaterialFeatureKey,
  right : MaterialFeatureKey,
) -> Bool {
  left.has_base_texture == right.has_base_texture &&
  left.has_normal_texture == right.has_normal_texture &&
  left.has_emissive_texture == right.has_emissive_texture &&
  left.has_occlusion_texture == right.has_occlusion_texture &&
  left.alpha_mode == right.alpha_mode &&
  left.double_sided == right.double_sided
}

///|
pub fn material_group_features(
  materials : Array[GltfMaterial],
) -> Map[String, Int] {
  let groups : Map[String, Int] = Map([])
  for material in materials {
    let key = material_feature_key_string(material_feature_key(material))
    groups[key] = match groups.get(key) {
      Some(value) => value + 1
      None => 1
    }
  }
  groups
}

///|
pub fn material_sample_lines(stats : MaterialSampleStats) -> Array[String] {
  [
    "samples=\{stats.count}",
    "average_metallic=\{stats.average_metallic}",
    "average_roughness=\{stats.average_roughness}",
    "transparent=\{stats.transparent_count}",
    "metallic=\{stats.metallic_count}",
    "emissive=\{stats.emissive_count}",
    "valid=\{stats.valid}",
  ]
}