///|
/// Named motif recipes used by interactive previews and repeatable exports.
pub(all) struct PatternRecipeSpec {
  name : String
  family : String
  bounds : Rect
  density : Int
  seed : UInt
  palette : Palette
} derive(Debug)

///|
pub(all) struct PatternRecipeResult {
  spec : PatternRecipeSpec
  batch : PatternBatch
  layer : PatternLayer
  document : SvgDocument
  quality : DesignQualityMetrics
} derive(Debug)

///|
pub fn PatternRecipeSpec::new(
  name : String,
  family : String,
  bounds : Rect,
  density : Int,
  seed : UInt,
  palette : Palette,
) -> PatternRecipeSpec {
  { name, family, bounds, density: density.max(1), seed, palette }
}

///|
pub fn PatternRecipeSpec::with_density(
  self : PatternRecipeSpec,
  density : Int,
) -> PatternRecipeSpec {
  { ..self, density: density.max(1) }
}

///|
pub fn pattern_recipe_batch(spec : PatternRecipeSpec) -> PatternBatch {
  let center = spec.bounds.center()
  match spec.family {
    "dots" =>
      catalog_dot_field(
        spec.bounds,
        spec.density,
        spec.density,
        2.0,
        12.0,
        spec.palette,
        spec.seed,
      )
    "sunburst" =>
      catalog_sunburst(
        center,
        spec.bounds.width.min(spec.bounds.height) / 2.0,
        spec.density * 2,
        4.0,
        spec.palette,
        spec.seed,
      )
    "moire" =>
      catalog_moire(
        center,
        spec.bounds.width.min(spec.bounds.height) / 2.0,
        spec.density * 2,
        0.2,
        8.0,
        tx_color_at(spec.palette, 0),
        spec.seed,
      )
    "frames" =>
      catalog_nested_frames(
        spec.bounds,
        spec.density,
        spec.bounds.width.min(spec.bounds.height) /
        (spec.density * 3).to_double(),
        spec.palette,
        spec.seed,
      )
    "isometric" =>
      catalog_isometric(
        spec.bounds,
        spec.density,
        spec.density,
        spec.bounds.width / spec.density.to_double(),
        spec.bounds.height / spec.density.to_double(),
        spec.palette,
        spec.seed,
      )
    "orbits" =>
      catalog_orbits(
        center,
        spec.density,
        spec.density.min(12),
        10.0,
        12.0,
        spec.palette,
        spec.seed,
      )
    "spiral" =>
      catalog_spiral_field(
        center,
        spec.density * 4,
        4.0,
        2.0,
        spec.bounds.width.min(spec.bounds.height) / 2.0,
        spec.palette,
        spec.seed,
      )
    "barcode" =>
      catalog_barcode(
        spec.bounds,
        spec.density * 2,
        2.0,
        spec.bounds.width / spec.density.to_double(),
        spec.palette,
        spec.seed,
      )
    "checker" =>
      catalog_round_checker(
        spec.bounds,
        spec.density,
        spec.density,
        3.0,
        tx_color_at(spec.palette, 0),
        tx_color_at(spec.palette, 1),
        spec.seed,
      )
    "cross" =>
      catalog_cross_weave(
        spec.bounds,
        spec.density,
        spec.density,
        spec.bounds.width / spec.density.to_double() * 0.6,
        spec.palette,
        spec.seed,
      )
    _ =>
      catalog_flow_segments(
        spec.bounds,
        spec.density,
        spec.density,
        12.0,
        ValueNoise::new(0.02, spec.seed),
        spec.palette,
        spec.seed,
      )
  }
}

///|
pub fn pattern_recipe_result(spec : PatternRecipeSpec) -> PatternRecipeResult {
  let batch = pattern_recipe_batch(spec)
  let layer = batch.to_layer(1.0)
  let composition = Composition::new(
    spec.bounds.width,
    spec.bounds.height,
    background=tx_color_at(spec.palette, 0),
  )
  composition.add_layer(layer)
  let document = composition.render()
  {
    spec,
    batch,
    layer,
    document,
    quality: composition_quality_metrics(composition),
  }
}

///|
pub fn pattern_recipe_asset(result : PatternRecipeResult) -> SvgAsset {
  svg_asset_from_document(
    "recipe",
    result.spec.name,
    1,
    result.document,
    result.spec.family,
  )
}

///|
pub fn pattern_recipe_report(result : PatternRecipeResult) -> String {
  "name=" +
  result.spec.name +
  " family=" +
  result.spec.family +
  " motifs=" +
  result.batch.count().to_string() +
  " quality=" +
  result.quality.score.to_string() +
  " checksum=" +
  svg_document_checksum(result.document).to_string()
}

///|
pub fn pattern_recipe_manifest(results : Array[PatternRecipeResult]) -> String {
  let builder = StringBuilder()
  builder.write_string("name,family,motifs,quality,coverage,checksum\n")
  for result in results {
    builder.write_string(
      result.spec.name +
      "," +
      result.spec.family +
      "," +
      result.batch.count().to_string() +
      "," +
      result.quality.score.to_string() +
      "," +
      result.quality.coverage.to_string() +
      "," +
      svg_document_checksum(result.document).to_string() +
      "\n",
    )
  }
  builder.to_string()
}

///|
pub fn pattern_recipe_grid(
  bounds : Rect,
  names : Array[String],
  density : Int,
  palette : Palette,
  seed : UInt,
) -> Array[PatternRecipeResult] {
  let result = []
  let columns = names.length().max(1).min(4)
  let rows = if names.length() == 0 {
    0
  } else {
    (names.length() + columns - 1) / columns
  }
  let cell_width = bounds.width / columns.to_double()
  let cell_height = if rows == 0 {
    0.0
  } else {
    bounds.height / rows.to_double()
  }
  for index, name in names {
    let cell = Rect::new(
      bounds.x + (index % columns).to_double() * cell_width,
      bounds.y + (index / columns).to_double() * cell_height,
      cell_width,
      cell_height,
    )
    result.push(
      pattern_recipe_result(
        PatternRecipeSpec::new(
          name,
          name,
          cell,
          density,
          seed + index.reinterpret_as_uint(),
          palette,
        ),
      ),
    )
  }
  result
}

///|
pub fn pattern_recipe_bundle(
  results : Array[PatternRecipeResult],
) -> SvgAssetBundle {
  let bundle = SvgAssetBundle::new("pattern-recipes", "1.0.0")
  for result in results {
    let _ = bundle.add(pattern_recipe_asset(result))
  }
  bundle
}

///|
pub fn pattern_recipe_layer(
  results : Array[PatternRecipeResult],
  opacity : Double,
) -> PatternLayer {
  let layer = PatternLayer::new("recipe-stack", opacity~)
  for result in results {
    for motif in result.layer.motifs {
      layer.add(motif)
    }
  }
  layer
}

///|
pub fn pattern_recipe_document(
  results : Array[PatternRecipeResult],
  width : Double,
  height : Double,
  background : Color,
) -> DesignDocument {
  let composition = Composition::new(width, height, background~)
  composition.add_layer(pattern_recipe_layer(results, 1.0))
  DesignDocument::new(
    composition,
    DocumentMetadata::new(
      "Pattern Recipe Sheet", "MoonBit Generative Patterns", 0, "recipe preview",
    ).tag("recipes"),
  )
}

///|
pub fn pattern_recipe_rank(
  results : Array[PatternRecipeResult],
) -> Array[(String, Double)] {
  let ranked = []
  for result in results {
    ranked.push((result.spec.name, result.quality.score))
  }
  for i in 1.. 0 && ranked[position - 1].1 < value.1 {
      ranked[position] = ranked[position - 1]
      position -= 1
    }
    ranked[position] = value
  }
  ranked
}

///|
pub fn pattern_recipe_filter(
  results : Array[PatternRecipeResult],
  minimum_score : Double,
) -> Array[PatternRecipeResult] {
  let result = []
  for item in results {
    if item.quality.score >= minimum_score {
      result.push(item)
    }
  }
  result
}

///|
pub fn pattern_recipe_translate(
  result : PatternRecipeResult,
  offset : Vec2,
) -> PatternRecipeResult {
  let batch = result.batch.translate(offset)
  let layer = batch.to_layer(1.0)
  let composition = Composition::new(
    result.spec.bounds.width,
    result.spec.bounds.height,
    background=tx_color_at(result.spec.palette, 0),
  )
  composition.add_layer(layer)
  let document = composition.render()
  {
    ..result,
    batch,
    layer,
    document,
    quality: composition_quality_metrics(composition),
  }
}

///|
pub fn pattern_recipe_scale(
  result : PatternRecipeResult,
  factor : Double,
) -> PatternRecipeResult {
  let batch = result.batch.translate(
    result.spec.bounds.center().scale(1.0 - factor.max(0.0)),
  )
  let layer = batch.to_layer(factor.max(0.0))
  let composition = Composition::new(
    result.spec.bounds.width,
    result.spec.bounds.height,
    background=tx_color_at(result.spec.palette, 0),
  )
  composition.add_layer(layer)
  let document = composition.render()
  {
    ..result,
    batch,
    layer,
    document,
    quality: composition_quality_metrics(composition),
  }
}

///|
pub fn pattern_recipe_validate(result : PatternRecipeResult) -> Array[String] {
  let errors = []
  if result.spec.name.length() == 0 {
    errors.push("recipe name is empty")
  }
  if result.batch.count() == 0 {
    errors.push("recipe has no motifs")
  }
  if result.document.width <= 0.0 || result.document.height <= 0.0 {
    errors.push("recipe document dimensions invalid")
  }
  errors
}

///|
pub fn pattern_recipe_thumbnail(
  result : PatternRecipeResult,
  size : Int,
) -> Raster {
  let raster = Raster::new(size, size, fill=tx_color_at(result.spec.palette, 0))
  raster_plus_draw_motifs(raster, result.layer.motifs)
}

///|
pub fn pattern_recipe_thumbnail_quality(
  result : PatternRecipeResult,
  size : Int,
) -> RasterQualityMetrics {
  raster_quality_metrics(pattern_recipe_thumbnail(result, size), 0.02)
}

///|
pub fn pattern_recipe_families() -> Array[String] {
  [
    "dots", "sunburst", "moire", "frames", "isometric", "orbits", "spiral", "barcode",
    "checker", "cross", "flow",
  ]
}

///|
pub fn pattern_recipe_default_suite(
  bounds : Rect,
  density : Int,
  palette : Palette,
  seed : UInt,
) -> Array[PatternRecipeResult] {
  pattern_recipe_grid(bounds, pattern_recipe_families(), density, palette, seed)
}

///|
pub fn pattern_recipe_suite_report(
  bounds : Rect,
  density : Int,
  palette : Palette,
  seed : UInt,
) -> String {
  let results = pattern_recipe_default_suite(bounds, density, palette, seed)
  pattern_recipe_manifest(results)
}

///|
pub fn pattern_recipe_export_plan(
  bounds : Rect,
  density : Int,
  palette : Palette,
  seed : UInt,
) -> ExportPlan {
  let _ = density
  let job = export_job_standard("recipe-suite", seed, palette)
  job.add_variant("sheet", bounds.width, bounds.height)
  export_plan(job)
}

///|
pub fn pattern_recipe_document_svg(
  bounds : Rect,
  density : Int,
  palette : Palette,
  seed : UInt,
) -> String {
  let results = pattern_recipe_default_suite(bounds, density, palette, seed)
  pattern_recipe_document(
    results,
    bounds.width,
    bounds.height,
    tx_color_at(palette, 0),
  ).to_svg()
}