///|
/// Errors produced when a value fails RSS-spec validation,
/// mirroring `rss::validation::ValidationError`.
pub(all) suberror ValidationError {
  /// A wrapped lower-level parse failure (URL, date, integer…).
  Invalid(String)
  /// A spec rule was violated.
  Validation(String)
} derive(Debug, Eq)

///|
pub impl Show for ValidationError with fn output(self, logger) -> Unit {
  match self {
    Invalid(msg) => logger.write_string(msg)
    Validation(msg) => logger.write_string(msg)
  }
}

///|
fn fail_validation(msg : String) -> ValidationError {
  Validation(msg)
}

///|
/// Lenient URL syntax check in the spirit of the `url` crate:
/// `scheme:` followed by a non-hierarchical or hierarchical part.
/// Requires `scheme://rest` with a reasonable scheme and non-empty rest.
fn is_valid_url(s : String) -> Bool {
  let mut colon = -1
  let limit = s.length() - 3
  let mut i = 0
  while i <= limit {
    if s[i] == (':' : UInt16) && substring_eq(s, i, "://") {
      colon = i
      break
    }
    i += 1
  }
  if colon < 0 {
    return false
  }
  if colon == 0 {
    return false
  }
  // scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
  let first = s[0].to_int()
  let is_alpha = fn(c : Int) -> Bool {
    (c >= 97 && c <= 122) || (c >= 65 && c <= 90)
  }
  let is_scheme_char = fn(c : Int) -> Bool {
    is_alpha(c) || (c >= 48 && c <= 57) || c == 43 || c == 45 || c == 46
  }
  if !is_alpha(first) {
    return false
  }
  for i in 1.. colon + 3
}

///|
fn validate_url(s : String, what~ : String) -> Unit raise ValidationError {
  if !is_valid_url(s) {
    raise Invalid("\{what}: invalid url \"\{s}\"")
  }
}

///|
let months : Array[String] = [
  "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
]

///|
let weekdays : Array[String] = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]

///|
/// Parse an RFC 822 / RFC 2822 date-time, returning its components.
/// Accepts the common forms used by feeds:
/// `[Weekday,] DD Mon YYYY HH:MM[:SS] Zone`.
pub fn parse_rfc2822(s : String) -> Bool {
  let parts : Array[String] = []
  let mut current = StringBuilder()
  let mut had_content = false
  for c in s {
    if c == ' ' || c == '\t' || c == ',' {
      if had_content {
        parts.push(current.to_string())
        current = StringBuilder()
        had_content = false
      }
    } else {
      current.write_char(c)
      had_content = true
    }
  }
  if had_content {
    parts.push(current.to_string())
  }
  let mut i = 0
  // Optional trailing-comment "(...)" sections and weekday names.
  while i < parts.length() {
    let p = parts[i]
    let mut is_weekday = false
    for wd in weekdays {
      if p.length() >= 3 && substring_eq(p, 0, wd) {
        is_weekday = true
        break
      }
    }
    if is_weekday {
      i += 1
    } else {
      break
    }
  }
  guard i < parts.length() else { return false }
  // Day: 1-2 digits.
  let day = parts[i]
  guard day.length() >= 1 && day.length() <= 2 else { return false }
  for c in day {
    guard c >= '0' && c <= '9' else { return false }
  }
  let day_num = digits_to_int(day)
  guard day_num >= 1 && day_num <= 31 else { return false }
  i += 1
  guard i < parts.length() else { return false }
  // Month name.
  let mon = parts[i]
  guard mon.length() == 3 else { return false }
  let mut found_month = false
  for m in months {
    if m == mon {
      found_month = true
      break
    }
  }
  guard found_month else { return false }
  i += 1
  guard i < parts.length() else { return false }
  // Year: 2 or 4 digits.
  let year = parts[i]
  guard year.length() == 2 || year.length() == 4 else { return false }
  for c in year {
    guard c >= '0' && c <= '9' else { return false }
  }
  i += 1
  guard i < parts.length() else { return false }
  // Time HH:MM[:SS].
  let time = parts[i]
  let tparts = split_char(time, ':')
  guard tparts.length() == 2 || tparts.length() == 3 else { return false }
  for tp in tparts {
    guard tp.length() == 2 else { return false }
    for c in tp {
      guard c >= '0' && c <= '9' else { return false }
    }
  }
  i += 1
  // Optional zone.
  if i < parts.length() {
    let zone = parts[i]
    if !(zone is "UT" ||
      zone is "GMT" ||
      zone is "UTC" ||
      zone is "EST" ||
      zone is "EDT" ||
      zone is "CST" ||
      zone is "CDT" ||
      zone is "MST" ||
      zone is "MDT" ||
      zone is "PST" ||
      zone is "PDT") {
      // numeric +/-HHMM
      guard zone.length() == 5 else { return false }
      let z0 = zone[0].to_int()
      guard z0 == 43 || z0 == 45 else { return false }
      for k in 1..<5 {
        let zc = zone[k].to_int()
        guard zc >= 48 && zc <= 57 else { return false }
      }
    }
  }
  true
}

///|
fn digits_to_int(s : String) -> Int {
  let mut v = 0
  for c in s {
    if c >= '0' && c <= '9' {
      v = v * 10 + (c.to_int() - '0'.to_int())
    }
  }
  v
}

///|
fn split_char(s : String, sep : Char) -> Array[String] {
  let out : Array[String] = []
  let mut start = 0
  for i in 0.. Bool {
  if s.length() == 0 {
    return false
  }
  for c in s {
    guard c >= '0' && c <= '9' else { return false }
  }
  true
}

///|
fn is_mime_type(s : String) -> Bool {
  let parts = split_char(s, '/')
  if parts.length() != 2 {
    return false
  }
  for p in parts {
    if p.length() == 0 {
      return false
    }
    for c in p {
      let ci = c.to_int()
      let ok = (ci >= 97 && ci <= 122) ||
        (ci >= 65 && ci <= 90) ||
        (ci >= 48 && ci <= 57) ||
        ci == 33 ||
        ci == 35 ||
        ci == 36 ||
        ci == 38 ||
        ci == 45 ||
        ci == 43 ||
        ci == 46 ||
        ci == 94 ||
        ci == 95 ||
        ci == 96 ||
        ci == 124 ||
        ci == 126
      guard ok else { return false }
    }
  }
  true
}

///|
/// Validate this category against the RSS spec.
pub fn Category::validate(self : Category) -> Unit raise ValidationError {
  if self.domain is Some(domain) {
    validate_url(domain, what="category domain")
  }
}

///|
/// Validate this cloud against the RSS spec.
pub fn Cloud::validate(self : Cloud) -> Unit raise ValidationError {
  guard is_positive_int(self.port) else {
    raise fail_validation("Cloud port must be greater than 0")
  }
  validate_url(self.domain, what="cloud domain")
  if !(self.protocol is "xml-rpc" ||
    self.protocol is "soap" ||
    self.protocol is "http-post") {
    raise fail_validation("Unknown cloud protocol: \{self.protocol}")
  }
}

///|
/// Validate this enclosure against the RSS spec.
pub fn Enclosure::validate(self : Enclosure) -> Unit raise ValidationError {
  validate_url(self.url, what="enclosure url")
  if !is_mime_type(self.mime_type) {
    raise Invalid("enclosure mime_type: invalid mime type")
  }
  let length = int_or_fail(self.length)
  guard length > 0 else {
    raise fail_validation("Enclosure length is not greater than 0")
  }
}

///|
/// Validate this text input against the RSS spec.
pub fn TextInput::validate(self : TextInput) -> Unit raise ValidationError {
  validate_url(self.link, what="textInput link")
}

///|
/// Validate this image against the RSS spec.
pub fn Image::validate(self : Image) -> Unit raise ValidationError {
  validate_url(self.link, what="image link")
  validate_url(self.url, what="image url")
  if self.width is Some(width) {
    let w = int_or_fail(width)
    guard w >= 0 && w <= 144 else {
      raise fail_validation("Image width is not between 0 and 144")
    }
  }
  if self.height is Some(height) {
    let h = int_or_fail(height)
    guard h >= 0 && h <= 144 else {
      raise fail_validation("Image height is not between 0 and 144")
    }
  }
}

///|
fn int_or_fail(s : String) -> Int raise ValidationError {
  @strconv.parse_int(s) catch {
    _ => raise Invalid("invalid integer \"\{s}\"")
  }
}

///|
/// Validate this source against the RSS spec.
pub fn Source::validate(self : Source) -> Unit raise ValidationError {
  validate_url(self.url, what="source url")
}

///|
/// Validate this item against the RSS spec.
pub fn Item::validate(self : Item) -> Unit raise ValidationError {
  if self.link is Some(link) {
    validate_url(link, what="item link")
  }
  if self.comments is Some(comments) {
    validate_url(comments, what="item comments")
  }
  if self.enclosure is Some(enclosure) {
    enclosure.validate()
  }
  if self.pub_date is Some(pub_date) {
    guard parse_rfc2822(pub_date) else {
      raise Invalid("item pub_date: invalid RFC2822 string")
    }
  }
  if self.source is Some(source) {
    source.validate()
  }
}

///|
/// Validate this channel against the RSS specification.
pub fn Channel::validate(self : Channel) -> Unit raise ValidationError {
  validate_url(self.link, what="channel link")
  for category in self.categories {
    category.validate()
  }
  if self.cloud is Some(cloud) {
    cloud.validate()
  }
  if self.docs is Some(docs) {
    validate_url(docs, what="channel docs")
  }
  if self.image is Some(image) {
    image.validate()
  }
  for item in self.items {
    item.validate()
  }
  if self.last_build_date is Some(date) {
    guard parse_rfc2822(date) else {
      raise Invalid("channel last_build_date: invalid RFC2822 string")
    }
  }
  if self.pub_date is Some(date) {
    guard parse_rfc2822(date) else {
      raise Invalid("channel pub_date: invalid RFC2822 string")
    }
  }
  for hour in self.skip_hours {
    let h = int_or_fail(hour)
    guard h >= 0 && h <= 23 else {
      raise fail_validation("Channel skip hour is not between 0 and 23")
    }
  }
  for day in self.skip_days {
    if !(day is "Monday" ||
      day is "Tuesday" ||
      day is "Wednesday" ||
      day is "Thursday" ||
      day is "Friday" ||
      day is "Saturday" ||
      day is "Sunday") {
      raise fail_validation("Unknown skip day: \{day}")
    }
  }
  if self.text_input is Some(text_input) {
    text_input.validate()
  }
  if self.ttl is Some(ttl) {
    let t = int_or_fail(ttl)
    guard t > 0 else {
      raise fail_validation("Channel TTL is not greater than 0")
    }
  }
}