///|
pub struct ThemePolicy {
  require_descriptions : Bool
  require_unique_paths : Bool
  allowed_prefix : String
  min_color_tokens : Int
  reject_empty_values : Bool
} derive(Debug, Eq)

///|
pub fn ThemePolicy::default() -> ThemePolicy {
  {
    require_descriptions: false,
    require_unique_paths: true,
    allowed_prefix: "",
    min_color_tokens: 1,
    reject_empty_values: true,
  }
}

///|
pub fn ThemePolicy::strict() -> ThemePolicy {
  {
    require_descriptions: true,
    require_unique_paths: true,
    allowed_prefix: "",
    min_color_tokens: 1,
    reject_empty_values: true,
  }
}

///|
pub fn ThemePolicy::with_prefix(
  self : ThemePolicy,
  prefix : String,
) -> ThemePolicy {
  { ..self, allowed_prefix: TokenPath::from_string(prefix).canonical() }
}

///|
pub fn ThemeToken::is_empty(self : ThemeToken) -> Bool {
  match self.value {
    ColorValue(value) => value.trim().to_owned() == ""
    NumberValue(_) => false
    TextValue(value) => value.trim().to_owned() == ""
    BooleanValue(_) => false
  }
}

///|
pub fn ThemeToken::is_valid(self : ThemeToken) -> Bool {
  match self.value {
    ColorValue(value) => is_hex_color(value)
    NumberValue(_) => true
    TextValue(_) => true
    BooleanValue(_) => true
  }
}

///|
pub fn ThemeDocument::audit_theme(
  self : ThemeDocument,
  policy : ThemePolicy,
) -> AuditReport {
  let findings : Array[Finding] = []
  let seen : Array[String] = []
  let mut colors = 0
  for token in self.tokens {
    let path = token.path.canonical()
    if policy.require_unique_paths && seen.any(fn(item) { item == path }) {
      findings.push(Finding::{
        severity: Error,
        path,
        message: "duplicate theme path",
      })
    }
    seen.push(path)
    if policy.allowed_prefix != "" &&
      !token.path.canonical().has_prefix(policy.allowed_prefix) {
      findings.push(Finding::{
        severity: Error,
        path: token.path.canonical(),
        message: "token is outside the allowed prefix",
      })
    }
    if policy.require_descriptions && token.description.trim().to_owned() == "" {
      findings.push(Finding::{
        severity: Warning,
        path: token.path.canonical(),
        message: "token description is required by policy",
      })
    }
    if policy.reject_empty_values && token.is_empty() {
      findings.push(Finding::{
        severity: Error,
        path: token.path.canonical(),
        message: "token value must not be empty",
      })
    }
    if !token.is_valid() {
      findings.push(Finding::{
        severity: Error,
        path: token.path.canonical(),
        message: "token value does not match its declared kind",
      })
    }
    if token.kind == Color {
      colors += 1
    }
  }
  if colors < policy.min_color_tokens {
    findings.push(Finding::{
      severity: Warning,
      path: "tokens",
      message: "theme has fewer color tokens than policy requires",
    })
  }
  for link in self.aliases {
    if self.find(link.target.canonical()) is None {
      findings.push(Finding::{
        severity: Error,
        path: link.name.canonical(),
        message: "alias target is missing: " + link.target.canonical(),
      })
    }
  }
  AuditReport::from_findings(findings)
}

///|
pub fn ThemeDocument::color_tokens(self : ThemeDocument) -> Array[ThemeToken] {
  let result : Array[ThemeToken] = []
  for token in self.tokens {
    if token.kind == Color {
      result.push(token)
    }
  }
  result
}

///|
pub fn ThemeDocument::number_tokens(self : ThemeDocument) -> Array[ThemeToken] {
  let result : Array[ThemeToken] = []
  for token in self.tokens {
    if token.kind == Number {
      result.push(token)
    }
  }
  result
}

///|
pub fn ThemeDocument::paths(self : ThemeDocument) -> Array[String] {
  let result : Array[String] = []
  for token in self.tokens {
    result.push(token.path.canonical())
  }
  result
}

///|
pub fn ThemeDocument::description_rate(self : ThemeDocument) -> Int {
  if self.tokens.length() == 0 {
    0
  } else {
    let mut documented = 0
    for token in self.tokens {
      if token.description.trim().to_owned() != "" {
        documented += 1
      }
    }
    documented * 100 / self.tokens.length()
  }
}