///|
/// Cross-module validation helpers used before a production artifact is published.
pub(all) struct SceneValidationReport {
  document_errors : Array[String]
  palette_errors : Array[String]
  layout_errors : Array[String]
  quality_errors : Array[String]
  passed : Bool
  score : Double
} derive(Debug)

///|
pub(all) struct SceneLayerSummary {
  name : String
  visible : Bool
  motif_count : Int
  opacity : Double
  bounds : Rect
  coverage : Double
} derive(Eq, Debug, ToJson)

///|
pub fn scene_layer_summary(
  layer : PatternLayer,
  bounds : Rect,
) -> SceneLayerSummary {
  let points = []
  for motif in layer.motifs {
    for point in motif.transformed_path().flatten(subdivisions=6) {
      points.push(point)
    }
  }
  let motif_bounds = bounds_of_points(points)
  let coverage = if bounds.width * bounds.height == 0.0 {
    0.0
  } else {
    motif_bounds.intersect(bounds).width *
    motif_bounds.intersect(bounds).height /
    (bounds.width * bounds.height)
  }
  {
    name: layer.name,
    visible: layer.visible,
    motif_count: layer.motifs.length(),
    opacity: layer.opacity,
    bounds: motif_bounds,
    coverage,
  }
}

///|
pub fn scene_layer_summaries(
  composition : Composition,
) -> Array[SceneLayerSummary] {
  let result = []
  let bounds = Rect::new(0.0, 0.0, composition.width, composition.height)
  for layer in composition.layers {
    result.push(scene_layer_summary(layer, bounds))
  }
  result
}

///|
pub fn scene_document_palette(document : DesignDocument) -> Palette {
  let colors = []
  for layer in document.composition.layers {
    for motif in layer.motifs {
      let color = motif.style.fill
      if !colors.contains(color) {
        colors.push(color)
      }
    }
  }
  let parsed = []
  for _ in colors {
    parsed.push(Color::rgb8(128, 128, 128))
  }
  Palette::new("scene-extracted", parsed)
}

///|
pub fn scene_validate_layers(composition : Composition) -> Array[String] {
  let errors = []
  for layer in composition.layers {
    if layer.name.length() == 0 {
      errors.push("layer name is empty")
    }
    if layer.opacity < 0.0 || layer.opacity > 1.0 {
      errors.push("layer opacity outside [0,1]")
    }
    if layer.visible && layer.motifs.length() == 0 {
      errors.push("visible layer has no motifs: " + layer.name)
    }
  }
  errors
}

///|
pub fn scene_validate_bounds(composition : Composition) -> Array[String] {
  let errors = []
  let bounds = Rect::new(0.0, 0.0, composition.width, composition.height)
  for layer in composition.layers {
    for motif in layer.motifs {
      let motif_bounds = bounds_of_points(
        motif.transformed_path().flatten(subdivisions=5),
      )
      if motif_bounds.width.is_nan() || motif_bounds.height.is_nan() {
        errors.push("motif has non-finite bounds: " + motif.name)
      }
      if motif_bounds.intersect(bounds).width == 0.0 &&
        motif_bounds.intersect(bounds).height == 0.0 {
        errors.push("motif lies outside canvas: " + motif.name)
      }
    }
  }
  errors
}

///|
pub fn scene_validate_render(document : DesignDocument) -> Array[String] {
  let errors = []
  let svg = document.to_svg()
  if svg.length() < 100 {
    errors.push("rendered SVG is unexpectedly small")
  }
  if !svg.contains("") {
    errors.push("rendered output is not closed")
  }
  errors
}

///|
pub fn scene_validation_report(
  document : DesignDocument,
) -> SceneValidationReport {
  let document_errors = document.validate()
  let palette_errors = []
  let layout_errors = scene_validate_layers(document.composition)
  for error in scene_validate_bounds(document.composition) {
    layout_errors.push(error)
  }
  for error in scene_validate_render(document) {
    document_errors.push(error)
  }
  let metrics = composition_quality_metrics(document.composition)
  let quality_errors = []
  if metrics.score < 0.05 {
    quality_errors.push("quality score below minimum")
  }
  if metrics.path_count == 0 {
    quality_errors.push("no production paths")
  }
  let total = document_errors.length() +
    palette_errors.length() +
    layout_errors.length() +
    quality_errors.length()
  let score = (1.0 - total.to_double() / 20.0).max(0.0)
  {
    document_errors,
    palette_errors,
    layout_errors,
    quality_errors,
    passed: total == 0,
    score,
  }
}

///|
pub fn scene_validation_text(report : SceneValidationReport) -> String {
  "passed=" +
  report.passed.to_string() +
  " score=" +
  report.score.to_string() +
  " document=" +
  report.document_errors.join("|") +
  " palette=" +
  report.palette_errors.join("|") +
  " layout=" +
  report.layout_errors.join("|") +
  " quality=" +
  report.quality_errors.join("|")
}

///|
pub fn scene_validation_csv(report : SceneValidationReport) -> String {
  "passed,score,document_errors,palette_errors,layout_errors,quality_errors\n" +
  report.passed.to_string() +
  "," +
  report.score.to_string() +
  "," +
  report.document_errors.length().to_string() +
  "," +
  report.palette_errors.length().to_string() +
  "," +
  report.layout_errors.length().to_string() +
  "," +
  report.quality_errors.length().to_string() +
  "\n"
}

///|
pub fn scene_validate_palette(
  document : DesignDocument,
  palette : Palette,
) -> Array[String] {
  let errors = []
  let metrics = analyze_palette(palette)
  if metrics.color_count == 0 {
    errors.push("provided palette is empty")
  }
  if palette_quality_score(palette, document.composition.background) < 0.1 {
    errors.push("palette quality score is too low")
  }
  errors
}

///|
pub fn scene_validate_all(
  document : DesignDocument,
  palette : Palette,
) -> SceneValidationReport {
  let report = scene_validation_report(document)
  for error in scene_validate_palette(document, palette) {
    report.palette_errors.push(error)
  }
  let passed = report.document_errors.length() == 0 &&
    report.palette_errors.length() == 0 &&
    report.layout_errors.length() == 0 &&
    report.quality_errors.length() == 0
  { ..report, passed, }
}

///|
pub fn scene_summary_json_like(document : DesignDocument) -> String {
  let summaries = scene_layer_summaries(document.composition)
  let builder = StringBuilder()
  builder.write_string(
    "{\"width\":" +
    document.composition.width.to_string() +
    ",\"height\":" +
    document.composition.height.to_string() +
    ",\"layers\":[",
  )
  for index, summary in summaries {
    if index > 0 {
      builder.write_string(",")
    }
    builder.write_string(
      "{\"name\":\"" +
      escape_svg(summary.name) +
      "\",\"visible\":" +
      summary.visible.to_string() +
      ",\"motifs\":" +
      summary.motif_count.to_string() +
      ",\"coverage\":" +
      summary.coverage.to_string() +
      "}",
    )
  }
  builder.write_string("]}")
  builder.to_string()
}

///|
pub fn scene_repair_document(document : DesignDocument) -> DesignDocument {
  let composition = Composition::new(
    document.composition.width.max(1.0),
    document.composition.height.max(1.0),
    background=document.composition.background,
  )
  for layer in document.composition.layers {
    if layer.motifs.length() > 0 {
      composition.add_layer(layer)
    }
  }
  { ..document, composition, }
}

///|
pub fn scene_repair_palette(palette : Palette) -> Palette {
  if palette.colors.length() == 0 {
    Palette::new("repaired", [Color::rgb8(0, 0, 0), Color::rgb8(255, 255, 255)])
  } else {
    palette_dedupe(palette, 0.01)
  }
}

///|
pub fn scene_layer_count(document : DesignDocument) -> Int {
  document.composition.layers.length()
}

///|
pub fn scene_visible_motif_count(document : DesignDocument) -> Int {
  let mut count = 0
  for layer in document.composition.layers {
    if layer.visible {
      count += layer.motifs.length()
    }
  }
  count
}

///|
pub fn scene_export_ready(document : DesignDocument, palette : Palette) -> Bool {
  let report = scene_validate_all(document, palette)
  report.passed && scene_visible_motif_count(document) > 0
}

///|
pub fn scene_dimension_signature(document : DesignDocument) -> String {
  document.composition.width.to_string() +
  "x" +
  document.composition.height.to_string() +
  ":" +
  scene_layer_count(document).to_string() +
  ":" +
  scene_visible_motif_count(document).to_string()
}

///|
pub fn scene_report_lines(
  document : DesignDocument,
  palette : Palette,
) -> Array[String] {
  let report = scene_validate_all(document, palette)
  [
    "signature=" + scene_dimension_signature(document),
    "ready=" + scene_export_ready(document, palette).to_string(),
    "validation=" + scene_validation_text(report),
    "quality=" +
    design_quality_report_text(
      composition_quality_metrics(document.composition),
    ),
  ]
}

///|
pub fn scene_background_contrast(
  document : DesignDocument,
  foreground : Color,
) -> Double {
  foreground.contrast_ratio(document.composition.background)
}

///|
pub fn scene_path_checksum(document : DesignDocument) -> UInt {
  svg_document_checksum(document.composition.render())
}

///|
pub fn scene_layer_names(document : DesignDocument) -> Array[String] {
  let names = []
  for layer in document.composition.layers {
    names.push(layer.name)
  }
  names
}

///|
pub fn scene_layer_opacities(document : DesignDocument) -> Array[Double] {
  let values = []
  for layer in document.composition.layers {
    values.push(layer.opacity)
  }
  values
}

///|
pub fn scene_has_transparent_layers(document : DesignDocument) -> Bool {
  for layer in document.composition.layers {
    if layer.opacity < 0.999 {
      return true
    }
  }
  false
}

///|
pub fn scene_canvas_area(document : DesignDocument) -> Double {
  document.composition.width * document.composition.height
}

///|
pub fn scene_quality_score(document : DesignDocument) -> Double {
  composition_quality_metrics(document.composition).score
}

///|
pub fn scene_quality_gate(document : DesignDocument, minimum : Double) -> Bool {
  let palette = Palette::new("background", [document.composition.background])
  scene_quality_score(document) >= minimum &&
  scene_export_ready(document, palette)
}

///|
pub fn scene_is_square(document : DesignDocument) -> Bool {
  document.composition.width == document.composition.height
}

///|
pub fn scene_aspect_ratio(document : DesignDocument) -> Double {
  document.composition.width / document.composition.height.max(0.0001)
}

///|
pub fn scene_layer_motif_counts(document : DesignDocument) -> Array[Int] {
  let counts = []
  for layer in document.composition.layers {
    counts.push(layer.motifs.length())
  }
  counts
}

///|
pub fn scene_is_deterministic(
  first : DesignDocument,
  second : DesignDocument,
) -> Bool {
  scene_path_checksum(first) == scene_path_checksum(second)
}

///|
pub fn scene_safe_width(document : DesignDocument, maximum : Double) -> Double {
  document.composition.width.min(maximum.max(1.0))
}

///|
pub fn scene_safe_height(document : DesignDocument, maximum : Double) -> Double {
  document.composition.height.min(maximum.max(1.0))
}

///|
pub fn scene_has_background(document : DesignDocument) -> Bool {
  document.composition.background.a > 0.0
}

///|
pub fn scene_nonempty(document : DesignDocument) -> Bool {
  scene_visible_motif_count(document) > 0
}

///|
pub fn scene_pixel_area(
  document : DesignDocument,
  pixels_per_unit : Double,
) -> Double {
  scene_canvas_area(document) *
  pixels_per_unit.max(0.0) *
  pixels_per_unit.max(0.0)
}

///|
pub fn scene_ready_signature(
  document : DesignDocument,
  palette : Palette,
) -> String {
  scene_dimension_signature(document) +
  ":" +
  scene_export_ready(document, palette).to_string() +
  ":" +
  scene_quality_score(document).to_string()
}

///|
pub fn scene_has_layers(document : DesignDocument) -> Bool {
  scene_layer_count(document) > 0
}

///|
pub fn scene_all_layers_visible(document : DesignDocument) -> Bool {
  for layer in document.composition.layers {
    if !layer.visible {
      return false
    }
  }
  true
}

///|
pub fn scene_total_opacity(document : DesignDocument) -> Double {
  document.composition.layers.fold(init=0.0, fn(total, layer) {
    total + layer.opacity
  })
}

///|
pub fn scene_average_layer_opacity(document : DesignDocument) -> Double {
  if scene_layer_count(document) == 0 {
    0.0
  } else {
    scene_total_opacity(document) / scene_layer_count(document).to_double()
  }
}

///|
pub fn scene_motif_density(document : DesignDocument) -> Double {
  scene_visible_motif_count(document).to_double() /
  scene_canvas_area(document).max(1.0)
}

///|
pub fn scene_valid_dimensions(document : DesignDocument) -> Bool {
  document.composition.width > 0.0 && document.composition.height > 0.0
}

///|
pub fn scene_document_bytes(document : DesignDocument) -> Int {
  document.to_svg().length()
}