///|
pub struct Theme {
  name : String
  tokens : Array[Token]
} derive(Debug, Eq)

///|
pub fn Theme::new(name~ : String, tokens~ : Array[Token]) -> Theme {
  { name, tokens }
}

///|
pub fn Theme::name(self : Theme) -> String {
  self.name
}

///|
pub fn Theme::tokens(self : Theme) -> Array[Token] {
  self.tokens
}

///|
pub fn Theme::get(self : Theme, name : String) -> Token? {
  find_token(self.tokens, name)
}

///|
pub fn Theme::merge(self : Theme, overlay : Theme) -> Theme {
  { name: overlay.name, tokens: merge_tokens(self.tokens, overlay.tokens) }
}

///|
pub fn Theme::diff(self : Theme, other : Theme) -> Array[TokenChange] {
  diff_tokens(self.tokens, other.tokens)
}

///|
pub fn theme(name~ : String, tokens~ : Array[Token]) -> Theme {
  Theme::new(name~, tokens~)
}

///|
pub fn theme_family(
  base : Array[Token],
  light_overrides : Array[Token],
  dark_overrides : Array[Token],
) -> Array[Theme] {
  [
    Theme::new(name="light", tokens=merge_tokens(base, light_overrides)),
    Theme::new(name="dark", tokens=merge_tokens(base, dark_overrides)),
  ]
}

///|
pub fn theme_css(theme : Theme, selector : String) -> String {
  to_css_scoped(theme.tokens, selector)
}

///|
pub fn theme_switcher(themes : Array[Theme], attribute : String) -> String {
  let blocks : Array[String] = []
  for item in themes {
    blocks.push(to_css_scoped(item.tokens, "[\{attribute}=\"\{item.name}\"]"))
  }
  blocks.join("\n")
}

///|
pub struct ThemeReport {
  theme : String
  errors : Array[String]
  warnings : Array[String]
} derive(Debug, Eq)

///|
pub fn ThemeReport::ok(self : ThemeReport) -> Bool {
  self.errors.length() == 0
}

///|
pub fn ThemeReport::summary(self : ThemeReport) -> String {
  "theme=\{self.theme}, errors=\{self.errors.length()}, warnings=\{self.warnings.length()}"
}

///|
pub fn inspect_theme(item : Theme) -> ThemeReport {
  let errors : Array[String] = []
  let warnings : Array[String] = []
  for problem in validate_names(item.tokens) {
    warnings.push(problem)
  }
  for problem in validate_unique_names(item.tokens) {
    errors.push(problem)
  }
  for problem in validate_all(item.tokens) {
    errors.push(problem.message())
  }
  { theme: item.name, errors, warnings }
}

///|
pub fn validate_themes(themes : Array[Theme]) -> Array[ThemeReport] {
  [
    for item in themes => inspect_theme(item)
  ]
}

///|
pub fn theme_token_matrix(
  themes : Array[Theme],
  names : Array[String],
) -> Array[Array[String]] {
  [
    for name in names => {
      [
        for item in themes => {
          match item.get(name) {
            None => ""
            Some(token) => token.value
          }
        }
      ]
    }
  ]
}

///|
pub fn fallback_value(
  themes : Array[Theme],
  name : String,
  default : String,
) -> String {
  for item in themes {
    match item.get(name) {
      Some(token) => return token.value
      None => ()
    }
  }
  default
}

///|
pub fn resolve_alias(value : String, tokens : Array[Token]) -> String {
  resolve_alias_with_seen(value, tokens, [])
}

///|
pub fn resolve_aliases(tokens : Array[Token]) -> Array[Token] {
  [
    for token in tokens => {
      { ..token, value: resolve_alias(token.value, tokens) }
    }
  ]
}

///|
fn resolve_alias_with_seen(
  value : String,
  tokens : Array[Token],
  seen : Array[String],
) -> String {
  if value.has_prefix("{") && value.has_suffix("}") {
    let name = value[1:value.length() - 1].to_owned()
    if contains_string(seen, name) {
      value
    } else {
      match find_token(tokens, name) {
        None => value
        Some(token) =>
          resolve_alias_with_seen(token.value, tokens, [..seen, name])
      }
    }
  } else {
    value
  }
}

///|
pub fn unresolved_aliases(tokens : Array[Token]) -> Array[String] {
  let names = token_names(tokens)
  let result : Array[String] = []
  for token in tokens {
    if token.value.has_prefix("{") && token.value.has_suffix("}") {
      let wanted = token.value[1:token.value.length() - 1].to_owned()
      if !contains_string(names, wanted) {
        result.push("\{token.name} references missing token \{wanted}")
      }
    }
  }
  result
}

///|
pub fn alias_cycles(tokens : Array[Token]) -> Array[String] {
  let cycles : Array[String] = []
  for token in tokens {
    let seen : Array[String] = []
    let mut current = token
    while current.value.has_prefix("{") && current.value.has_suffix("}") {
      if contains_string(seen, current.name) {
        cycles.push("alias cycle at \{current.name}")
        break
      }
      seen.push(current.name)
      let wanted = current.value[1:current.value.length() - 1].to_owned()
      match find_token(tokens, wanted) {
        None => break
        Some(next) => current = next
      }
    }
  }
  unique_strings(cycles)
}

///|
fn unique_strings(values : Array[String]) -> Array[String] {
  let result : Array[String] = []
  for value in values {
    if !contains_string(result, value) {
      result.push(value)
    }
  }
  result
}