///|
/// What to run and how to print: the plugin list in execution order,
/// numeric precision, multipass and pretty printing. Start from
/// `Config::default()` and override fields with record update syntax.
pub(all) struct Config {
  /// names of plugins to run, in order; defaults to the preset
  plugins : Array[String]
  /// per-plugin JSON parameter objects, keyed by plugin name
  params : Map[String, Json]
  /// decimal places kept for numbers
  precision : Int
  /// repeat the pipeline until nothing changes (at most 10 passes)
  multipass : Bool
  /// indent the output
  pretty : Bool
} derive(Debug)

///|
/// The implemented subset of svgo's preset-default plugins, 3 decimal places,
/// multipass on, compact output.
pub fn Config::default() -> Config {
  {
    plugins: @plugins.preset_default.map(p => p.name),
    params: Map([]),
    precision: 3,
    multipass: true,
    pretty: false,
  }
}

///|
/// Outcome of `optimize`: the optimized document plus size statistics and
/// the list of plugins that changed something.
pub(all) struct Result {
  data : String
  /// UTF-8 byte sizes before and after
  original_size : Int
  size : Int
  passes : Int
  /// plugins that changed the document at least once
  applied : Array[String]
} derive(Debug)

///|
/// Bytes saved as a percentage of the original size (0 for an empty input).
pub fn Result::saved_percent(self : Result) -> Double {
  if self.original_size == 0 {
    0.0
  } else {
    (self.original_size - self.size).to_double() *
    100.0 /
    self.original_size.to_double()
  }
}

///|
/// Raised by `optimize` when `Config::plugins` names a plugin that does not exist.
pub suberror UnknownPlugin {
  UnknownPlugin(String)
} derive(Debug)

///|
pub impl Show for UnknownPlugin with fn output(self, logger) {
  match self {
    UnknownPlugin(n) => logger.write_string("unknown plugin '\{n}'")
  }
}

///|
/// Optimize an SVG document.
pub fn optimize(
  svg : String,
  config? : Config = Config::default(),
) -> Result raise {
  let doc = @xml.parse(svg)
  let plugins : Array[@plugins.Plugin] = []
  for name in config.plugins {
    match @plugins.find_plugin(name) {
      Some(p) => plugins.push(p)
      None => raise UnknownPlugin(name)
    }
  }
  let ctx = @plugins.Context::new(precision=config.precision)
  let applied : Set[String] = Set([])
  let mut passes = 0
  for ;; {
    passes += 1
    let mut changed = false
    for p in plugins {
      ctx.set_params(config.params.get(p.name).unwrap_or(Json::object(Map([]))))
      if (p.run)(doc, ctx) {
        changed = true
        applied.add(p.name)
      }
    }
    if !changed || !config.multipass || passes >= 10 {
      break
    }
  }
  let data = doc.to_string(options={ pretty: config.pretty, prolog: true, })
  {
    data,
    original_size: utf8_length(svg),
    size: utf8_length(data),
    passes,
    applied: applied.iter().to_array(),
  }
}

///|
/// Names of all available plugins with their descriptions.
pub fn list_plugins() -> Array[(String, String, Bool)] {
  let out = []
  for p in @plugins.preset_default {
    out.push((p.name, p.description, true))
  }
  for p in @plugins.optional_plugins {
    out.push((p.name, p.description, false))
  }
  out
}

///|
/// Number of bytes of the UTF-8 encoding of `s`, without encoding it.
pub fn utf8_length(s : String) -> Int {
  let mut n = 0
  for c in s.code_units() {
    let u = c.to_int()
    n += if u < 0x80 {
      1
    } else if u < 0x800 {
      2
    } else if u >= 0xD800 && u <= 0xDBFF {
      4 // high surrogate: the pair encodes to four bytes, low one adds nothing
    } else if u >= 0xDC00 && u <= 0xDFFF {
      0
    } else {
      3
    }
  }
  n
}