///| Week starts on Monday.  The type deliberately models only a repeating

///| seven-day work pattern; dates, time zones and holiday rules remain inputs

///|
/// to the host application.
pub enum Weekday {
  Monday
  Tuesday
  Wednesday
  Thursday
  Friday
  Saturday
  Sunday
} derive(Eq, Debug)

///|
pub fn weekdays() -> Array[Weekday] {
  [Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday]
}

///|
pub fn Weekday::index(self : Weekday) -> Int {
  match self {
    Monday => 0
    Tuesday => 1
    Wednesday => 2
    Thursday => 3
    Friday => 4
    Saturday => 5
    Sunday => 6
  }
}

///|
pub fn Weekday::render(self : Weekday) -> String {
  match self {
    Monday => "monday"
    Tuesday => "tuesday"
    Wednesday => "wednesday"
    Thursday => "thursday"
    Friday => "friday"
    Saturday => "saturday"
    Sunday => "sunday"
  }
}

///|
/// A within-day availability window expressed in minutes after midnight.
pub struct WeeklyWindow {
  day : Weekday
  window : Interval
} derive(Debug)

///|
pub enum WeeklyError {
  WindowOutsideDay(Interval)
  HorizonBeforeWeekStart
  NonPositiveWeekCount(Int)
} derive(Eq, Debug)

///|
pub enum WeeklyResourceError {
  Weekly(WeeklyError)
  Resource(ResourceError)
} derive(Eq, Debug)

///|
pub fn WeeklyWindow::new(
  day : Weekday,
  start_minute : Int,
  end_minute : Int,
) -> Result[WeeklyWindow, WeeklyError] {
  match Interval::new(start_minute, end_minute) {
    Err(_) => Err(WindowOutsideDay({ start: start_minute, end: end_minute }))
    Ok(window) =>
      if start_minute < 0 || end_minute > 1440 {
        Err(WindowOutsideDay(window))
      } else {
        Ok({ day, window })
      }
  }
}

///|
pub fn WeeklyWindow::day(self : WeeklyWindow) -> Weekday {
  self.day
}

///|
pub fn WeeklyWindow::window(self : WeeklyWindow) -> Interval {
  self.window
}

///|
pub fn WeeklyWindow::render(self : WeeklyWindow) -> String {
  self.day.render() + " " + self.window.render()
}

///| A normalized weekly work template.  Overlapping windows on a day are

///|
/// joined so expansion never produces duplicate availability.
pub struct WeeklyTemplate {
  windows : Array[WeeklyWindow]
} derive(Debug)

///|
pub fn WeeklyTemplate::new(windows : Array[WeeklyWindow]) -> WeeklyTemplate {
  let normalized : Array[WeeklyWindow] = []
  for day in weekdays() {
    let day_ranges : Array[Interval] = []
    for window in windows {
      if window.day() == day {
        day_ranges.push(window.window())
      }
    }
    for range in IntervalSet::from_ranges(day_ranges).ranges() {
      normalized.push({ day, window: range })
    }
  }
  { windows: normalized }
}

///|
pub fn WeeklyTemplate::windows(self : WeeklyTemplate) -> Array[WeeklyWindow] {
  self.windows.copy()
}

///|
pub fn WeeklyTemplate::windows_for(
  self : WeeklyTemplate,
  day : Weekday,
) -> IntervalSet {
  let ranges : Array[Interval] = []
  for window in self.windows {
    if window.day() == day {
      ranges.push(window.window())
    }
  }
  IntervalSet::from_ranges(ranges)
}

///|
pub fn WeeklyTemplate::is_open(
  self : WeeklyTemplate,
  day : Weekday,
  minute : Int,
) -> Bool {
  minute >= 0 && minute < 1440 && self.windows_for(day).contains(minute)
}

///|
fn expand_week(template : WeeklyTemplate, week_start : Tick) -> IntervalSet {
  let ranges : Array[Interval] = []
  for window in template.windows {
    let offset = week_start + window.day().index() * 1440
    ranges.push(window.window().shift(offset))
  }
  IntervalSet::from_ranges(ranges)
}

///|
/// Expand exactly `week_count` repeating weeks.  `week_start` is the Monday

///|
/// 00:00 tick chosen by the embedding application.
pub fn WeeklyTemplate::expand_weeks(
  self : WeeklyTemplate,
  week_start : Tick,
  week_count : Int,
) -> Result[IntervalSet, WeeklyError] {
  if week_count <= 0 {
    return Err(NonPositiveWeekCount(week_count))
  }
  let mut output = IntervalSet::empty()
  for week in 0.. Result[IntervalSet, WeeklyError] {
  if horizon.start() < week_start {
    return Err(HorizonBeforeWeekStart)
  }
  let first_week = (horizon.start() - week_start) / 10080
  let last_week = (horizon.end() - 1 - week_start) / 10080
  let mut output = IntervalSet::empty()
  for week in first_week..<(last_week + 1) {
    output = output.union(expand_week(self, week_start + week * 10080))
  }
  Ok(output.within(horizon))
}

///|
/// Construct a bookable resource directly from a weekly template and a finite

///| planning horizon.  Applications may then add holidays or maintenance via

///|
/// `ResourceCalendar::with_blocked`.
pub fn resource_from_weekly_template(
  id : String,
  capacity : Int,
  template : WeeklyTemplate,
  week_start : Tick,
  horizon : Interval,
) -> Result[ResourceCalendar, WeeklyResourceError] {
  let available = match template.availability_within(week_start, horizon) {
    Ok(value) => value
    Err(error) => return Err(Weekly(error))
  }
  match ResourceCalendar::new(id, capacity, available) {
    Ok(value) => Ok(value)
    Err(error) => Err(Resource(error))
  }
}