///|
/// An explicitly maintained stop. min_change is seconds before reboarding.
pub(all) struct Stop {
  id : String
  name : String
  min_change : Int32
  accessible : Bool
} derive(Eq, ToJson)

///|
/// Times are service-day seconds, including hours beyond 24.
pub(all) struct Call {
  stop : String
  arrival : Int32
  departure : Int32
  pickup : Bool
  dropoff : Bool
} derive(Eq, ToJson)

///|
pub(all) struct Trip {
  id : String
  calls : Array[Call]
  accessible : Bool
  cancelled : Bool
  delay : Int32
} derive(Eq, ToJson)

///|
/// Directed walking link; positive duration makes its resource use explicit.
pub(all) struct Footpath {
  id : String
  from : String
  to : String
  duration : Int32
  accessible : Bool
} derive(Eq, ToJson)

///|
/// Functions validate this caller-owned value and do not mutate it.
pub(all) struct Timetable {
  name : String
  stops : Array[Stop]
  trips : Array[Trip]
  footpaths : Array[Footpath]
} derive(Eq, ToJson)

///|
pub(all) struct Query {
  from : String
  to : String
  departure : Int32
  latest : Int32
  max_boardings : Int32
  max_walk : Int32
  accessible : Bool
  max_labels : Int32
  max_work : Int32
} derive(Eq, ToJson)

///|
/// A ride may pass intermediate calls without another boarding.
pub(all) struct Leg {
  kind : String
  id : String
  from : String
  to : String
  departure : Int32
  arrival : Int32
  board_index : Int32
  alight_index : Int32
} derive(Eq, ToJson)

///|
pub(all) struct Journey {
  arrival : Int32
  boardings : Int32
  walked : Int32
  legs : Array[Leg]
} derive(Eq, ToJson)

///|
pub(all) struct Report {
  schema_version : Int32
  timetable : String
  query : Query
  status : String
  complete : Bool
  journeys : Array[Journey]
  labels : Int32
  work : Int32
} derive(ToJson)

///|
pub(all) suberror PlanError {
  PlanError(String)
} derive(Eq)

///|
fn[T] fail(message : String) -> T raise PlanError {
  raise PlanError(message)
}

///|
pub fn query(
  from : String,
  to : String,
  departure : Int32,
  latest~ : Int32 = 259199,
  max_boardings~ : Int32 = 4,
  max_walk~ : Int32 = 1800,
  accessible~ : Bool = false,
  max_labels~ : Int32 = 100000,
  max_work~ : Int32 = 2000000,
) -> Query {
  {
    from,
    to,
    departure,
    latest,
    max_boardings,
    max_walk,
    accessible,
    max_labels,
    max_work,
  }
}

///|
fn identifier(s : String) -> Unit raise PlanError {
  if s.length() < 1 || s.length() > 128 {
    fail("IDs must contain 1..128 UTF-16 units")
  }
}

///|
/// Validate all references, event chronology and resource bounds up front.
pub fn validate(t : Timetable) -> Unit raise PlanError {
  if t.name.length() > 256 ||
    t.stops.length() < 1 ||
    t.stops.length() > 512 ||
    t.trips.length() > 2048 ||
    t.footpaths.length() > 4096 {
    fail("timetable capacity exceeded")
  }
  let stops : Map[String, Bool] = Map([])
  for s in t.stops {
    identifier(s.id)
    if s.name.length() > 256 || s.min_change < 0 || s.min_change > 86400 {
      fail("invalid stop attributes")
    }
    if stops.contains(s.id) {
      fail("duplicate stop ID")
    }
    stops[s.id] = true
  }
  let trips : Map[String, Bool] = Map([])
  let mut total = 0
  for trip in t.trips {
    identifier(trip.id)
    if trips.contains(trip.id) {
      fail("duplicate trip ID")
    }
    trips[trip.id] = true
    if trip.calls.length() < 2 ||
      trip.calls.length() > 128 ||
      trip.delay < 0 ||
      trip.delay > 86400 {
      fail("invalid trip size or delay")
    }
    total = total + trip.calls.length()
    if total > 20000 {
      fail("at most 20000 stop calls")
    }
    let mut previous = 0
    for c in trip.calls {
      if !stops.contains(c.stop) {
        fail("call references unknown stop")
      }
      if c.arrival < previous ||
        c.arrival < 0 ||
        c.departure < c.arrival ||
        c.departure.to_int64() + trip.delay.to_int64() > 259199 {
        fail("invalid or overflowing trip chronology")
      }
      previous = c.departure
    }
  }
  let links : Map[String, Bool] = Map([])
  for f in t.footpaths {
    identifier(f.id)
    if links.contains(f.id) {
      fail("duplicate footpath ID")
    }
    links[f.id] = true
    if !stops.contains(f.from) ||
      !stops.contains(f.to) ||
      f.duration < 1 ||
      f.duration > 86400 {
      fail("invalid footpath")
    }
  }
}

///|
pub fn validate_query(t : Timetable, q : Query) -> Unit raise PlanError {
  validate(t)
  if !t.stops.any(s => s.id == q.from) || !t.stops.any(s => s.id == q.to) {
    fail("query references unknown stop")
  }
  if q.departure < 0 ||
    q.latest < q.departure ||
    q.latest > 259199 ||
    q.max_boardings < 0 ||
    q.max_boardings > 8 ||
    q.max_walk < 0 ||
    q.max_walk > 86400 ||
    q.max_labels < 1 ||
    q.max_labels > 100000 ||
    q.max_work < 1 ||
    q.max_work > 2000000 {
    fail("invalid query bounds")
  }
}