///|
pub(all) enum StrictResponse {
  Ignore
  Warn
  Error
}

///|
pub type StrictHandler = (String, String, SourceLocation?) -> StrictResponse raise ParseFailure

///|
pub(all) enum Strictness {
  Ignore
  Warn
  Error
  Callback(StrictHandler)
}

///|
pub type StrictWarningHandler = (String) -> Unit

///|
pub(all) enum TrustContext {
  UrlTrust(command~ : String, url~ : String, protocol~ : String?)
  HtmlClass(class~ : String)
  HtmlId(id~ : String)
  HtmlStyle(style~ : String)
  HtmlData(attributes~ : Map[String, String])
}

///|
pub type TrustHandler = (TrustContext) -> Bool

///|
pub(all) enum TrustPolicy {
  Untrusted
  Trusted
  Callback(TrustHandler)
}

///|
pub struct Macros(Map[String, MacroDefinition])

///|
pub fn Macros::make(macros? : Map[String, String] = Map([])) -> Macros {
  let definitions : Map[String, MacroDefinition] = Map([])
  for name, expansion in macros {
    definitions[name] = MacroDefinition::text(expansion)
  }
  Macros(definitions)
}

///|
pub struct Settings {
  throw_on_error : Bool
  display_mode : Bool
  leqno : Bool
  error_color : String
  color_is_text_color : Bool
  max_expand : Int
  global_group : Bool
  macros : Map[String, String]
  macro_store : Macros?
  strict : Strictness
  strict_warning_handler : StrictWarningHandler?
  trust : TrustPolicy
}

///|
pub fn Settings::make(
  throw_on_error? : Bool = true,
  display_mode? : Bool = false,
  leqno? : Bool = false,
  error_color? : String = "#cc0000",
  color_is_text_color? : Bool = false,
  max_expand? : Int = 1000,
  global_group? : Bool = false,
  macros? : Map[String, String] = Map([]),
  macro_store? : Macros,
  strict? : Strictness = Ignore,
  strict_warning_handler? : StrictWarningHandler,
  trust? : TrustPolicy = Untrusted,
) -> Settings {
  {
    throw_on_error,
    display_mode,
    leqno,
    error_color,
    color_is_text_color,
    max_expand,
    global_group,
    macros,
    macro_store,
    strict,
    strict_warning_handler,
    trust,
  }
}

///|
pub impl Default for Settings with fn default() {
  Settings::make()
}

///|
fn Settings::macro_definitions(self : Settings) -> Map[String, MacroDefinition] {
  let definitions : Map[String, MacroDefinition] = Map([])
  for name, expansion in self.macros {
    definitions[name] = MacroDefinition::text(expansion)
  }
  definitions
}

///|
fn Settings::use_strict_behavior(
  self : Settings,
  error_code : String,
  error_message : String,
  token : Token?,
) -> Bool {
  match
    strict_response(
      self.strict,
      error_code,
      error_message,
      token_location(token),
    ) {
    Error => true
    Warn => {
      let warning = "LaTeX-incompatible input and strict mode is set to 'warn': \{error_message} [\{error_code}]"
      if self.strict_warning_handler is Some(handler) {
        handler(warning)
      }
      false
    }
    Ignore => false
  }
}

///|
fn ascii_lower(text : String) -> String {
  let builder = StringBuilder()
  for _, code in text {
    builder.write_char(code.to_ascii_lowercase())
  }
  builder.to_string()
}

///|
fn url_starts_with(url : String, offset : Int, prefix : String) -> Bool {
  offset + prefix.length() <= url.length() &&
  url.unsafe_substring(start=offset, end=offset + prefix.length()) == prefix
}

///|
fn encoded_colon_at(url : String, offset : Int) -> Bool {
  if url_starts_with(ascii_lower(url), offset, "&colon") {
    return true
  }
  guard url_starts_with(url, offset, "&#") else { false }
  let mut index = offset + 2
  if index < url.length() && (url[index] == 'x' || url[index] == 'X') {
    index = index + 1
    while index < url.length() && url[index] == '0' {
      index = index + 1
    }
    url_starts_with(ascii_lower(url), index, "3a")
  } else {
    while index < url.length() && url[index] == '0' {
      index = index + 1
    }
    url_starts_with(url, index, "58")
  }
}

///|
fn url_protocol(url : String) -> String? {
  let mut index = 0
  while index < url.length() && url[index] <= 0x20 {
    index = index + 1
  }
  let start = index
  while index < url.length() {
    let code = url[index]
    if code == ':' {
      guard index > start else { return None }
      let scheme = url.unsafe_substring(start~, end=index)
      guard (scheme[0] >= 'A' && scheme[0] <= 'Z') ||
        (scheme[0] >= 'a' && scheme[0] <= 'z') else {
        return None
      }
      for _, char in scheme {
        guard (char >= 'A' && char <= 'Z') ||
          (char >= 'a' && char <= 'z') ||
          (char >= '0' && char <= '9') ||
          char == '+' ||
          char == '-' ||
          char == '.' else {
          return None
        }
      }
      return Some(ascii_lower(scheme))
    }
    if code == '/' || code == '#' || code == '?' {
      return Some("_relative")
    }
    if code == '&' && encoded_colon_at(url, index) {
      return None
    }
    index = index + 1
  }
  Some("_relative")
}

///|
fn Settings::is_trusted(self : Settings, context : TrustContext) -> Bool {
  let context = match context {
    UrlTrust(command~, url~, ..) => {
      guard url_protocol(url) is Some(protocol) else { return false }
      UrlTrust(command~, url~, protocol=Some(protocol))
    }
    HtmlClass(..) | HtmlId(..) | HtmlStyle(..) | HtmlData(..) => context
  }
  match self.trust {
    Untrusted => false
    Trusted => true
    Callback(handler) => handler(context)
  }
}

///|
fn strict_response(
  strictness : Strictness,
  error_code : String,
  error_message : String,
  loc : SourceLocation?,
) -> StrictResponse {
  match strictness {
    Ignore => Ignore
    Warn => Warn
    Error => Error
    Callback(handler) =>
      handler(error_code, error_message, loc) catch {
        _ => Error
      }
  }
}

///|
fn Settings::report_nonstrict(
  self : Settings,
  error_code : String,
  error_message : String,
  token : Token?,
) -> Unit raise ParseFailure {
  match
    strict_response(
      self.strict,
      error_code,
      error_message,
      token_location(token),
    ) {
    Ignore => ()
    Warn => {
      let warning = "LaTeX-incompatible input and strict mode is set to 'warn': " +
        error_message +
        " [" +
        error_code +
        "]"
      if self.strict_warning_handler is Some(handler) {
        handler(warning)
      }
    }
    Error =>
      raise InvalidArgument(
        message="LaTeX-incompatible input and strict mode is set to 'error': " +
          error_message +
          " [" +
          error_code +
          "]",
        loc=token_location(token),
      )
  }
}