///|
fn classify_source(token : String, line : Int) -> Source {
  let raw = trim_ascii(token)
  let lowered = lower_ascii(raw)
  let clean = strip_quotes(lowered)
  if raw == "*" {
    make_source(raw, Wildcard, "*", line)
  } else if starts_with(clean, "nonce-") {
    make_source(raw, Nonce, clean[6:].to_owned(), line)
  } else if starts_with(clean, "sha256-") ||
    starts_with(clean, "sha384-") ||
    starts_with(clean, "sha512-") {
    make_source(raw, Hash, clean, line)
  } else if starts_with(raw, "'") && ends_with(raw, "'") {
    make_source(raw, Keyword, clean, line)
  } else if ends_with(clean, ":") {
    make_source(raw, Scheme, clean, line)
  } else if raw.length() > 0 {
    make_source(raw, Host, lowered, line)
  } else {
    make_source(raw, UnknownSource, raw, line)
  }
}

///|
fn parse_directive(
  segment : String,
  line : Int,
  diagnostics : Array[Finding],
) -> Directive? {
  let text = trim_ascii(segment)
  if text.length() == 0 {
    return None
  }
  let words = split_words(text)
  if words.length() == 0 {
    return None
  }
  let name = normalize_token(words[0])
  let sources : Array[Source] = []
  let mut i = 1
  while i < words.length() {
    let src = classify_source(words[i], line)
    if src.kind == UnknownSource {
      diagnostics.push(
        make_finding(
          Warning,
          "empty-source",
          name,
          "empty source token ignored",
          line,
        ),
      )
    } else {
      sources.push(src)
    }
    i = i + 1
  }
  Some(make_directive(name, sources, text, line))
}

///|
/// Parse one Content-Security-Policy header value.
pub fn parse(input : String) -> Policy {
  let diagnostics : Array[Finding] = []
  let directives : Array[Directive] = []
  let segments = split_by_char(input, 59)
  for i, segment in segments {
    match parse_directive(segment, i + 1, diagnostics) {
      None => ()
      Some(directive) => {
        let mut duplicate = false
        for existing in directives {
          if existing.name == directive.name {
            duplicate = true
          }
        }
        if duplicate {
          diagnostics.push(
            make_finding(
              Warning,
              "duplicate-directive",
              directive.name,
              "duplicate directive appears later in the policy",
              directive.line,
            ),
          )
        }
        directives.push(directive)
      }
    }
  }
  if directives.length() == 0 {
    diagnostics.push(
      make_finding(Error, "empty-policy", "", "policy has no directives", 0),
    )
  }
  { directives, diagnostics }
}

///|
pub fn has_directive(policy : Policy, name : String) -> Bool {
  let needle = normalize_token(name)
  for directive in policy.directives {
    if directive.name == needle {
      return true
    }
  }
  false
}

///|
pub fn get_directive(policy : Policy, name : String) -> Directive? {
  let needle = normalize_token(name)
  for directive in policy.directives {
    if directive.name == needle {
      return Some(directive)
    }
  }
  None
}

///|
pub fn directive_has_source(directive : Directive, value : String) -> Bool {
  let needle = normalize_token(value)
  for source in directive.sources {
    if normalize_token(source.raw) == needle ||
      source.value == strip_quotes(needle) {
      return true
    }
  }
  false
}

///|
fn directive_names(policy : Policy) -> Array[String] {
  let out : Array[String] = []
  for directive in policy.directives {
    out.push(directive.name)
  }
  out
}