///|
/// Production-oriented visual quality metrics for generated artwork.
pub(all) struct DesignQualityMetrics {
  canvas_area : Double
  occupied_area : Double
  coverage : Double
  path_count : Int
  layer_count : Int
  total_length : Double
  edge_density : Double
  color_diversity : Double
  contrast : Double
  balance_x : Double
  balance_y : Double
  score : Double
} derive(Eq, Debug, ToJson)

///|
pub(all) struct MotifQualityEntry {
  name : String
  area : Double
  perimeter : Double
  density : Double
  contrast : Double
  score : Double
} derive(Eq, Debug, ToJson)

///|
pub(all) struct RasterQualityMetrics {
  width : Int
  height : Int
  non_empty : Int
  occupancy : Double
  mean_luminance : Double
  luminance_span : Double
  edge_pixels : Int
  edge_density : Double
  connected_regions : Int
  score : Double
} derive(Eq, Debug, ToJson)

///|
pub(all) struct SymmetryQuality {
  horizontal : Double
  vertical : Double
  diagonal : Double
  radial : Double
  best : Double
} derive(Eq, Debug, ToJson)

///|
fn dq_clamp(value : Double) -> Double {
  value.max(0.0).min(1.0)
}

///|
pub fn path_quality_area(path : Path, subdivisions : Int) -> Double {
  let points = path.flatten(subdivisions=subdivisions.max(1))
  polygon_area(points).abs()
}

///|
pub fn path_quality_perimeter(
  path : Path,
  subdivisions : Int,
  closed : Bool,
) -> Double {
  let points = path.flatten(subdivisions=subdivisions.max(1))
  if closed {
    polygon_perimeter(points)
  } else if points.length() < 2 {
    0.0
  } else {
    let mut total = 0.0
    for index in 0..<(points.length() - 1) {
      total += points[index].distance(points[index + 1])
    }
    total
  }
}

///|
pub fn path_quality_density(
  path : Path,
  bounds : Rect,
  subdivisions : Int,
) -> Double {
  let area = path_quality_area(path, subdivisions)
  let canvas = (bounds.width * bounds.height).max(0.0001)
  dq_clamp(area / canvas)
}

///|
pub fn path_quality_center(path : Path, subdivisions : Int) -> Vec2 {
  let points = path.flatten(subdivisions=subdivisions.max(1))
  if points.length() == 0 {
    Vec2::zero()
  } else {
    let mut x = 0.0
    let mut y = 0.0
    for point in points {
      x += point.x
      y += point.y
    }
    Vec2::new(x / points.length().to_double(), y / points.length().to_double())
  }
}

///|
pub fn motif_quality_entry(
  motif : Motif,
  bounds : Rect,
  background : Color,
) -> MotifQualityEntry {
  let path = motif.transformed_path()
  let area = path_quality_area(path, 8)
  let perimeter = path_quality_perimeter(path, 8, true)
  let density = path_quality_density(path, bounds, 8)
  let contrast = Color::rgba(0.5, 0.5, 0.5).contrast_ratio(background)
  let score = dq_clamp(
    0.25 * density +
    0.4 * (contrast / 7.0).min(1.0) +
    0.35 * (perimeter / bounds.width.max(bounds.height).max(1.0)).min(1.0),
  )
  { name: motif.name, area, perimeter, density, contrast, score }
}

///|
pub fn layer_quality_entries(
  layer : PatternLayer,
  bounds : Rect,
  background : Color,
) -> Array[MotifQualityEntry] {
  let result = []
  for motif in layer.motifs {
    result.push(motif_quality_entry(motif, bounds, background))
  }
  result
}

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

///|
pub fn quality_color_diversity(composition : Composition) -> Double {
  let colors = []
  for layer in composition.layers {
    for motif in layer.motifs {
      if !colors.contains(motif.style.fill) {
        colors.push(motif.style.fill)
      }
    }
  }
  if composition.layers.length() == 0 {
    0.0
  } else {
    (colors.length().to_double() /
    composition.layers
    .fold(init=0, fn(total, layer) { total + layer.motifs.length() })
    .max(1)
    .to_double()).min(1.0)
  }
}

///|
pub fn quality_path_length(composition : Composition) -> Double {
  let mut total = 0.0
  for layer in composition.layers {
    for motif in layer.motifs {
      total += path_quality_perimeter(motif.transformed_path(), 6, false)
    }
  }
  total
}

///|
pub fn quality_bounds_of_composition(composition : Composition) -> Rect {
  let bounds = Rect::new(0.0, 0.0, composition.width, composition.height)
  let mut current = Rect::new(0.0, 0.0, 0.0, 0.0)
  let mut initialized = false
  for layer in composition.layers {
    for motif in layer.motifs {
      let next = bounds_of_points(
        motif.transformed_path().flatten(subdivisions=6),
      )
      if !initialized {
        current = next
        initialized = true
      } else {
        current = current.union(next)
      }
    }
  }
  if initialized {
    current.intersect(bounds)
  } else {
    current
  }
}

///|
pub fn quality_balance(composition : Composition) -> (Double, Double) {
  let center = Vec2::new(composition.width / 2.0, composition.height / 2.0)
  let mut mass = 0.0
  let mut x = 0.0
  let mut y = 0.0
  for layer in composition.layers {
    for motif in layer.motifs {
      let path = motif.transformed_path()
      let area = path_quality_area(path, 5).max(0.1)
      let point = path_quality_center(path, 5)
      mass += area
      x += point.x * area
      y += point.y * area
    }
  }
  if mass == 0.0 {
    (0.0, 0.0)
  } else {
    (
      (x / mass - center.x) / composition.width.max(1.0),
      (y / mass - center.y) / composition.height.max(1.0),
    )
  }
}

///|
pub fn quality_symmetry_points(
  points : Array[Vec2],
  bounds : Rect,
) -> SymmetryQuality {
  if points.length() == 0 {
    { horizontal: 1.0, vertical: 1.0, diagonal: 1.0, radial: 1.0, best: 1.0 }
  } else {
    let center = bounds.center()
    let threshold = bounds.width.max(bounds.height).max(1.0)
    let mut horizontal = 0.0
    let mut vertical = 0.0
    let mut diagonal = 0.0
    let mut radial = 0.0
    for point in points {
      let mirrored_h = Vec2::new(point.x, 2.0 * center.y - point.y)
      let mirrored_v = Vec2::new(2.0 * center.x - point.x, point.y)
      let mirrored_d = Vec2::new(
        center.x + point.y - center.y,
        center.y + point.x - center.x,
      )
      let mirrored_r = Vec2::new(
        2.0 * center.x - point.x,
        2.0 * center.y - point.y,
      )
      let mut best_h = threshold
      let mut best_v = threshold
      let mut best_d = threshold
      let mut best_r = threshold
      for candidate in points {
        best_h = best_h.min(mirrored_h.distance(candidate))
        best_v = best_v.min(mirrored_v.distance(candidate))
        best_d = best_d.min(mirrored_d.distance(candidate))
        best_r = best_r.min(mirrored_r.distance(candidate))
      }
      horizontal += 1.0 - (best_h / threshold).min(1.0)
      vertical += 1.0 - (best_v / threshold).min(1.0)
      diagonal += 1.0 - (best_d / threshold).min(1.0)
      radial += 1.0 - (best_r / threshold).min(1.0)
    }
    let n = points.length().to_double()
    {
      horizontal: horizontal / n,
      vertical: vertical / n,
      diagonal: diagonal / n,
      radial: radial / n,
      best: horizontal.max(vertical).max(diagonal).max(radial) / n,
    }
  }
}

///|
pub fn composition_symmetry(composition : Composition) -> SymmetryQuality {
  let points = []
  for layer in composition.layers {
    for motif in layer.motifs {
      points.push(path_quality_center(motif.transformed_path(), 5))
    }
  }
  quality_symmetry_points(
    points,
    Rect::new(0.0, 0.0, composition.width, composition.height),
  )
}

///|
pub fn composition_quality_metrics(
  composition : Composition,
) -> DesignQualityMetrics {
  let canvas_area = (composition.width * composition.height).max(0.0)
  let bounds = quality_bounds_of_composition(composition)
  let occupied = (bounds.width * bounds.height).min(canvas_area)
  let entries = composition_quality_entries(composition)
  let mut edge_density = 0.0
  let mut contrast = 0.0
  for entry in entries {
    edge_density += entry.perimeter
    contrast += entry.contrast
  }
  let path_count = composition.layers.fold(init=0, fn(total, layer) {
    total + layer.motifs.length()
  })
  let (balance_x, balance_y) = quality_balance(composition)
  let normalized_edges = (edge_density / canvas_area.max(1.0).sqrt()).min(1.0)
  let normalized_contrast = if path_count == 0 {
    0.0
  } else {
    (contrast / path_count.to_double() / 7.0).min(1.0)
  }
  let coverage = if canvas_area == 0.0 { 0.0 } else { occupied / canvas_area }
  let score = dq_clamp(
    0.3 * coverage +
    0.2 * normalized_edges +
    0.2 * normalized_contrast +
    0.2 * quality_color_diversity(composition) +
    0.1 * (1.0 - balance_x.abs().min(1.0) - balance_y.abs().min(1.0)).max(0.0),
  )
  {
    canvas_area,
    occupied_area: occupied,
    coverage,
    path_count,
    layer_count: composition.layers.length(),
    total_length: quality_path_length(composition),
    edge_density: normalized_edges,
    color_diversity: quality_color_diversity(composition),
    contrast: normalized_contrast,
    balance_x,
    balance_y,
    score,
  }
}

///|
pub fn design_quality_report_text(metrics : DesignQualityMetrics) -> String {
  "canvas=" +
  metrics.canvas_area.to_string() +
  " occupied=" +
  metrics.occupied_area.to_string() +
  " coverage=" +
  metrics.coverage.to_string() +
  " paths=" +
  metrics.path_count.to_string() +
  " layers=" +
  metrics.layer_count.to_string() +
  " length=" +
  metrics.total_length.to_string() +
  " edges=" +
  metrics.edge_density.to_string() +
  " colors=" +
  metrics.color_diversity.to_string() +
  " contrast=" +
  metrics.contrast.to_string() +
  " balance=" +
  metrics.balance_x.to_string() +
  "," +
  metrics.balance_y.to_string() +
  " score=" +
  metrics.score.to_string()
}

///|
pub fn design_quality_report_csv(metrics : DesignQualityMetrics) -> String {
  "canvas_area,occupied_area,coverage,path_count,layer_count,total_length,edge_density,color_diversity,contrast,balance_x,balance_y,score\n" +
  metrics.canvas_area.to_string() +
  "," +
  metrics.occupied_area.to_string() +
  "," +
  metrics.coverage.to_string() +
  "," +
  metrics.path_count.to_string() +
  "," +
  metrics.layer_count.to_string() +
  "," +
  metrics.total_length.to_string() +
  "," +
  metrics.edge_density.to_string() +
  "," +
  metrics.color_diversity.to_string() +
  "," +
  metrics.contrast.to_string() +
  "," +
  metrics.balance_x.to_string() +
  "," +
  metrics.balance_y.to_string() +
  "," +
  metrics.score.to_string() +
  "\n"
}

///|
pub fn raster_quality_metrics(
  raster : Raster,
  threshold : Double,
) -> RasterQualityMetrics {
  let total = raster.pixels.length()
  let limit = threshold.max(0.0).min(1.0)
  let mut non_empty = 0
  let mut lum = 0.0
  let mut low = 1.0
  let mut high = 0.0
  for color in raster.pixels {
    if color.a > 0.001 && color.luminance() > limit {
      non_empty += 1
    }
    let value = color.luminance()
    lum += value
    low = low.min(value)
    high = high.max(value)
  }
  let edges = raster.sobel_edges()
  let mut edge_pixels = 0
  for color in edges.pixels {
    if color.r > 0.5 {
      edge_pixels += 1
    }
  }
  let edge_density = if total == 0 {
    0.0
  } else {
    edge_pixels.to_double() / total.to_double()
  }
  let regions = raster.connected_components(limit).length()
  let occupancy = if total == 0 {
    0.0
  } else {
    non_empty.to_double() / total.to_double()
  }
  let score = dq_clamp(
    0.35 * occupancy +
    0.25 * (high - low) +
    0.2 * (1.0 - edge_density).max(0.0) +
    0.2 * (regions.min(20).to_double() / 20.0),
  )
  {
    width: raster.width,
    height: raster.height,
    non_empty,
    occupancy,
    mean_luminance: if total == 0 {
      0.0
    } else {
      lum / total.to_double()
    },
    luminance_span: high - low,
    edge_pixels,
    edge_density,
    connected_regions: regions,
    score,
  }
}

///|
pub fn raster_quality_report_text(metrics : RasterQualityMetrics) -> String {
  "size=" +
  metrics.width.to_string() +
  "x" +
  metrics.height.to_string() +
  " non_empty=" +
  metrics.non_empty.to_string() +
  " occupancy=" +
  metrics.occupancy.to_string() +
  " mean_luminance=" +
  metrics.mean_luminance.to_string() +
  " span=" +
  metrics.luminance_span.to_string() +
  " edge_pixels=" +
  metrics.edge_pixels.to_string() +
  " edge_density=" +
  metrics.edge_density.to_string() +
  " regions=" +
  metrics.connected_regions.to_string() +
  " score=" +
  metrics.score.to_string()
}

///|
pub fn raster_quality_report_csv(metrics : RasterQualityMetrics) -> String {
  "width,height,non_empty,occupancy,mean_luminance,luminance_span,edge_pixels,edge_density,connected_regions,score\n" +
  metrics.width.to_string() +
  "," +
  metrics.height.to_string() +
  "," +
  metrics.non_empty.to_string() +
  "," +
  metrics.occupancy.to_string() +
  "," +
  metrics.mean_luminance.to_string() +
  "," +
  metrics.luminance_span.to_string() +
  "," +
  metrics.edge_pixels.to_string() +
  "," +
  metrics.edge_density.to_string() +
  "," +
  metrics.connected_regions.to_string() +
  "," +
  metrics.score.to_string() +
  "\n"
}

///|
pub fn quality_compare_documents(
  left : DesignDocument,
  right : DesignDocument,
) -> (Double, Double, Double) {
  let left_metrics = composition_quality_metrics(left.composition)
  let right_metrics = composition_quality_metrics(right.composition)
  (
    right_metrics.score - left_metrics.score,
    right_metrics.coverage - left_metrics.coverage,
    right_metrics.contrast - left_metrics.contrast,
  )
}

///|
pub fn quality_rank_compositions(
  compositions : Array[Composition],
) -> Array[(Int, Double)] {
  let result = []
  for index, composition in compositions {
    result.push((index, composition_quality_metrics(composition).score))
  }
  for i in 1.. 0 && result[position - 1].1 < value.1 {
      result[position] = result[position - 1]
      position -= 1
    }
    result[position] = value
  }
  result
}

///|
pub fn quality_threshold_pass(
  metrics : DesignQualityMetrics,
  minimum_score : Double,
  minimum_coverage : Double,
) -> Bool {
  metrics.score >= minimum_score && metrics.coverage >= minimum_coverage
}

///|
pub fn raster_threshold_pass(
  metrics : RasterQualityMetrics,
  minimum_score : Double,
  minimum_occupancy : Double,
) -> Bool {
  metrics.score >= minimum_score && metrics.occupancy >= minimum_occupancy
}