///|
/// A CSS value the caller vouches for: templ's `templ.SafeCSS` and
/// `templ.SafeCSSProperty`. It is escaped for its place in the attribute but
/// never handed to the sanitizer.
pub(all) struct SafeCSS {
  priv value : String
}

///|
pub fn SafeCSS::new(value : String) -> SafeCSS {
  { value, }
}

///|
/// One piece of a `style` attribute. templ takes `...any` and switches on the
/// dynamic type, reflection included; MoonBit has neither `any` nor reflection,
/// so the shapes that switch handles by name are the variants here, and the
/// shapes it reached by reflection — a function, a slice of anything — are
/// written out by the caller as the variants they hold.
pub(all) enum StylePart {
  /// A whole style string, escaped the way a CSS string token is.
  Text(String)
  /// A whole style string the caller vouches for.
  Trusted(SafeCSS)
  /// `property: value`, both sanitized.
  Property(KV[String, String])
  /// `property: value` whose value the caller vouches for; the name is still
  /// checked.
  TrustedProperty(KV[String, SafeCSS])
  /// templ's `templ.KV("property: value", on)`: the key is a whole declaration
  /// rather than a name, and it is written only while the flag holds.
  Toggle(KV[String, Bool])
  /// The same, with a declaration the caller vouches for.
  TrustedToggle(KV[SafeCSS, Bool])
}

///|
/// A list of pieces: what `style={ ... }` renders, and the MoonBit spelling of
/// templ's `SanitizeStyleAttributeValues(values ...any)`.
pub(all) struct Style {
  priv parts : Array[StylePart]
}

///|
pub fn Style::new(parts : Array[StylePart]) -> Style {
  { parts, }
}

///|
/// The declarations of a map, rendered in property-name order — templ's
/// `map[string]string`, which it sorts for the same reason, a map having no
/// order of its own.
pub fn Style::from_map(properties : Map[String, String]) -> Style {
  let items : Array[KV[String, String]] = []
  for name, value in properties {
    items.push({ key: name, value, })
  }
  items.sort_by(fn(a, b) { a.key[:].lexical_compare(b.key[:]) })
  Style::new(items.map(fn(item) { Property(item) }))
}

///|
/// The declarations of a map whose values the caller vouches for — templ's
/// `map[string]templ.SafeCSSProperty`.
pub fn Style::from_trusted_map(properties : Map[String, SafeCSS]) -> Style {
  let items : Array[KV[String, SafeCSS]] = []
  for name, value in properties {
    items.push({ key: name, value, })
  }
  items.sort_by(fn(a, b) { a.key[:].lexical_compare(b.key[:]) })
  Style::new(items.map(fn(item) { TrustedProperty(item) }))
}

///|
/// What a `style={ ... }` attribute may hold. templ switches on the value's
/// dynamic type; here the value says what it is by which implementation it
/// takes, and a plain `String` — the common case, and the one templ's own
/// golden opens with — is one of them.
///
/// The value a `Stylable` renders is the one `SanitizeStyleAttributeValues`
/// returns: each piece escaped for its place between the quotes. The attribute
/// escapes that whole result again, exactly as templ's generator does, which is
/// why a value holding an `&` comes out as `&` here as well.
pub(open) trait Stylable {
  fn sanitized(Self) -> String
}

///|
/// The value of a `style={ ... }` attribute, ready to be written between the
/// quotes of the attribute: the sanitized value, escaped once more.
pub fn[A : Stylable] sanitize_style_attribute_value(v : A) -> String {
  escape(v.sanitized())
}

///|
pub impl Stylable for String with fn sanitized(self) {
  process_string(self)
}

///|
pub impl Stylable for SafeCSS with fn sanitized(self) {
  process_safe_css(self)
}

///|
pub extend SafeCSS with Stylable::{sanitized}

///|
pub impl Stylable for Style with fn sanitized(self) {
  let buf = StringBuilder()
  for part in self.parts {
    write_style_part(buf, part)
  }
  buf.to_string()
}

///|
pub extend Style with Stylable::{sanitized}

///|
fn write_style_part(buf : StringBuilder, part : StylePart) -> Unit {
  match part {
    Text(text) => buf.write_string(process_string(text))
    Trusted(safe) => buf.write_string(process_safe_css(safe))
    Property(pair) =>
      buf.write_string(process_property(pair.key, Sanitized(pair.value)))
    TrustedProperty(pair) =>
      buf.write_string(process_property(pair.key, Trusted(pair.value)))
    Toggle(pair) => if pair.value { buf.write_string(process_string(pair.key)) }
    TrustedToggle(pair) =>
      if pair.value {
        buf.write_string(process_safe_css(pair.key))
      }
  }
}

///|
/// The sanitized form of a whole style string: `processString`, which escapes
/// the value as a CSS string token and ends it with a declaration's semicolon.
fn process_string(value : String) -> String {
  if value.is_empty() {
    return ""
  }
  let sanitized = trim_space(sanitize_style_value(value))
  escape(sanitized) + semicolon_after(sanitized)
}

///|
/// The same for a value the caller vouches for: `processSafeCSS`, which escapes
/// it without sanitizing it.
fn process_safe_css(value : SafeCSS) -> String {
  if value.value.is_empty() {
    return ""
  }
  escape(value.value) + semicolon_after(value.value)
}

///|
fn semicolon_after(value : String) -> String {
  if value.has_suffix(";") {
    ""
  } else {
    ";"
  }
}

///|
/// One `property: value` declaration, which is what both of templ's key-value
/// shapes and both of its maps end up as.
fn process_property(name : String, value : PropertyValue) -> String {
  let (name, value) = match value {
    Sanitized(value) => sanitize_css(name, value)
    Trusted(value) => sanitize_css_name(name, value.value)
  }
  escape(name) + ":" + escape(value) + ";"
}

///|
/// Which side of a declaration the sanitizer is still owed: the value, or a name
/// plus a value the caller vouches for.
priv enum PropertyValue {
  Sanitized(String)
  Trusted(SafeCSS)
}

///|
/// `sanitizeRegular`'s verdict on a property value: one templ knows nothing
/// about is safe while every rune of it is one a value may hold, and a `*` or a
/// `/` — the halves of a comment — only while the next rune is safe too.
fn sanitize_regular(value : String) -> String {
  let chars = value.to_array()
  let mut i = 0
  while i < chars.length() {
    let c = chars[i]
    if is_safe_regular(c) {
      i = i + 1
      continue
    }
    if (c == '*' || c == '/') &&
      (i + 1 == chars.length() || is_safe_regular(chars[i + 1])) {
      i = i + 1
      continue
    }
    return INNOCUOUS_VALUE
  }
  value
}

///|
/// The runes `[0-9a-zA-Z+-.!#%_ \t]` holds. The `+-.` of that class is a range
/// rather than three runes, so the comma is in it — a property value may hold
/// one, whatever the comment beside the pattern in templ's source says.
fn is_safe_regular(c : Char) -> Bool {
  match c {
    '0'..<='9' | 'a'..<='z' | 'A'..<='Z' => true
    '+' | ',' | '-' | '.' | '!' | '#' | '%' | '_' | ' ' | '\t' => true
    _ => false
  }
}

///|
/// `sanitizeEnum`'s verdict: a value an enumerated property may hold is letters
/// and dashes.
fn sanitize_enum(value : String) -> String {
  for c in value {
    match c {
      'a'..<='z' | 'A'..<='Z' | '-' => ()
      _ => return INNOCUOUS_VALUE
    }
  }
  value
}

///|
/// `sanitizeFontFamily`'s verdict: a list of family names, each either quoted or
/// a generic name.
fn sanitize_font_family(value : String) -> String {
  for part in value.split(",") {
    let part = trim_space(part.to_owned())
    if part.has_prefix("\"") {
      if !part.has_suffix("\"") {
        return INNOCUOUS_VALUE
      }
      continue
    }
    if !is_generic_font_family(part) {
      return INNOCUOUS_VALUE
    }
  }
  value
}

///|
/// `^[a-zA-Z][- a-zA-Z]+$`: a generic family name, which is at least two runes
/// long because the pattern is.
fn is_generic_font_family(value : String) -> Bool {
  let chars = value.to_array()
  if chars.length() < 2 {
    return false
  }
  match chars[0] {
    'a'..<='z' | 'A'..<='Z' => ()
    _ => return false
  }
  for c in chars[1:] {
    match c {
      'a'..<='z' | 'A'..<='Z' | '-' | ' ' => ()
      _ => return false
    }
  }
  true
}

///|
/// `sanitizeBackgroundImage`'s verdict: a list of `url(...)`s, each of a URL
/// that may be followed.
fn sanitize_background_image(value : String) -> String {
  if value.contains("<") || value.contains(">") {
    return INNOCUOUS_VALUE
  }
  for part in value.split(",") {
    let part = trim_space(part.to_owned())
    match unwrap_url(part) {
      Some(url) => if !url_is_safe(url) { return INNOCUOUS_VALUE }
      None => return INNOCUOUS_VALUE
    }
  }
  value
}

///|
/// The URL inside a `url("...")`, `url('...')` or `url(...)`, or nothing when
/// the value is not one of those three.
fn unwrap_url(value : String) -> String? {
  let prefixes : Array[String] = ["url(\"", "url('", "url("]
  let suffixes : Array[String] = ["\")", "')", ")"]
  for i = 0; i < prefixes.length(); i = i + 1 {
    let prefix = prefixes[i]
    let suffix = suffixes[i]
    if value.has_prefix(prefix) && value.has_suffix(suffix) {
      let start = prefix.length()
      let end = value.length() - suffix.length()
      if start <= end {
        return Some(value[start:end].to_owned())
      }
    }
  }
  None
}

///|
/// Whether a URL inside `url(...)` may be followed: templ takes anything
/// relative, and a URL that names a scheme only when the scheme is one of the
/// three it knows.
///
/// This is templ's `urlIsSafe`, which asks `url.Parse` and then looks at the
/// scheme. Go refuses a URL it cannot parse, which is why the three checks above
/// the scheme are here: a control rune, a `%` that does not begin an escape, and
/// — for a URL that names no scheme and is not rooted — a `:` in its first path
/// segment, which is how a relative URL is told apart from a misspelled scheme.
fn url_is_safe(url : String) -> Bool {
  for c in url {
    if c <= '\u{1f}' || c == '\u{7f}' {
      return false
    }
  }
  if has_bad_escape(url) {
    return false
  }
  match scheme_of(url) {
    Some(scheme) =>
      scheme.to_lower() == "http" ||
      scheme.to_lower() == "https" ||
      scheme.to_lower() == "mailto"
    None =>
      if url.has_prefix(":") {
        false
      } else if url.has_prefix("/") {
        true
      } else {
        !first_segment(url).contains(":")
      }
  }
}

///|
/// Whether a `%` in the URL fails to begin an escape, which is what Go's
/// `EscapeError` reports.
fn has_bad_escape(url : String) -> Bool {
  let chars = url.to_array()
  let mut i = 0
  while i < chars.length() {
    if chars[i] == '%' {
      if i + 2 >= chars.length() ||
        !is_hex_digit(chars[i + 1]) ||
        !is_hex_digit(chars[i + 2]) {
        return true
      }
      i = i + 3
    } else {
      i = i + 1
    }
  }
  false
}

///|
fn is_hex_digit(c : Char) -> Bool {
  match c {
    '0'..<='9' | 'a'..<='f' | 'A'..<='F' => true
    _ => false
  }
}

///|
/// The text before the URL's first `/`, which is the segment Go checks for a
/// colon.
fn first_segment(url : String) -> String {
  match url.find("/") {
    Some(index) => url[:index].to_owned()
    None => url
  }
}

///|
/// The scheme a URL names, or nothing when it is relative. A scheme is the text
/// before the first `:` where that comes before any `/`, `?` or `#`, and it has
/// the shape `[a-zA-Z][a-zA-Z0-9+.-]*`.
fn scheme_of(url : String) -> String? {
  let buf = StringBuilder()
  for c in url {
    match c {
      'a'..<='z' | 'A'..<='Z' | '0'..<='9' | '+' | '-' | '.' => {
        if buf.is_empty() && !is_letter(c) {
          return None
        }
        buf.write_char(c)
      }
      ':' => {
        let scheme = buf.to_string()
        if scheme.is_empty() {
          return None
        }
        return Some(scheme)
      }
      _ => return None
    }
  }
  None
}

///|
fn is_letter(c : Char) -> Bool {
  match c {
    'a'..<='z' | 'A'..<='Z' => true
    _ => false
  }
}

///|
/// The sanitizers a property's value is owed, by the property's name —
/// `SanitizeCSSValue`'s table, which knows three properties by name and lets
/// everything else through the one check.
fn sanitize_css_value(property : String, value : String) -> String {
  match property {
    "display" => sanitize_enum(value)
    "font-family" => sanitize_font_family(value)
    "background-image" => sanitize_background_image(value)
    _ => sanitize_regular(value)
  }
}

///|
/// templ's `safehtml.SanitizeCSS`: the property's name is checked and lowered,
/// and its value is sanitized as the property's kind of value.
fn sanitize_css(property : String, value : String) -> (String, String) {
  let property = sanitize_css_property(property)
  if property == INNOCUOUS_PROPERTY {
    return (INNOCUOUS_PROPERTY, INNOCUOUS_VALUE)
  }
  (property, sanitize_css_value(property, value))
}

///|
/// The same for a value the caller vouches for: the name is checked, the value
/// is not.
fn sanitize_css_name(property : String, value : String) -> (String, String) {
  let property = sanitize_css_property(property)
  if property == INNOCUOUS_PROPERTY {
    return (INNOCUOUS_PROPERTY, INNOCUOUS_VALUE)
  }
  (property, value)
}

///|
/// Whether a name may be a CSS property's: `^[-a-zA-Z]+$`, lowered. Anything
/// else is replaced rather than dropped, so the declaration still has a name and
/// the page still has an attribute.
fn sanitize_css_property(property : String) -> String {
  if property.is_empty() {
    return INNOCUOUS_PROPERTY
  }
  for c in property {
    match c {
      'a'..<='z' | 'A'..<='Z' | '-' => ()
      _ => return INNOCUOUS_PROPERTY
    }
  }
  property.to_lower()
}

///|
/// The name a declaration with an unusable property gets, and the value one with
/// an unusable value gets. Both are templ's, and both are meant to be noticed.
const INNOCUOUS_PROPERTY : String = "zTemplUnsafeCSSPropertyName"

///|
const INNOCUOUS_VALUE : String = "zTemplUnsafeCSSPropertyValue"

///|
/// `safehtml.SanitizeStyleValue`: a style string escaped so that it may sit
/// between the quotes of an attribute, which is what keeps a `` inside
/// one from ending the element it is written in.
///
/// A rune a string token may not hold is written as the CSS escape `\` and six
/// hex digits, and a null is replaced rather than escaped, both as templ does.
fn sanitize_style_value(value : String) -> String {
  let buf = StringBuilder()
  for c in value {
    match c {
      '\u{0}' => buf.write_string("\u{fffd}")
      '<' | '"' | '\\' => write_css_escape(buf, c)
      '\u{1}'..<='\u{1f}' | '\u{7f}'..<='\u{9f}' | '\u{2028}' | '\u{2029}' =>
        write_css_escape(buf, c)
      _ => buf.write_char(c)
    }
  }
  buf.to_string()
}

///|
fn write_css_escape(buf : StringBuilder, c : Char) -> Unit {
  let digits : Array[Char] = [
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F',
  ]
  buf.write_char('\\')
  let mut shift = 20
  while shift >= 0 {
    buf.write_char(digits[(c.to_int() >> shift) & 0xF])
    shift = shift - 4
  }
}

///|
/// Go's `unicode.IsSpace`, which is what `strings.TrimSpace` trims and what
/// `strings.Fields` splits on.
fn trim_space(value : String) -> String {
  let chars = value.to_array()
  let mut start = 0
  let mut end = chars.length()
  while start < end && is_space(chars[start]) {
    start = start + 1
  }
  while end > start && is_space(chars[end - 1]) {
    end = end - 1
  }
  let buf = StringBuilder()
  for i = start; i < end; i = i + 1 {
    buf.write_char(chars[i])
  }
  buf.to_string()
}

///|
fn is_space(c : Char) -> Bool {
  c == ' ' ||
  c == '\t' ||
  c == '\n' ||
  c == '\r' ||
  c == '\u{0b}' ||
  c == '\u{0c}' ||
  c == '\u{85}' ||
  c == '\u{a0}'
}