///|
/// Policy used when checking a type scale for readable production output.
pub struct TypographyPolicy {
  min_size_px : Int
  max_size_px : Int
  min_line_height_px : Int
  min_line_ratio_x100 : Int
  max_line_ratio_x100 : Int
  require_fallback : Bool
  require_monotonic_scale : Bool
} derive(Debug, Eq)

///|
pub fn TypographyPolicy::default() -> TypographyPolicy {
  {
    min_size_px: 10,
    max_size_px: 160,
    min_line_height_px: 12,
    min_line_ratio_x100: 110,
    max_line_ratio_x100: 220,
    require_fallback: true,
    require_monotonic_scale: true,
  }
}

///|
pub fn TypographyPolicy::strict() -> TypographyPolicy {
  {
    min_size_px: 12,
    max_size_px: 128,
    min_line_height_px: 14,
    min_line_ratio_x100: 115,
    max_line_ratio_x100: 200,
    require_fallback: true,
    require_monotonic_scale: true,
  }
}

///|
pub fn TypographyPolicy::with_size_bounds(
  self : TypographyPolicy,
  minimum : Int,
  maximum : Int,
) -> TypographyPolicy {
  {
    ..self,
    min_size_px: if minimum < 1 {
      1
    } else {
      minimum
    },
    max_size_px: if maximum < minimum {
      minimum
    } else {
      maximum
    },
  }
}

///|
pub fn TypographyPolicy::with_line_ratio_bounds(
  self : TypographyPolicy,
  minimum_x100 : Int,
  maximum_x100 : Int,
) -> TypographyPolicy {
  {
    ..self,
    min_line_ratio_x100: if minimum_x100 < 100 {
      100
    } else {
      minimum_x100
    },
    max_line_ratio_x100: if maximum_x100 < minimum_x100 {
      minimum_x100
    } else {
      maximum_x100
    },
  }
}

///|
pub fn TypographyPolicy::with_fallback_requirement(
  self : TypographyPolicy,
  required : Bool,
) -> TypographyPolicy {
  { ..self, require_fallback: required }
}

///|
pub fn TypographyPolicy::with_monotonic_requirement(
  self : TypographyPolicy,
  required : Bool,
) -> TypographyPolicy {
  { ..self, require_monotonic_scale: required }
}

///|
pub fn TypeStep::ratio_x100(self : TypeStep) -> Int {
  if self.size_px <= 0 {
    0
  } else {
    self.line_height_px * 100 / self.size_px
  }
}

///|
pub fn TypeStep::ratio_to(self : TypeStep, other : TypeStep) -> Int {
  if other.size_px <= 0 {
    0
  } else {
    self.size_px * 100 / other.size_px
  }
}

///|
pub fn TypeStep::is_valid(self : TypeStep, policy : TypographyPolicy) -> Bool {
  self.name.trim().to_owned() != "" &&
  self.size_px >= policy.min_size_px &&
  self.size_px <= policy.max_size_px &&
  self.line_height_px >= policy.min_line_height_px &&
  self.ratio_x100() >= policy.min_line_ratio_x100 &&
  self.ratio_x100() <= policy.max_line_ratio_x100
}

///|
pub fn TypeStep::is_accessible(self : TypeStep) -> Bool {
  self.ratio_x100() >= 115
}

///|
pub fn TypeStep::line_gap(self : TypeStep) -> Int {
  self.line_height_px - self.size_px
}

///|
pub fn TypeStep::css_size(self : TypeStep) -> String {
  self.size_px.to_string() + "px"
}

///|
pub fn TypeStep::css_line_height(self : TypeStep) -> String {
  self.line_height_px.to_string() + "px"
}

///|
pub fn TypeStep::css_variable_name(self : TypeStep) -> String {
  "--brand-type-" + normalize_identifier(self.name)
}

///|
pub fn TypeStep::description(self : TypeStep) -> String {
  self.name +
  ": " +
  self.css_size() +
  "/" +
  self.css_line_height() +
  " (" +
  self.ratio_x100().to_string() +
  "%)"
}

///|
pub fn FontToken::fallback_chain(self : FontToken) -> Array[String] {
  let values : Array[String] = [self.name]
  for value in self.fallback.split(",") {
    let clean = value.trim().to_owned()
    if clean != "" {
      values.push(clean)
    }
  }
  values
}

///|
pub fn FontToken::has_fallback(self : FontToken) -> Bool {
  self.fallback_chain().length() >= 2
}

///|
pub fn FontToken::css_family(self : FontToken) -> String {
  "'" + self.name + "', " + self.fallback
}

///|
pub fn FontToken::css_variable_name(self : FontToken) -> String {
  "--brand-font-" + normalize_identifier(self.name)
}

///|
pub fn FontToken::usage_label(self : FontToken) -> String {
  if self.usage.trim().to_owned() == "" {
    "unassigned"
  } else {
    self.usage
  }
}

///|
pub fn Typography::audit_rhythm(
  self : Typography,
  policy : TypographyPolicy,
) -> AuditReport {
  let findings : Array[Finding] = []
  if self.families.length() == 0 {
    findings.push(Finding::{
      severity: Error,
      path: "typography.families",
      message: "at least one font family is required",
    })
  }
  for index, family in self.families {
    if family.name.trim().to_owned() == "" {
      findings.push(Finding::{
        severity: Error,
        path: "typography.families[" + index.to_string() + "]",
        message: "font family name cannot be empty",
      })
    }
    if policy.require_fallback && !family.has_fallback() {
      findings.push(Finding::{
        severity: Error,
        path: "typography.families[" + index.to_string() + "].fallback",
        message: "font family must declare a fallback chain",
      })
    }
  }
  if self.scale.length() == 0 {
    findings.push(Finding::{
      severity: Error,
      path: "typography.scale",
      message: "at least one type step is required",
    })
  }
  for index, step in self.scale {
    if !step.is_valid(policy) {
      findings.push(Finding::{
        severity: Error,
        path: "typography.scale[" + index.to_string() + "]",
        message: "type step is outside the configured rhythm bounds",
      })
    }
    if step.name.trim().to_owned() == "" {
      findings.push(Finding::{
        severity: Error,
        path: "typography.scale[" + index.to_string() + "].name",
        message: "type step name cannot be empty",
      })
    }
  }
  if policy.require_monotonic_scale {
    for index in 0.. 0 && self.scale[index].size_px < self.scale[index - 1].size_px {
        findings.push(Finding::{
          severity: Error,
          path: "typography.scale[" + index.to_string() + "]",
          message: "type scale must be monotonic by size",
        })
      }
    }
  }
  AuditReport::from_findings(findings)
}

///|
pub fn Typography::is_valid(self : Typography) -> Bool {
  !self.audit_rhythm(TypographyPolicy::default()).has_errors()
}

///|
pub fn Typography::css_variable_map(self : Typography) -> String {
  let lines : Array[String] = [":root {"]
  for family in self.families {
    lines.push(
      "  " + family.css_variable_name() + ": " + family.css_family() + ";",
    )
  }
  for step in self.scale {
    lines.push("  " + step.css_variable_name() + ": " + step.css_size() + ";")
    lines.push(
      "  " + step.css_variable_name() + "-line: " + step.css_line_height() + ";",
    )
  }
  lines.push("}")
  lines.join("\n")
}

///|
pub fn Typography::font_stack(self : Typography, name : String) -> String {
  for family in self.families {
    if family.name == name {
      return family.css_family()
    }
  }
  "system-ui, sans-serif"
}

///|
pub fn Typography::step_named(self : Typography, name : String) -> TypeStep? {
  for step in self.scale {
    if step.name == name {
      return Some(step)
    }
  }
  None
}

///|
pub fn Typography::scale_names(self : Typography) -> Array[String] {
  self.scale.map(fn(step) { step.name })
}

///|
pub fn Typography::largest_step(self : Typography) -> TypeStep? {
  if self.scale.length() == 0 {
    None
  } else {
    let mut result = self.scale[0]
    for step in self.scale {
      if step.size_px > result.size_px {
        result = step
      }
    }
    Some(result)
  }
}

///|
pub fn Typography::smallest_step(self : Typography) -> TypeStep? {
  if self.scale.length() == 0 {
    None
  } else {
    let mut result = self.scale[0]
    for step in self.scale {
      if step.size_px < result.size_px {
        result = step
      }
    }
    Some(result)
  }
}

///|
pub fn Typography::total_rhythm_units(self : Typography) -> Int {
  self.scale.fold(init=0, fn(total, step) { total + step.line_height_px })
}

///|
pub fn Typography::average_line_ratio(self : Typography) -> Int {
  if self.scale.length() == 0 {
    0
  } else {
    self.scale.fold(init=0, fn(total, step) { total + step.ratio_x100() }) /
    self.scale.length()
  }
}

///|
pub fn Typography::to_scale_markdown(self : Typography) -> String {
  let lines : Array[String] = [
    "| Name | Size | Line height | Ratio |", "| --- | ---: | ---: | ---: |",
  ]
  for step in self.scale {
    lines.push(
      "| " +
      step.name +
      " | " +
      step.size_px.to_string() +
      "px | " +
      step.line_height_px.to_string() +
      "px | " +
      step.ratio_x100().to_string() +
      "% |",
    )
  }
  lines.join("\n")
}

///|
pub fn Typography::family_names(self : Typography) -> Array[String] {
  self.families.map(fn(family) { family.name })
}

///|
pub fn Typography::has_step(self : Typography, name : String) -> Bool {
  self.step_named(name) is Some(_)
}

///|
pub fn Typography::accessible_step_count(self : Typography) -> Int {
  self.scale.filter(fn(step) { step.is_accessible() }).length()
}

///|
pub fn Typography::family_for_usage(
  self : Typography,
  usage : String,
) -> FontToken? {
  for family in self.families {
    if family.usage.contains(usage) {
      return Some(family)
    }
  }
  None
}