///|
fn is_valid_css_property_name(name : StringView) -> Bool {
  if name.is_empty() {
    return false
  }
  for ch in name {
    if ch.is_ascii_alphabetic() || (ch >= '0' && ch <= '9') || ch == '-' {
      continue
    }
    return false
  }
  true
}

///|
fn css_safety_normalize(value : StringView) -> String? {
  let out = StringBuilder::new(size_hint=value.length())
  let mut pos = 0
  while pos < value.length() {
    let ch = value.get_char(pos).unwrap()
    match ch {
      '\\' => return None
      '/' if value.get_char(pos + 1) is Some('*') => {
        pos += 2
        let mut closed = false
        while pos < value.length() {
          if value.get_char(pos) is Some('*') &&
            value.get_char(pos + 1) is Some('/') {
            pos += 2
            closed = true
            break
          }
          let ch = value.get_char(pos).unwrap()
          pos += ch.utf16_len()
        }
        if !closed {
          return None
        }
      }
      _ if is_url_ignored_scheme_char(ch) => pos += ch.utf16_len()
      _ => {
        if ch.is_ascii_uppercase() {
          out.write_char(ch.to_ascii_lowercase())
        } else {
          out.write_char(ch)
        }
        pos += ch.utf16_len()
      }
    }
  }
  Some(out.to_string())
}

///|
/// Conservatively detect CSS values that can load external resources.
///
/// This returns `true` for imports, `url(...)`, image-set-like functions,
/// legacy browser extension hooks, and values that cannot be normalized safely
/// because of escapes or malformed comments. It is a prefilter; URL-bearing
/// declarations still need URL-policy validation before they are kept.
pub fn css_value_may_load_external_resource(value : StringView) -> Bool {
  match css_safety_normalize(value) {
    None => true
    Some(normalized) =>
      string_view_contains(normalized, "@import") ||
      string_view_contains(normalized, "url(") ||
      string_view_contains(normalized, "image-set(") ||
      string_view_contains(normalized, "expression(") ||
      string_view_contains(normalized, "progid:") ||
      string_view_contains(normalized, "alphaimageloader") ||
      string_view_contains(normalized, "behavior:") ||
      string_view_contains(normalized, "-moz-binding")
  }
}

///|
fn css_value_has_disallowed_resource_functions(value : StringView) -> Bool {
  match css_safety_normalize(value) {
    None => true
    Some(normalized) =>
      string_view_contains(normalized, "@import") ||
      string_view_contains(normalized, "image-set(") ||
      string_view_contains(normalized, "expression(") ||
      string_view_contains(normalized, "progid:") ||
      string_view_contains(normalized, "alphaimageloader") ||
      string_view_contains(normalized, "behavior:") ||
      string_view_contains(normalized, "-moz-binding")
  }
}

///|
fn UrlPolicy::lookup_css_rule(
  self : UrlPolicy,
  tag_name : String,
  prop : String,
) -> UrlRule? {
  let key = "style:" + prop
  match self.lookup_rule(tag_name, key) {
    Some(rule) => Some(rule)
    None => self.lookup_rule("*", key)
  }
}

///|
fn css_url_skip_whitespace(value : StringView, start : Int) -> Int {
  let mut pos = start
  while pos < value.length() {
    match value.get_char(pos) {
      Some(ch) if is_url_ignored_scheme_char(ch) => pos += ch.utf16_len()
      _ => return pos
    }
  }
  pos
}

///|
fn css_find_char(value : StringView, start : Int, needle : Char) -> Int? {
  let mut pos = start
  while pos < value.length() {
    let ch = value.get_char(pos).unwrap()
    if ch == needle {
      return Some(pos)
    } else {
      pos += ch.utf16_len()
    }
  }
  None
}

///|
fn css_next_url_function(value : StringView, start : Int) -> Int? {
  let mut pos = start
  while pos < value.length() {
    if @syn.starts_with_case_insensitive(value, pos, "url(") {
      return Some(pos)
    }
    let ch = value.get_char(pos).unwrap()
    pos += ch.utf16_len()
  }
  None
}

///|
fn css_url_value_is_safe_to_quote(value : StringView) -> Bool {
  for ch in value {
    let code = ch.to_int()
    if code <= 0x20 ||
      code == 0x7F ||
      ch == '\'' ||
      ch == '"' ||
      ch == '(' ||
      ch == ')' ||
      ch == '\\' {
      return false
    }
  }
  true
}

///|
fn write_string_view(out : StringBuilder, value : StringView) -> Unit {
  for ch in value {
    out.write_char(ch)
  }
}

///|
fn sanitize_css_url_functions(
  policy : SanitizationPolicy,
  tag_name : String,
  prop : String,
  value : StringView,
) -> String? {
  guard policy.url_policy.lookup_css_rule(tag_name, prop) is Some(rule) else {
    return None
  }
  let attr_name = "style:" + prop
  sanitize_url_function_value(
    policy.url_policy,
    rule,
    tag_name,
    attr_name,
    value,
  )
}

///|
fn sanitize_url_function_value(
  url_policy : UrlPolicy,
  rule : UrlRule,
  tag_name : String,
  attr_name : String,
  value : StringView,
) -> String? {
  if css_value_has_disallowed_resource_functions(value) ||
    string_view_contains(value, "/*") {
    return None
  }
  let out = StringBuilder::new(size_hint=value.length())
  let mut pos = 0
  let mut replaced_any = false
  while true {
    match css_next_url_function(value, pos) {
      Some(url_start) => {
        let prefix = value[pos:url_start]
        write_string_view(out, prefix)
        let mut cursor = css_url_skip_whitespace(value, url_start + 4)
        guard value.get_char(cursor) is Some(first) else { return None }
        let url_raw = match first {
          '\'' | '"' => {
            let quote = first
            cursor += quote.utf16_len()
            let url_start = cursor
            guard css_find_char(value, cursor, quote) is Some(url_end) else {
              return None
            }
            cursor = css_url_skip_whitespace(value, url_end + quote.utf16_len())
            if !(value.get_char(cursor) is Some(')')) {
              return None
            }
            value[url_start:url_end]
          }
          _ => {
            let url_start = cursor
            guard css_find_char(value, cursor, ')') is Some(url_end) else {
              return None
            }
            cursor = url_end
            let raw = value[url_start:url_end].trim()
            if raw.is_empty() || string_contains_forbidden_url_char(raw) {
              return None
            }
            raw
          }
        }
        let after_paren = cursor + 1
        if after_paren < value.length() {
          match value.get_char(after_paren) {
            Some(ch) if is_url_ignored_scheme_char(ch) || ch == ',' || ch == '/' =>
              ()
            _ => return None
          }
        }
        match
          sanitize_url_attribute_value_with_rule(
            url_policy,
            rule,
            tag_name,
            attr_name,
            url_raw.to_owned(),
          ) {
          Some(sanitized_url) if css_url_value_is_safe_to_quote(sanitized_url) => {
            out.write_string("url('")
            out.write_string(sanitized_url)
            out.write_string("')")
          }
          _ => return None
        }
        replaced_any = true
        pos = after_paren
      }
      None => {
        let rest = value[pos:value.length()]
        write_string_view(out, rest)
        break
      }
    }
  }
  if replaced_any {
    Some(out.to_string())
  } else {
    None
  }
}

///|
fn sanitize_foreign_url_function_attribute_value(
  policy : SanitizationPolicy,
  tag_name : String,
  attr_name : String,
  value : String,
) -> String? {
  match policy.url_policy.lookup_rule(tag_name, attr_name) {
    Some(rule) =>
      sanitize_url_function_value(
        policy.url_policy,
        rule,
        tag_name,
        attr_name,
        value,
      )
    None => None
  }
}

///|
fn style_declaration_colon(declaration : StringView) -> Int? {
  let mut pos = 0
  while pos < declaration.length() {
    let ch = declaration.get_char(pos).unwrap()
    if ch == ':' {
      return Some(pos)
    } else {
      pos += ch.utf16_len()
    }
  }
  None
}

///|
fn sanitize_style_declaration(
  declaration : StringView,
  policy : SanitizationPolicy,
  tag_name : String,
) -> String? {
  let declaration = declaration.trim()
  if declaration.is_empty() {
    return None
  }
  guard style_declaration_colon(declaration) is Some(colon) else { return None }
  let prop_view = declaration[0:colon]
  let prop = @syn.lower_ascii(prop_view.trim())
  if !is_valid_css_property_name(prop) ||
    !policy.allowed_css_properties.contains(prop) {
    return None
  }
  let value_view = declaration[colon + 1:declaration.length()]
  let value = value_view.trim()
  if value.is_empty() {
    return None
  }
  if css_value_may_load_external_resource(value) {
    match sanitize_css_url_functions(policy, tag_name, prop, value) {
      Some(sanitized) => Some(prop + ": " + sanitized)
      None => None
    }
  } else {
    Some(prop + ": " + value.to_owned())
  }
}

///|
fn sanitize_inline_style_value(
  policy : SanitizationPolicy,
  tag_name : String,
  value : String,
) -> String? {
  if policy.allowed_css_properties.is_empty() || value == "" {
    return None
  }
  let view = value[:]
  let parts : Array[String] = []
  let mut start = 0
  let mut pos = 0
  while pos < view.length() {
    let ch = view.get_char(pos).unwrap()
    if ch == ';' {
      let declaration = view[start:pos]
      match sanitize_style_declaration(declaration, policy, tag_name) {
        Some(part) => parts.push(part)
        None => ()
      }
      pos += 1
      start = pos
    } else {
      pos += ch.utf16_len()
    }
  }
  let declaration = view[start:view.length()]
  match sanitize_style_declaration(declaration, policy, tag_name) {
    Some(part) => parts.push(part)
    None => ()
  }
  if parts.is_empty() {
    None
  } else {
    Some(parts.join("; "))
  }
}