///|
/// Campaign scheduling, resource loading, and maintenance windows.
pub enum ScheduleKind {
  Production
  Cleaning
  Maintenance
  Inspection
  Changeover
}

///|
pub fn production_schedule() -> ScheduleKind {
  Production
}

///|
pub fn cleaning_schedule() -> ScheduleKind {
  Cleaning
}

///|
pub fn maintenance_schedule() -> ScheduleKind {
  Maintenance
}

///|
pub fn inspection_schedule() -> ScheduleKind {
  Inspection
}

///|
pub fn changeover_schedule() -> ScheduleKind {
  Changeover
}

///|
pub struct ScheduleBlock {
  id : String
  asset : String
  kind : ScheduleKind
  start_hour : Float
  duration_hours : Float
  priority : Int
  operator : String
}

///|
pub fn schedule_block(
  id : String,
  asset : String,
  kind : ScheduleKind,
  start_hour : Float,
  duration_hours : Float,
  priority : Int,
  operator : String,
) -> ScheduleBlock {
  { id, asset, kind, start_hour, duration_hours, priority, operator }
}

///|
pub fn ScheduleBlock::end_hour(self : ScheduleBlock) -> Float {
  self.start_hour + self.duration_hours
}

///|
pub fn ScheduleBlock::valid(self : ScheduleBlock) -> Bool {
  self.start_hour >= 0.0 && self.duration_hours > 0.0 && self.priority >= 0
}

///|
pub fn ScheduleBlock::overlaps(
  self : ScheduleBlock,
  other : ScheduleBlock,
) -> Bool {
  self.asset == other.asset &&
  self.start_hour < other.end_hour() &&
  other.start_hour < self.end_hour()
}

///|
pub struct ResourceCalendar {
  name : String
  capacity_hours : Float
  blocks : Array[ScheduleBlock]
}

///|
pub fn resource_calendar(
  name : String,
  capacity_hours : Float,
  blocks : Array[ScheduleBlock],
) -> ResourceCalendar {
  { name, capacity_hours, blocks }
}

///|
pub fn ResourceCalendar::loaded_hours(self : ResourceCalendar) -> Float {
  self.blocks.fold(init=0.0, fn(total, block) { total + block.duration_hours })
}

///|
pub fn ResourceCalendar::utilization(self : ResourceCalendar) -> Float {
  if self.capacity_hours == 0.0 {
    0.0
  } else {
    self.loaded_hours() / self.capacity_hours
  }
}

///|
pub fn ResourceCalendar::available(self : ResourceCalendar) -> Float {
  self.capacity_hours - self.loaded_hours()
}

///|
pub fn ResourceCalendar::valid(self : ResourceCalendar) -> Bool {
  self.capacity_hours >= 0.0 && self.blocks.all(fn(b) { b.valid() })
}

///|
pub fn ResourceCalendar::conflicts(self : ResourceCalendar) -> Array[String] {
  let result : Array[String] = []
  for i = 0; i < self.blocks.length(); i = i + 1 {
    for j = i + 1; j < self.blocks.length(); j = j + 1 {
      if self.blocks[i].overlaps(self.blocks[j]) {
        result.push(self.blocks[i].id + ":" + self.blocks[j].id)
      }
    }
  }
  result
}

///|
pub struct Shift {
  name : String
  start_hour : Float
  end_hour : Float
  crew : Int
}

///|
pub fn shift(
  name : String,
  start_hour : Float,
  end_hour : Float,
  crew : Int,
) -> Shift {
  { name, start_hour, end_hour, crew }
}

///|
pub fn Shift::hours(self : Shift) -> Float {
  if self.end_hour < self.start_hour {
    self.end_hour + 24.0 - self.start_hour
  } else {
    self.end_hour - self.start_hour
  }
}

///|
pub fn Shift::valid(self : Shift) -> Bool {
  self.crew > 0 && self.hours() > 0.0
}

///|
pub fn total_crew_hours(shifts : Array[Shift]) -> Float {
  shifts.fold(init=0.0, fn(v, s) { v + s.hours() * Float::from_int(s.crew) })
}

///|
pub struct MaintenanceWindow {
  asset : String
  due_hour : Float
  duration_hours : Float
  critical : Bool
  completed : Bool
}

///|
pub fn maintenance_window(
  asset : String,
  due_hour : Float,
  duration_hours : Float,
  critical : Bool,
  completed : Bool,
) -> MaintenanceWindow {
  { asset, due_hour, duration_hours, critical, completed }
}

///|
pub fn MaintenanceWindow::overdue(
  self : MaintenanceWindow,
  current_hour : Float,
) -> Bool {
  !self.completed && current_hour > self.due_hour
}

///|
pub fn MaintenanceWindow::risk(
  self : MaintenanceWindow,
  current_hour : Float,
) -> String {
  if self.overdue(current_hour) && self.critical {
    "critical"
  } else if self.overdue(current_hour) {
    "overdue"
  } else if self.completed {
    "complete"
  } else {
    "planned"
  }
}

///|
pub fn maintenance_windows_table(
  items : Array[MaintenanceWindow],
  current_hour : Float,
) -> ReportTable {
  let rows : Array[Array[String]] = []
  for item in items {
    rows.push([
      item.asset,
      "\{item.due_hour}",
      "\{item.duration_hours}",
      "\{item.critical}",
      item.risk(current_hour),
    ])
  }
  table(["asset", "due hour", "duration", "critical", "risk"], rows)
}

///|
pub fn earliest_available(
  calendar : ResourceCalendar,
  duration_hours : Float,
  from_hour : Float,
) -> Float {
  let mut cursor = from_hour
  let ordered = calendar.blocks.copy()
  ordered.sort_by(fn(a, b) {
    if a.start_hour < b.start_hour {
      -1
    } else if a.start_hour > b.start_hour {
      1
    } else {
      0
    }
  })
  for block in ordered {
    if cursor + duration_hours <= block.start_hour {
      return cursor
    }
    if cursor < block.end_hour() {
      cursor = block.end_hour()
    }
  }
  cursor
}

///|
pub fn schedule_load_gate(
  calendar : ResourceCalendar,
  maximum_utilization : Float,
) -> Bool {
  calendar.valid() &&
  calendar.conflicts().length() == 0 &&
  calendar.utilization() <= maximum_utilization
}

///|
pub fn schedule_table(blocks : Array[ScheduleBlock]) -> ReportTable {
  let rows : Array[Array[String]] = []
  for b in blocks {
    rows.push([
      b.id,
      b.asset,
      "\{b.start_hour}",
      "\{b.end_hour()}",
      "\{b.duration_hours}",
      "\{b.priority}",
      b.operator,
    ])
  }
  table(
    ["id", "asset", "start", "end", "duration", "priority", "operator"],
    rows,
  )
}