///|
/// A stable identifier and export hint for a generated SVG asset.
pub(all) struct SvgAssetId {
  space : String
  name : String
  revision : Int
} derive(Eq, Debug, ToJson)

///|
pub(all) struct SvgAsset {
  id : SvgAssetId
  document : SvgDocument
  tags : Array[String]
  usage : String
  view_box : Rect
} derive(Debug)

///|
pub(all) struct SvgSpriteSymbol {
  id : String
  content : String
  view_box : Rect
  width : Double
  height : Double
} derive(Eq, Debug, ToJson)

///|
pub(all) struct SvgCssRule {
  selector : String
  declarations : Array[(String, String)]
} derive(Eq, Debug, ToJson)

///|
pub(all) struct SvgAssetBundle {
  name : String
  assets : Array[SvgAsset]
  symbols : Array[SvgSpriteSymbol]
  styles : Array[SvgCssRule]
  version : String
} derive(Debug)

///|
pub(all) struct SvgOptimizationReport {
  source_bytes : Int
  optimized_bytes : Int
  path_count : Int
  group_count : Int
  removed_empty_groups : Int
  compression_ratio : Double
} derive(Eq, Debug, ToJson)

///|
pub fn SvgAssetId::new(
  space : String,
  name : String,
  revision : Int,
) -> SvgAssetId {
  { space, name, revision: revision.max(0) }
}

///|
pub fn SvgAssetId::key(self : SvgAssetId) -> String {
  self.space + ":" + self.name + "@" + self.revision.to_string()
}

///|
pub fn SvgAssetId::file_name(self : SvgAssetId) -> String {
  self.space + "-" + self.name + "-v" + self.revision.to_string() + ".svg"
}

///|
pub fn SvgAsset::new(
  id : SvgAssetId,
  document : SvgDocument,
  usage : String,
) -> SvgAsset {
  {
    id,
    document,
    tags: [],
    usage,
    view_box: Rect::new(0.0, 0.0, document.width, document.height),
  }
}

///|
pub fn SvgAsset::tag(self : SvgAsset, value : String) -> SvgAsset {
  if value.length() > 0 && !self.tags.contains(value) {
    self.tags.push(value)
  }
  self
}

///|
pub fn SvgAsset::render(self : SvgAsset) -> String {
  self.document.to_svg_string()
}

///|
pub fn SvgAsset::bytes(self : SvgAsset) -> Int {
  self.render().length()
}

///|
pub fn SvgAsset::with_view_box(self : SvgAsset, view_box : Rect) -> SvgAsset {
  { ..self, view_box, }
}

///|
pub fn SvgSpriteSymbol::new(
  id : String,
  content : String,
  view_box : Rect,
) -> SvgSpriteSymbol {
  { id, content, view_box, width: view_box.width, height: view_box.height }
}

///|
pub fn SvgSpriteSymbol::sized(
  self : SvgSpriteSymbol,
  width : Double,
  height : Double,
) -> SvgSpriteSymbol {
  { ..self, width: width.max(0.0), height: height.max(0.0) }
}

///|
pub fn SvgCssRule::new(selector : String) -> SvgCssRule {
  { selector, declarations: [] }
}

///|
pub fn SvgCssRule::set(
  self : SvgCssRule,
  property : String,
  value : String,
) -> SvgCssRule {
  let mut replaced = false
  for index, declaration in self.declarations {
    if declaration.0 == property {
      self.declarations[index] = (property, value)
      replaced = true
      break
    }
  }
  if !replaced {
    self.declarations.push((property, value))
  }
  self
}

///|
pub fn SvgCssRule::to_css(self : SvgCssRule) -> String {
  let builder = StringBuilder()
  builder.write_string(self.selector + " {")
  for declaration in self.declarations {
    builder.write_string(declaration.0 + ":" + declaration.1 + ";")
  }
  builder.write_string("}")
  builder.to_string()
}

///|
pub fn SvgAssetBundle::new(name : String, version : String) -> SvgAssetBundle {
  { name, assets: [], symbols: [], styles: [], version }
}

///|
pub fn SvgAssetBundle::add(self : SvgAssetBundle, asset : SvgAsset) -> Bool {
  for existing in self.assets {
    if existing.id.key() == asset.id.key() {
      return false
    }
  }
  self.assets.push(asset)
  true
}

///|
pub fn SvgAssetBundle::add_symbol(
  self : SvgAssetBundle,
  symbol : SvgSpriteSymbol,
) -> Bool {
  for existing in self.symbols {
    if existing.id == symbol.id {
      return false
    }
  }
  self.symbols.push(symbol)
  true
}

///|
pub fn SvgAssetBundle::add_style(
  self : SvgAssetBundle,
  rule : SvgCssRule,
) -> Unit {
  self.styles.push(rule)
}

///|
pub fn SvgAssetBundle::asset(self : SvgAssetBundle, key : String) -> SvgAsset? {
  for asset in self.assets {
    if asset.id.key() == key {
      return Some(asset)
    }
  }
  None
}

///|
pub fn SvgAssetBundle::tagged(
  self : SvgAssetBundle,
  tag : String,
) -> Array[SvgAsset] {
  let result = []
  for asset in self.assets {
    if asset.tags.contains(tag) {
      result.push(asset)
    }
  }
  result
}

///|
pub fn SvgAssetBundle::manifest(self : SvgAssetBundle) -> String {
  let builder = StringBuilder()
  builder.write_string("name,version,key,file,usage,width,height,tags,bytes\n")
  for asset in self.assets {
    builder.write_string(
      self.name +
      "," +
      self.version +
      "," +
      asset.id.key() +
      "," +
      asset.id.file_name() +
      "," +
      asset.usage +
      "," +
      asset.view_box.width.to_string() +
      "," +
      asset.view_box.height.to_string() +
      "," +
      asset.tags.join("|") +
      "," +
      asset.bytes().to_string() +
      "\n",
    )
  }
  builder.to_string()
}

///|
pub fn SvgAssetBundle::render_sprite(self : SvgAssetBundle) -> String {
  let builder = StringBuilder()
  builder.write_string(
    "",
  )
  for symbol in self.symbols {
    builder.write_string(
      "",
    )
    builder.write_string(symbol.content)
    builder.write_string("")
  }
  builder.write_string("")
  builder.to_string()
}

///|
pub fn SvgAssetBundle::render_styles(self : SvgAssetBundle) -> String {
  let builder = StringBuilder()
  builder.write_string("")
  builder.to_string()
}

///|
pub fn SvgAssetBundle::size(self : SvgAssetBundle) -> Int {
  self.assets.length()
}

///|
pub fn SvgAssetBundle::symbol_size(self : SvgAssetBundle) -> Int {
  self.symbols.length()
}

///|
pub fn svg_asset_from_document(
  space : String,
  name : String,
  revision : Int,
  document : SvgDocument,
  usage : String,
) -> SvgAsset {
  SvgAsset::new(SvgAssetId::new(space, name, revision), document, usage)
}

///|
pub fn svg_symbol_from_document(
  id : String,
  document : SvgDocument,
) -> SvgSpriteSymbol {
  let content = document.to_svg_string()
  SvgSpriteSymbol::new(
    id,
    content,
    Rect::new(0.0, 0.0, document.width, document.height),
  )
}

///|
pub fn svg_use_symbol(
  id : String,
  x : Double,
  y : Double,
  width : Double,
  height : Double,
) -> String {
  ""
}

///|
pub fn svg_asset_link(
  asset : SvgAsset,
  x : Double,
  y : Double,
  width : Double,
  height : Double,
) -> String {
  "" +
  asset.document.to_svg_string() +
  ""
}

///|
fn svg_count_elements(element : SvgElement) -> (Int, Int) {
  match element {
    SvgPath(_, _) => (1, 0)
    SvgGroup(children, ..) => {
      let mut paths = 0
      let mut groups = 1
      for child in children {
        let (child_paths, child_groups) = svg_count_elements(child)
        paths += child_paths
        groups += child_groups
      }
      (paths, groups)
    }
  }
}

///|
pub fn svg_document_element_counts(document : SvgDocument) -> (Int, Int) {
  let mut paths = 0
  let mut groups = 0
  for element in document.elements {
    let (element_paths, element_groups) = svg_count_elements(element)
    paths += element_paths
    groups += element_groups
  }
  (paths, groups)
}

///|
pub fn svg_document_bounds(document : SvgDocument) -> Rect {
  Rect::new(0.0, 0.0, document.width, document.height)
}

///|
pub fn svg_document_has_transparency(document : SvgDocument) -> Bool {
  for element in document.elements {
    match element {
      SvgPath(_, style) => if style.opacity < 0.999 { return true }
      SvgGroup(children, ..) =>
        for child in children {
          match child {
            SvgPath(_, style) => if style.opacity < 0.999 { return true }
            _ => ()
          }
        }
    }
  }
  false
}

///|
pub fn svg_document_to_data_uri(document : SvgDocument) -> String {
  "data:image/svg+xml," + document.to_svg_string()
}

///|
pub fn svg_document_to_download_name(
  prefix : String,
  width : Double,
  height : Double,
) -> String {
  prefix +
  "-" +
  width.to_int().to_string() +
  "x" +
  height.to_int().to_string() +
  ".svg"
}

///|
pub fn svg_style_for_color(
  color : Color,
  stroke : Bool,
  width : Double,
) -> SvgStyle {
  if stroke {
    SvgStyle::new(
      fill="none",
      stroke=color.to_hex(),
      stroke_width=width.max(0.0),
    )
  } else {
    SvgStyle::new(fill=color.to_hex(), stroke="none", stroke_width=0.0)
  }
}

///|
pub fn svg_style_opacity(style : SvgStyle, opacity : Double) -> SvgStyle {
  { ..style, opacity: opacity.max(0.0).min(1.0) }
}

///|
pub fn svg_style_scaled_stroke(style : SvgStyle, scale : Double) -> SvgStyle {
  { ..style, stroke_width: style.stroke_width * scale.max(0.0) }
}

///|
pub fn svg_group_with_transform(
  children : Array[SvgElement],
  transform : Matrix2D,
) -> SvgElement {
  SvgElement::SvgGroup(children, transform~)
}

///|
pub fn svg_document_group(
  document : SvgDocument,
  transform : Matrix2D,
) -> SvgDocument {
  let result = SvgDocument::new(document.width, document.height)
  result.add(SvgElement::SvgGroup(document.elements, transform~))
  result
}

///|
pub fn svg_document_tile(
  document : SvgDocument,
  columns : Int,
  rows : Int,
  gap : Double,
) -> SvgDocument {
  let result = SvgDocument::new(
    document.width * columns.max(0).to_double() +
    gap.max(0.0) * (columns - 1).max(0).to_double(),
    document.height * rows.max(0).to_double() +
    gap.max(0.0) * (rows - 1).max(0).to_double(),
  )
  if columns > 0 && rows > 0 {
    for row in 0.. SvgDocument {
  let factor = scale.max(0.0)
  let result = SvgDocument::new(
    document.width * factor,
    document.height * factor,
  )
  result.add(
    SvgElement::SvgGroup(
      document.elements,
      transform=Matrix2D::scale(factor, factor),
    ),
  )
  result
}

///|
pub fn svg_document_crop(document : SvgDocument, bounds : Rect) -> SvgDocument {
  let result = SvgDocument::new(bounds.width, bounds.height)
  result.add(
    SvgElement::SvgGroup(
      document.elements,
      transform=Matrix2D::translate(-bounds.x, -bounds.y),
    ),
  )
  result
}

///|
pub fn svg_document_optimize(
  document : SvgDocument,
) -> (SvgDocument, SvgOptimizationReport) {
  let source = document.to_svg_string()
  let (paths, groups) = svg_document_element_counts(document)
  let mut empty = 0
  let elements = []
  for element in document.elements {
    match element {
      SvgGroup(children, transform~) =>
        if children.length() == 0 {
          empty += 1
        } else {
          elements.push(SvgElement::SvgGroup(children, transform~))
        }
      SvgPath(path, style) => elements.push(SvgElement::SvgPath(path, style))
    }
  }
  let result = { ..document, elements, }
  let optimized = result.to_svg_string()
  let ratio = if source.length() == 0 {
    1.0
  } else {
    optimized.length().to_double() / source.length().to_double()
  }
  (
    result,
    {
      source_bytes: source.length(),
      optimized_bytes: optimized.length(),
      path_count: paths,
      group_count: groups,
      removed_empty_groups: empty,
      compression_ratio: ratio,
    },
  )
}

///|
pub fn svg_optimization_report_text(report : SvgOptimizationReport) -> String {
  "source=" +
  report.source_bytes.to_string() +
  " optimized=" +
  report.optimized_bytes.to_string() +
  " paths=" +
  report.path_count.to_string() +
  " groups=" +
  report.group_count.to_string() +
  " empty=" +
  report.removed_empty_groups.to_string() +
  " ratio=" +
  report.compression_ratio.to_string()
}

///|
pub fn svg_document_checksum(document : SvgDocument) -> UInt {
  let text = document.to_svg_string()
  let mut hash : UInt = 2166136261
  for index in 0.. UInt {
  svg_document_checksum(asset.document)
}

///|
pub fn svg_bundle_checksum(bundle : SvgAssetBundle) -> UInt {
  let mut hash : UInt = 2166136261
  for asset in bundle.assets {
    hash = (hash ^ svg_asset_checksum(asset)) * 16777619
  }
  hash
}

///|
pub fn svg_document_with_metadata(
  document : SvgDocument,
  metadata : DocumentMetadata,
) -> String {
  metadata.to_comment() + document.to_svg_string()
}

///|
pub fn svg_asset_index(bundle : SvgAssetBundle) -> Array[String] {
  let result = []
  for asset in bundle.assets {
    result.push(asset.id.key())
  }
  result
}

///|
pub fn svg_bundle_validate(bundle : SvgAssetBundle) -> Array[String] {
  let errors = []
  if bundle.name.length() == 0 {
    errors.push("bundle name is empty")
  }
  if bundle.version.length() == 0 {
    errors.push("bundle version is empty")
  }
  for asset in bundle.assets {
    if asset.id.name.length() == 0 {
      errors.push("asset name is empty")
    }
    if asset.document.width <= 0.0 || asset.document.height <= 0.0 {
      errors.push("asset dimensions must be positive")
    }
  }
  for symbol in bundle.symbols {
    if symbol.id.length() == 0 {
      errors.push("symbol id is empty")
    }
    if symbol.view_box.width <= 0.0 || symbol.view_box.height <= 0.0 {
      errors.push("symbol view box must be positive")
    }
  }
  errors
}

///|
pub fn svg_bundle_report(bundle : SvgAssetBundle) -> String {
  let builder = StringBuilder()
  builder.write_string(
    "bundle=" +
    bundle.name +
    " version=" +
    bundle.version +
    " assets=" +
    bundle.assets.length().to_string() +
    " symbols=" +
    bundle.symbols.length().to_string() +
    " styles=" +
    bundle.styles.length().to_string() +
    " checksum=" +
    svg_bundle_checksum(bundle).to_string() +
    "\n",
  )
  for asset in bundle.assets {
    let (paths, groups) = svg_document_element_counts(asset.document)
    builder.write_string(
      "- " +
      asset.id.key() +
      " paths=" +
      paths.to_string() +
      " groups=" +
      groups.to_string() +
      " bytes=" +
      asset.bytes().to_string() +
      " tags=" +
      asset.tags.join("|") +
      "\n",
    )
  }
  builder.to_string()
}

///|
pub fn svg_make_icon_asset(
  name : String,
  path : Path,
  color : Color,
  size : Double,
) -> SvgAsset {
  let document = SvgDocument::new(size, size)
  document.add(
    SvgElement::SvgPath(path, svg_style_for_color(color, false, 0.0)),
  )
  svg_asset_from_document("icon", name, 1, document, "interface icon")
}

///|
pub fn svg_make_pattern_asset(
  name : String,
  composition : Composition,
  usage : String,
) -> SvgAsset {
  svg_asset_from_document("pattern", name, 1, composition.render(), usage)
}

///|
pub fn svg_make_palette_styles(
  palette : Palette,
  prefix : String,
) -> SvgCssRule {
  let rule = SvgCssRule::new(":root")
  let mut result = rule
  for index, color in palette.colors {
    result = result.set("--" + prefix + "-" + index.to_string(), color.to_hex())
  }
  result
}

///|
pub fn svg_bundle_from_documents(
  name : String,
  documents : Array[(String, SvgDocument)],
) -> SvgAssetBundle {
  let bundle = SvgAssetBundle::new(name, "1.0.0")
  for item in documents {
    let asset = svg_asset_from_document(
      "generated",
      item.0,
      1,
      item.1,
      "generated artwork",
    )
    let _ = bundle.add(asset)
  }
  bundle
}

///|
pub fn svg_bundle_scale_assets(
  bundle : SvgAssetBundle,
  scale : Double,
) -> SvgAssetBundle {
  let result = SvgAssetBundle::new(bundle.name, bundle.version)
  for asset in bundle.assets {
    let scaled = {
      ..asset,
      document: svg_document_scale(asset.document, scale),
      view_box: Rect::new(
        0.0,
        0.0,
        asset.view_box.width * scale.max(0.0),
        asset.view_box.height * scale.max(0.0),
      ),
    }
    let _ = result.add(scaled)
  }
  for symbol in bundle.symbols {
    let _ = result.add_symbol(
      symbol.sized(symbol.width * scale, symbol.height * scale),
    )
  }
  for rule in bundle.styles {
    result.add_style(rule)
  }
  result
}

///|
pub fn svg_bundle_crop_assets(
  bundle : SvgAssetBundle,
  bounds : Rect,
) -> SvgAssetBundle {
  let result = SvgAssetBundle::new(bundle.name, bundle.version)
  for asset in bundle.assets {
    let cropped = {
      ..asset,
      document: svg_document_crop(asset.document, bounds),
      view_box: Rect::new(0.0, 0.0, bounds.width, bounds.height),
    }
    let _ = result.add(cropped)
  }
  result
}

///|
pub fn svg_symbol_grid(
  symbols : Array[SvgSpriteSymbol],
  columns : Int,
  gap : Double,
) -> SvgDocument {
  let safe_columns = columns.max(1)
  let cell_width = symbols.fold(init=0.0, fn(best, symbol) {
    best.max(symbol.width)
  })
  let cell_height = symbols.fold(init=0.0, fn(best, symbol) {
    best.max(symbol.height)
  })
  let rows = if symbols.length() == 0 {
    0
  } else {
    (symbols.length() + safe_columns - 1) / safe_columns
  }
  let document = SvgDocument::new(
    safe_columns.to_double() * (cell_width + gap.max(0.0)),
    rows.to_double() * (cell_height + gap.max(0.0)),
  )
  for index, symbol in symbols {
    let x = (index % safe_columns).to_double() * (cell_width + gap.max(0.0))
    let y = (index / safe_columns).to_double() * (cell_height + gap.max(0.0))
    let path = Path::rect(symbol.width, symbol.height).transform(
      Matrix2D::translate(x, y),
    )
    document.add(
      SvgElement::SvgPath(
        path,
        SvgStyle::new(fill="none", stroke="#cccccc", stroke_width=0.5),
      ),
    )
  }
  document
}

///|
pub fn svg_document_add_border(
  document : SvgDocument,
  color : Color,
  width : Double,
) -> SvgDocument {
  let result = { ..document, elements: document.elements.copy() }
  result.add(
    SvgElement::SvgPath(
      Path::rect(document.width, document.height),
      svg_style_for_color(color, true, width),
    ),
  )
  result
}

///|
pub fn svg_document_background(
  document : SvgDocument,
  color : Color,
) -> SvgDocument {
  let result = SvgDocument::new(document.width, document.height)
  result.add(
    SvgElement::SvgPath(
      Path::rect(document.width, document.height),
      svg_style_for_color(color, false, 0.0),
    ),
  )
  for element in document.elements {
    result.add(element)
  }
  result
}

///|
pub fn svg_asset_with_tags(asset : SvgAsset, tags : Array[String]) -> SvgAsset {
  let result = { ..asset, tags: [] }
  for tag in tags {
    let _ = result.tag(tag)
  }
  result
}

///|
pub fn svg_bundle_tag_index(bundle : SvgAssetBundle) -> String {
  let builder = StringBuilder()
  let tags = []
  for asset in bundle.assets {
    for tag in asset.tags {
      if !tags.contains(tag) {
        tags.push(tag)
      }
    }
  }
  for tag in tags {
    builder.write_string(tag + ":")
    for asset in bundle.assets {
      if asset.tags.contains(tag) {
        builder.write_string(asset.id.key() + ",")
      }
    }
    builder.write_string("\n")
  }
  builder.to_string()
}

///|
pub fn svg_bundle_size_bytes(bundle : SvgAssetBundle) -> Int {
  bundle.assets.fold(init=0, fn(total, asset) { total + asset.bytes() })
}

///|
pub fn svg_bundle_optimize(
  bundle : SvgAssetBundle,
) -> (SvgAssetBundle, Array[SvgOptimizationReport]) {
  let result = SvgAssetBundle::new(bundle.name, bundle.version)
  let reports = []
  for asset in bundle.assets {
    let (optimized, report) = svg_document_optimize(asset.document)
    let _ = result.add({ ..asset, document: optimized })
    reports.push(report)
  }
  for symbol in bundle.symbols {
    let _ = result.add_symbol(symbol)
  }
  for rule in bundle.styles {
    result.add_style(rule)
  }
  (result, reports)
}

///|
pub fn svg_bundle_manifest_json_like(bundle : SvgAssetBundle) -> String {
  let builder = StringBuilder()
  builder.write_string("{")
  builder.write_string(
    "\"name\":\"" +
    escape_svg(bundle.name) +
    "\",\"version\":\"" +
    escape_svg(bundle.version) +
    "\",\"assets\":[",
  )
  for index, asset in bundle.assets {
    if index > 0 {
      builder.write_string(",")
    }
    builder.write_string("{")
    builder.write_string(
      "\"key\":\"" +
      escape_svg(asset.id.key()) +
      "\",\"file\":\"" +
      escape_svg(asset.id.file_name()) +
      "\",\"bytes\":" +
      asset.bytes().to_string(),
    )
    builder.write_string("}")
  }
  builder.write_string("]}")
  builder.to_string()
}

///|
pub fn svg_text_label(
  text : String,
  position : Vec2,
  size : Double,
  color : Color,
) -> String {
  "" +
  escape_svg(text) +
  ""
}

///|
pub fn svg_guides(
  document : SvgDocument,
  spacing : Double,
  color : Color,
) -> SvgDocument {
  let result = { ..document, elements: document.elements.copy() }
  let step = spacing.max(1.0)
  let style = svg_style_for_color(color, true, 0.25)
  let mut x = 0.0
  while x <= document.width {
    result.add(
      SvgElement::SvgPath(
        Path::new().move_to(x, 0.0).line_to(x, document.height),
        style,
      ),
    )
    x += step
  }
  let mut y = 0.0
  while y <= document.height {
    result.add(
      SvgElement::SvgPath(
        Path::new().move_to(0.0, y).line_to(document.width, y),
        style,
      ),
    )
    y += step
  }
  result
}