///|
pub(all) enum RecordKind {
  PersonRecord
  SkillRecord
  AvailabilityRecord
  PreferenceRecord
  ShiftRecord
  RequirementRecord
  AssignmentRecord
  RuleRecord
  CommentRecord
  BlankRecord
  UnknownRecord
} derive(Debug, Eq)

///|
pub(all) struct ParseIssue {
  line : Int
  column : Int
  message : String
  source : String
} derive(Debug, Eq)

///|
pub(all) struct ParsedRoster {
  problem : ScheduleProblem
  issues : Array[ParseIssue]
} derive(Debug, Eq)

///|
pub fn trim_ascii(text : String) -> String {
  let chars : Array[Char] = text.iter().collect()
  let mut left = 0
  let mut right = chars.length()
  while left < right &&
        (chars[left] == ' ' || chars[left] == '\t' || chars[left] == '\r') {
    left = left + 1
  }
  while right > left &&
        (
          chars[right - 1] == ' ' ||
          chars[right - 1] == '\t' ||
          chars[right - 1] == '\r'
        ) {
    right = right - 1
  }
  let mut out = ""
  for i = left; i < right; i = i + 1 {
    out = out + chars[i].to_string()
  }
  out
}

///|
pub fn parse_integer(text : String) -> Int? {
  let value = trim_ascii(text)
  if value == "" {
    return None
  }
  let chars : Array[Char] = value.iter().collect()
  let mut index = 0
  let mut sign = 1
  if chars[0] == '-' {
    sign = -1
    index = 1
  } else if chars[0] == '+' {
    index = 1
  }
  if index >= chars.length() {
    return None
  }
  let mut number = 0
  while index < chars.length() {
    let c = chars[index]
    if c < '0' || c > '9' {
      return None
    }
    number = number * 10 + c.to_int() - '0'.to_int()
    index = index + 1
  }
  Some(number * sign)
}

///|
pub fn split_fields(line : String) -> Array[String] {
  line.split("|").map(part => trim_ascii(part.to_owned())).collect()
}

///|
pub fn record_kind(line : String) -> RecordKind {
  let value = trim_ascii(line)
  if value == "" {
    return BlankRecord
  }
  if value.has_prefix("#") {
    return CommentRecord
  }
  let fields = split_fields(value)
  if fields.length() == 0 {
    return UnknownRecord
  }
  match fields[0] {
    "person" => PersonRecord
    "skill" => SkillRecord
    "available" => AvailabilityRecord
    "prefer" => PreferenceRecord
    "shift" => ShiftRecord
    "require" => RequirementRecord
    "assign" => AssignmentRecord
    "rule" => RuleRecord
    _ => UnknownRecord
  }
}

///|
fn issue(
  line : Int,
  column : Int,
  message : String,
  source : String,
) -> ParseIssue {
  { line, column, message, source }
}

///|
fn empty_person(
  id : String,
  name : String,
  target : Int,
  maximum : Int,
) -> Person {
  {
    id,
    name,
    skills: [],
    availability: [],
    preferences: [],
    target_minutes: target,
    maximum_minutes: maximum,
    maximum_consecutive_shifts: 5,
  }
}

///|
fn find_person_index(people : Array[Person], id : String) -> Int {
  for i = 0; i < people.length(); i = i + 1 {
    if people[i].id == id {
      return i
    }
  }
  -1
}

///|
fn find_shift_index(shifts : Array[Shift], id : String) -> Int {
  for i = 0; i < shifts.length(); i = i + 1 {
    if shifts[i].id == id {
      return i
    }
  }
  -1
}

///|
fn update_person_skills(person : Person, skill : Skill) -> Person {
  let values = person.skills.copy()
  values.push(skill)
  { ..person, skills: values }
}

///|
fn update_person_availability(person : Person, span : TimeSpan) -> Person {
  let values = person.availability.copy()
  values.push(span)
  { ..person, availability: values }
}

///|
fn update_person_preferences(
  person : Person,
  preference : ShiftPreference,
) -> Person {
  let values = person.preferences.copy()
  values.push(preference)
  { ..person, preferences: values }
}

///|
fn update_shift_requirements(
  shift : Shift,
  requirement : SkillRequirement,
) -> Shift {
  let values = shift.requirements.copy()
  values.push(requirement)
  { ..shift, requirements: values }
}

///|
pub fn parse_roster(text : String) -> ParsedRoster {
  let people : Array[Person] = []
  let shifts : Array[Shift] = []
  let assignments : Array[Assignment] = []
  let issues : Array[ParseIssue] = []
  let mut minimum_rest = 0
  let mut fairness_weight = 1
  let mut preference_weight = 1
  let mut disruption_weight = 10
  let lines : Array[String] = text
    .split("\n")
    .map(value => value.to_owned())
    .collect()
  for line_index = 0; line_index < lines.length(); line_index = line_index + 1 {
    let source = lines[line_index]
    let fields = split_fields(source)
    match record_kind(source) {
      BlankRecord | CommentRecord => ()
      PersonRecord =>
        if fields.length() != 5 {
          issues.push(
            issue(
              line_index + 1,
              1,
              "person expects id|name|target|max",
              source,
            ),
          )
        } else {
          match (parse_integer(fields[3]), parse_integer(fields[4])) {
            (Some(target), Some(maximum)) =>
              people.push(empty_person(fields[1], fields[2], target, maximum))
            _ =>
              issues.push(
                issue(
                  line_index + 1,
                  4,
                  "person minute limits must be integers",
                  source,
                ),
              )
          }
        }
      SkillRecord =>
        if fields.length() != 4 {
          issues.push(
            issue(line_index + 1, 1, "skill expects person|name|level", source),
          )
        } else {
          let index = find_person_index(people, fields[1])
          match parse_integer(fields[3]) {
            Some(level) =>
              if index < 0 {
                issues.push(
                  issue(
                    line_index + 1,
                    2,
                    "skill references unknown person",
                    source,
                  ),
                )
              } else {
                people[index] = update_person_skills(people[index], {
                  name: fields[2],
                  level,
                })
              }
            None =>
              issues.push(
                issue(
                  line_index + 1,
                  4,
                  "skill level must be an integer",
                  source,
                ),
              )
          }
        }
      AvailabilityRecord =>
        if fields.length() != 4 {
          issues.push(
            issue(
              line_index + 1,
              1,
              "available expects person|start|end",
              source,
            ),
          )
        } else {
          let index = find_person_index(people, fields[1])
          match (parse_integer(fields[2]), parse_integer(fields[3])) {
            (Some(start), Some(end)) =>
              if index < 0 {
                issues.push(
                  issue(
                    line_index + 1,
                    2,
                    "availability references unknown person",
                    source,
                  ),
                )
              } else {
                people[index] = update_person_availability(people[index], {
                  start,
                  end,
                })
              }
            _ =>
              issues.push(
                issue(
                  line_index + 1,
                  3,
                  "availability bounds must be integers",
                  source,
                ),
              )
          }
        }
      PreferenceRecord =>
        if fields.length() != 4 {
          issues.push(
            issue(
              line_index + 1,
              1,
              "prefer expects person|shift|weight",
              source,
            ),
          )
        } else {
          let index = find_person_index(people, fields[1])
          match parse_integer(fields[3]) {
            Some(weight) =>
              if index < 0 {
                issues.push(
                  issue(
                    line_index + 1,
                    2,
                    "preference references unknown person",
                    source,
                  ),
                )
              } else {
                people[index] = update_person_preferences(people[index], {
                  shift_id: fields[2],
                  weight,
                })
              }
            None =>
              issues.push(
                issue(
                  line_index + 1,
                  4,
                  "preference weight must be an integer",
                  source,
                ),
              )
          }
        }
      ShiftRecord =>
        if fields.length() != 8 {
          issues.push(
            issue(
              line_index + 1,
              1,
              "shift expects id|label|start|end|people|priority|group",
              source,
            ),
          )
        } else {
          match
            (
              parse_integer(fields[3]),
              parse_integer(fields[4]),
              parse_integer(fields[5]),
              parse_integer(fields[6]),
            ) {
            (Some(start), Some(end), Some(required), Some(priority)) =>
              shifts.push({
                id: fields[1],
                label: fields[2],
                time: { start, end },
                required_people: required,
                requirements: [],
                priority,
                group: fields[7],
              })
            _ =>
              issues.push(
                issue(
                  line_index + 1,
                  4,
                  "shift numeric fields are invalid",
                  source,
                ),
              )
          }
        }
      RequirementRecord =>
        if fields.length() != 4 {
          issues.push(
            issue(
              line_index + 1,
              1,
              "require expects shift|skill|level",
              source,
            ),
          )
        } else {
          let index = find_shift_index(shifts, fields[1])
          match parse_integer(fields[3]) {
            Some(level) =>
              if index < 0 {
                issues.push(
                  issue(
                    line_index + 1,
                    2,
                    "requirement references unknown shift",
                    source,
                  ),
                )
              } else {
                shifts[index] = update_shift_requirements(shifts[index], {
                  skill: fields[2],
                  minimum_level: level,
                })
              }
            None =>
              issues.push(
                issue(
                  line_index + 1,
                  4,
                  "requirement level must be integer",
                  source,
                ),
              )
          }
        }
      AssignmentRecord =>
        if fields.length() != 5 {
          issues.push(
            issue(
              line_index + 1,
              1,
              "assign expects shift|slot|person|locked",
              source,
            ),
          )
        } else {
          match parse_integer(fields[2]) {
            Some(slot) =>
              assignments.push({
                shift_id: fields[1],
                slot,
                person_id: fields[3],
                locked: fields[4] == "true",
              })
            None =>
              issues.push(
                issue(
                  line_index + 1,
                  3,
                  "assignment slot must be integer",
                  source,
                ),
              )
          }
        }
      RuleRecord =>
        if fields.length() != 3 {
          issues.push(
            issue(line_index + 1, 1, "rule expects name|value", source),
          )
        } else {
          match parse_integer(fields[2]) {
            None =>
              issues.push(
                issue(line_index + 1, 3, "rule value must be integer", source),
              )
            Some(value) =>
              match fields[1] {
                "minimum_rest" => minimum_rest = value
                "fairness_weight" => fairness_weight = value
                "preference_weight" => preference_weight = value
                "disruption_weight" => disruption_weight = value
                _ =>
                  issues.push(
                    issue(line_index + 1, 2, "unknown rule name", source),
                  )
              }
          }
        }
      UnknownRecord =>
        issues.push(issue(line_index + 1, 1, "unknown record kind", source))
    }
  }
  {
    problem: {
      people,
      shifts,
      existing: assignments,
      minimum_rest_minutes: minimum_rest,
      fairness_weight,
      preference_weight,
      disruption_weight,
    },
    issues,
  }
}

///|
pub fn render_roster(problem : ScheduleProblem) -> String {
  let mut text = "# ShiftWeave roster v1\n"
  text = text + "rule|minimum_rest|\{problem.minimum_rest_minutes}\n"
  text = text + "rule|fairness_weight|\{problem.fairness_weight}\n"
  text = text + "rule|preference_weight|\{problem.preference_weight}\n"
  text = text + "rule|disruption_weight|\{problem.disruption_weight}\n"
  for person in problem.people {
    text = text +
      "person|\{person.id}|\{person.name}|\{person.target_minutes}|\{person.maximum_minutes}\n"
    for skill in person.skills {
      text = text + "skill|\{person.id}|\{skill.name}|\{skill.level}\n"
    }
    for span in person.availability {
      text = text + "available|\{person.id}|\{span.start}|\{span.end}\n"
    }
    for pref in person.preferences {
      text = text + "prefer|\{person.id}|\{pref.shift_id}|\{pref.weight}\n"
    }
  }
  for shift in problem.shifts {
    text = text +
      "shift|\{shift.id}|\{shift.label}|\{shift.time.start}|\{shift.time.end}|\{shift.required_people}|\{shift.priority}|\{shift.group}\n"
    for requirement in shift.requirements {
      text = text +
        "require|\{shift.id}|\{requirement.skill}|\{requirement.minimum_level}\n"
    }
  }
  for assignment in problem.existing {
    text = text +
      "assign|\{assignment.shift_id}|\{assignment.slot}|\{assignment.person_id}|\{assignment.locked}\n"
  }
  text
}