///|
/// Transformations for turning a theme document into stable variants without
/// losing descriptions or aliases.
pub enum ThemeTransform {
  Prefix(String)
  Rename(String, String)
  FilterKind(TokenKind)
  DropPrefix(String)
} derive(Debug, Eq)

///|
pub fn ThemeTransform::prefix(value : String) -> ThemeTransform {
  Prefix(value)
}

///|
pub fn ThemeTransform::rename(from : String, to : String) -> ThemeTransform {
  Rename(from, to)
}

///|
pub fn ThemeTransform::filter_kind(kind : TokenKind) -> ThemeTransform {
  FilterKind(kind)
}

///|
pub fn ThemeTransform::drop_prefix(value : String) -> ThemeTransform {
  DropPrefix(value)
}

///|
pub fn ThemeDocument::map_paths(
  self : ThemeDocument,
  transform : ThemeTransform,
) -> ThemeDocument {
  let tokens : Array[ThemeToken] = []
  for token in self.tokens {
    match transform_token(token, transform) {
      Some(mapped) => tokens.push(mapped)
      None => continue
    }
  }
  let aliases : Array[ThemeAlias] = []
  for link in self.aliases {
    let name = transform_path(link.name.canonical(), transform)
    let target = transform_path(link.target.canonical(), transform)
    match (name, target) {
      (Some(mapped_name), Some(mapped_target)) =>
        aliases.push({
          name: TokenPath::from_string(mapped_name),
          target: TokenPath::from_string(mapped_target),
        })
      _ => continue
    }
  }
  { tokens, aliases, provenance: self.provenance }.sorted()
}

///|
pub fn ThemeDocument::select_prefix(
  self : ThemeDocument,
  prefix : String,
) -> ThemeDocument {
  self.select_prefix_kind(prefix)
}

///|
pub fn ThemeDocument::select_prefix_kind(
  self : ThemeDocument,
  prefix : String,
) -> ThemeDocument {
  let wanted = TokenPath::from_string(prefix).canonical()
  let tokens = self.tokens.filter(fn(token) {
    token.path.canonical() == wanted ||
    token.path.canonical().has_prefix(wanted + ".")
  })
  { tokens, aliases: self.aliases, provenance: self.provenance }
}

///|
pub fn ThemeDocument::with_prefix(
  self : ThemeDocument,
  prefix : String,
) -> ThemeDocument {
  self.map_paths(ThemeTransform::prefix(prefix))
}

///|
pub fn ThemeDocument::without_prefix(
  self : ThemeDocument,
  prefix : String,
) -> ThemeDocument {
  self.map_paths(ThemeTransform::drop_prefix(prefix))
}

///|
pub fn ThemeDocument::only_kind(
  self : ThemeDocument,
  kind : TokenKind,
) -> ThemeDocument {
  self.map_paths(ThemeTransform::filter_kind(kind))
}

///|
pub fn ThemeDocument::rename_prefix(
  self : ThemeDocument,
  from : String,
  to : String,
) -> ThemeDocument {
  self.map_paths(ThemeTransform::rename(from, to))
}

///|
pub fn ThemeDocument::kind_counts(self : ThemeDocument) -> Array[Int] {
  let mut colors = 0
  let mut numbers = 0
  let mut texts = 0
  let mut booleans = 0
  for token in self.tokens {
    match token.kind {
      Color => colors += 1
      Number => numbers += 1
      Text => texts += 1
      Boolean => booleans += 1
    }
  }
  [colors, numbers, texts, booleans]
}

///|
pub fn ThemeDocument::kind_label_counts(self : ThemeDocument) -> Array[String] {
  let counts = self.kind_counts()
  [
    "color=" + counts[0].to_string(),
    "number=" + counts[1].to_string(),
    "text=" + counts[2].to_string(),
    "boolean=" + counts[3].to_string(),
  ]
}

///|
pub fn ThemeDocument::descriptions(self : ThemeDocument) -> Array[String] {
  self.tokens.map(fn(token) { token.description })
}

///|
pub fn ThemeDocument::undocumented_paths(self : ThemeDocument) -> Array[String] {
  self.tokens
  .filter(fn(token) { token.description.trim().to_owned() == "" })
  .map(fn(token) { token.path.canonical() })
}

///|
pub fn ThemeDocument::documentation_rate(self : ThemeDocument) -> Int {
  if self.tokens.length() == 0 {
    100
  } else {
    (self.tokens.length() - self.undocumented_paths().length()) *
    100 /
    self.tokens.length()
  }
}

///|
pub fn ThemeDocument::has_duplicate_paths(self : ThemeDocument) -> Bool {
  for left_index in 0.. String {
  let suffix = if self.tokens.length() == 0 { "" } else { "\n" }
  self.sorted().paths().join("\n") + suffix
}

///|
pub fn ThemeDocument::to_summary_markdown(self : ThemeDocument) -> String {
  "## Theme summary\n\n" +
  "Tokens: " +
  self.token_count().to_string() +
  "\n\n" +
  "Aliases: " +
  self.aliases.length().to_string() +
  "\n\n" +
  "Kinds: " +
  self.kind_label_counts().join(", ") +
  "\n\n" +
  "Documentation: " +
  self.documentation_rate().to_string() +
  "%"
}

///|
fn transform_token(
  token : ThemeToken,
  transform : ThemeTransform,
) -> ThemeToken? {
  match transform {
    FilterKind(kind) if token.kind != kind => return None
    _ => ()
  }
  match transform_path(token.path.canonical(), transform) {
    None => None
    Some(path) => Some({ ..token, path: TokenPath::from_string(path) })
  }
}

///|
fn transform_path(path : String, transform : ThemeTransform) -> String? {
  match transform {
    Prefix(prefix) =>
      Some(
        if prefix.trim().to_owned() == "" {
          path
        } else {
          prefix + "." + path
        },
      )
    Rename(from, to) => {
      let old = TokenPath::from_string(from).canonical()
      if path == old {
        Some(to)
      } else if path.has_prefix(old + ".") {
        Some(to + path[old.length():].to_owned())
      } else {
        Some(path)
      }
    }
    FilterKind(_) => Some(path)
    DropPrefix(prefix) => {
      let old = TokenPath::from_string(prefix).canonical()
      if path == old {
        None
      } else if path.has_prefix(old + ".") {
        Some(path[old.length() + 1:].to_owned())
      } else {
        Some(path)
      }
    }
  }
}