///|
pub enum Severity {
  Error
  Warning
} derive(Debug, Eq)

///|
pub struct ValidationIssue {
  severity : Severity
  path : String
  message : String
} derive(Debug, Eq)

///|
pub struct ValidationReport {
  ok : Bool
  issues : Array[ValidationIssue]
} derive(Debug, Eq)

///|
pub struct LogisticsProfile {
  has_sscc : Bool
  has_gtin : Bool
  has_lot : Bool
  has_serial : Bool
  has_expiration : Bool
} derive(Debug, Eq)

///|
pub fn GS1Message::validate(self : GS1Message) -> ValidationReport {
  let issues : Array[ValidationIssue] = []
  for idx, elem in self.elements {
    if !elem.is_valid() {
      issues.push({
        severity: Error,
        path: elem.code(),
        message: "AI \{elem.code()} value does not match \{elem.title()} constraints",
      })
    }
    if is_date_ai(elem.code()) && !is_valid_yymmdd(elem.value) {
      issues.push({
        severity: Error,
        path: elem.code(),
        message: "AI \{elem.code()} date must use YYMMDD with a valid month and day",
      })
    }
    if has_previous_code(self.elements, idx, elem.code()) {
      issues.push({
        severity: Warning,
        path: elem.code(),
        message: "AI \{elem.code()} appears more than once",
      })
    }
  }
  { ok: issues.is_empty(), issues }
}

///|
pub fn GS1Message::profile(self : GS1Message) -> LogisticsProfile {
  let mut has_sscc = false
  let mut has_gtin = false
  let mut has_lot = false
  let mut has_serial = false
  let mut has_expiration = false
  for elem in self.elements {
    match elem.code() {
      "00" => has_sscc = true
      "01" | "02" => has_gtin = true
      "10" => has_lot = true
      "21" => has_serial = true
      "17" => has_expiration = true
      _ => ()
    }
  }
  { has_sscc, has_gtin, has_lot, has_serial, has_expiration }
}

///|
fn is_date_ai(code : String) -> Bool {
  code == "11" ||
  code == "12" ||
  code == "13" ||
  code == "15" ||
  code == "16" ||
  code == "17"
}

///|
fn is_valid_yymmdd(value : String) -> Bool {
  if value.length() != 6 || !value.all(fn(ch) { ch.is_ascii_digit() }) {
    false
  } else {
    let month = two_digits(value, 2)
    let day = two_digits(value, 4)
    month >= 1 && month <= 12 && day >= 1 && day <= days_in_month(month)
  }
}

///|
fn two_digits(value : String, pos : Int) -> Int {
  (value[pos].to_int() - ('0' : UInt16).to_int()) * 10 +
  value[pos + 1].to_int() -
  ('0' : UInt16).to_int()
}

///|
fn days_in_month(month : Int) -> Int {
  match month {
    2 => 29
    4 | 6 | 9 | 11 => 30
    _ => 31
  }
}

///|
fn has_previous_code(
  elements : Array[GS1Element],
  idx : Int,
  code : String,
) -> Bool {
  for i = 0; i < idx; i = i + 1 {
    if elements[i].code() == code {
      return true
    }
  } nobreak {
    false
  }
}

///|
pub fn ValidationReport::summary(self : ValidationReport) -> String {
  if self.ok {
    "ok"
  } else {
    self.issues.map(fn(issue) { "\{issue.path}: \{issue.message}" }).join("; ")
  }
}