///|
/// Errors returned when a benchmark instance is not structurally valid.
pub(all) suberror ParseError {
  MissingHeader(String)
  InvalidHeader(String)
  InvalidNumber(String)
  MissingSection(String)
  InvalidRecord(String)
  UnsupportedFormat(String)
} derive(Debug, Eq, ToJson)

///|
fn cleaned_lines(text : String) -> Array[String] {
  let result : Array[String] = []
  for line in text.split("\n") {
    let cleaned = line.trim().to_owned()
    if !cleaned.is_empty() {
      result.push(cleaned)
    }
  }
  result
}

///|
fn words(line : String) -> Array[String] {
  let result : Array[String] = []
  for word in line.split(" ") {
    let cleaned = word.trim()
    if !cleaned.is_empty() {
      result.push(cleaned.to_owned())
    }
  }
  result
}

///|
fn find_section(lines : Array[String], name : String) -> Int? {
  for index, line in lines {
    if line.trim().equal_to_string(name) {
      return Some(index)
    }
  }
  None
}

///|
fn header_value(lines : Array[String], name : String) -> String? {
  for line in lines {
    match line.split_once(":") {
      None => ()
      Some((key, value)) =>
        if key.trim().equal_to_string(name) {
          return Some(value.trim().to_owned())
        }
    }
  }
  None
}

///|
fn required_header(
  lines : Array[String],
  name : String,
) -> String raise ParseError {
  match header_value(lines, name) {
    Some(value) => value
    None => raise ParseError::MissingHeader(name)
  }
}

///|
fn parse_int(token : String) -> Int raise ParseError {
  @strconv.from_str(token[:]) catch {
    _ => raise ParseError::InvalidNumber(token)
  }
}

///|
fn parse_double(token : String) -> Double raise ParseError {
  @strconv.from_str(token[:]) catch {
    _ => raise ParseError::InvalidNumber(token)
  }
}

///|
fn parse_metric(value : String) -> @core.DistanceMetric raise ParseError {
  match value.trim() {
    "EUC_2D" => @core.DistanceMetric::RoundedEuclidean
    "CEIL_2D" => @core.DistanceMetric::Euclidean
    "MAN_2D" => @core.DistanceMetric::Manhattan
    "EXPLICIT" => raise ParseError::UnsupportedFormat(value)
    other => raise ParseError::UnsupportedFormat(other.to_owned())
  }
}

///|
priv struct CoordinateRecord {
  id : Int
  x : Double
  y : Double
}

///|
priv struct DemandRecord {
  id : Int
  demand : Double
}

///|
fn coordinate_records(
  lines : Array[String],
  start : Int,
  end : Int,
) -> Array[CoordinateRecord] raise ParseError {
  let records : Array[CoordinateRecord] = []
  for index in (start + 1).. Array[DemandRecord] raise ParseError {
  let records : Array[DemandRecord] = []
  for index in (start + 1).. Int raise ParseError {
  for index in (start + 1).. CoordinateRecord? {
  for record in records {
    if record.id == id {
      return Some(record)
    }
  }
  None
}

///|
fn demand_by_id(records : Array[DemandRecord], id : Int) -> Double? {
  for record in records {
    if record.id == id {
      return Some(record.demand)
    }
  }
  None
}

///|
fn optional_int_header(
  lines : Array[String],
  names : Array[String],
) -> Int? raise ParseError {
  for name in names {
    match header_value(lines, name) {
      None => ()
      Some(value) => return Some(parse_int(value))
    }
  }
  None
}

///|
/// Parses the CVRPLIB/TSPLIB CVRP text format.
pub fn parse_cvrp(text : String) -> @core.VrpInstance raise ParseError {
  let lines = cleaned_lines(text)
  let name = required_header(lines, "NAME")
  let kind = required_header(lines, "TYPE")
  if kind != "CVRP" {
    raise ParseError::UnsupportedFormat(kind)
  }
  let dimension = parse_int(required_header(lines, "DIMENSION"))
  let capacity = parse_double(required_header(lines, "CAPACITY"))
  let metric = parse_metric(required_header(lines, "EDGE_WEIGHT_TYPE"))
  let coordinate_start = match find_section(lines, "NODE_COORD_SECTION") {
    Some(index) => index
    None => raise ParseError::MissingSection("NODE_COORD_SECTION")
  }
  let demand_start = match find_section(lines, "DEMAND_SECTION") {
    Some(index) => index
    None => raise ParseError::MissingSection("DEMAND_SECTION")
  }
  let depot_start = match find_section(lines, "DEPOT_SECTION") {
    Some(index) => index
    None => raise ParseError::MissingSection("DEPOT_SECTION")
  }
  if !(coordinate_start < demand_start && demand_start < depot_start) {
    raise ParseError::InvalidRecord("section order")
  }
  let coordinates = coordinate_records(lines, coordinate_start, demand_start)
  let demands = demand_records(lines, demand_start, depot_start)
  if coordinates.length() != dimension || demands.length() != dimension {
    raise ParseError::InvalidRecord("record count does not match DIMENSION")
  }
  let depot = depot_id(lines, depot_start, lines.length())
  let depot_record = match coordinate_by_id(coordinates, depot) {
    Some(record) => record
    None => raise ParseError::InvalidRecord("depot coordinate")
  }
  let customers : Array[@core.Customer] = []
  for coordinate in coordinates {
    if coordinate.id == depot {
      continue
    }
    let demand = match demand_by_id(demands, coordinate.id) {
      Some(value) => value
      None => raise ParseError::InvalidRecord("customer demand")
    }
    customers.push(
      @core.Customer::new(coordinate.id, coordinate.x, coordinate.y, demand),
    )
  }
  let fleet_size = match optional_int_header(lines, ["TRUCKS", "VEHICLES"]) {
    Some(value) => value
    None => dimension.max(1)
  }
  @core.make_instance_with_metric(
    name,
    @core.ProblemVariant::CVRP,
    @core.Depot::new(depot_record.x, depot_record.y),
    @core.VehicleProfile::new(capacity, fleet_size~),
    customers,
    metric,
  )
}

///|
fn solomon_data_start(lines : Array[String]) -> Int raise ParseError {
  for index, line in lines {
    if line
      .trim()
      .equal_to_string(
        "CUST NO. XCOORD. YCOORD. DEMAND READY TIME DUE DATE SERVICE TIME",
      ) {
      return index + 1
    }
  }
  raise ParseError::MissingSection("CUSTOMER")
}

///|
fn solomon_vehicle_data(
  lines : Array[String],
) -> (Int, Double) raise ParseError {
  for index, line in lines {
    if line.trim().equal_to_string("NUMBER CAPACITY") &&
      index + 1 < lines.length() {
      let fields = words(lines[index + 1])
      if fields.length() < 2 {
        raise ParseError::InvalidRecord(lines[index + 1])
      }
      return (parse_int(fields[0]), parse_double(fields[1]))
    }
  }
  raise ParseError::MissingSection("VEHICLE")
}

///|
/// Parses the Solomon 100-customer style VRPTW format.
pub fn parse_solomon(text : String) -> @core.VrpInstance raise ParseError {
  let lines = cleaned_lines(text)
  if lines.is_empty() {
    raise ParseError::InvalidRecord("empty Solomon file")
  }
  let name = lines[0]
  let (fleet_size, capacity) = solomon_vehicle_data(lines)
  let start = solomon_data_start(lines)
  let mut depot : @core.Depot? = None
  let customers : Array[@core.Customer] = []
  for index in start.. value
    None => raise ParseError::InvalidRecord("missing Solomon depot")
  }
  @core.make_instance_with_metric(
    name,
    @core.ProblemVariant::VRPTW,
    depot,
    @core.VehicleProfile::new(
      capacity,
      fleet_size~,
      max_route_duration=depot.due_time(),
    ),
    customers,
    @core.DistanceMetric::Euclidean,
  )
}

///|
/// Parses either a CVRP or Solomon document based on its identifying headers.
pub fn parse_instance(text : String) -> @core.VrpInstance raise ParseError {
  let lines = cleaned_lines(text)
  if header_value(lines, "TYPE") is Some("CVRP") {
    parse_cvrp(text)
  } else {
    parse_solomon(text)
  }
}