///|
priv struct DirEntry {
  path : String
  mut children : Array[String]
}

///|
priv struct PendingTimedEvent {
  event : Event
  created_at : UInt64
}

///|
priv struct PendingWrite {
  mut event : Event
  mut stats : FileStats
  mut stable_since : UInt64
  mut last_checked : UInt64
}

///|
priv struct WatchState {
  options : Options
  dirs : Array[DirEntry]
  root_paths : Array[String]
  snapshots : Map[String, Array[SnapshotEntry]]
  handles : Array[BackendHandle]
  pending_events : Array[Event]
  pending_unlinks : Array[PendingTimedEvent]
  pending_writes : Array[PendingWrite]
  throttled_changes : Array[String]
  mut closed : Bool
  mut ready : Bool
}

///|
fn WatchState::new(options : Options) -> WatchState {
  {
    options,
    dirs: [],
    root_paths: [],
    snapshots: Map::new(),
    handles: [],
    pending_events: [],
    pending_unlinks: [],
    pending_writes: [],
    throttled_changes: [],
    closed: false,
    ready: false,
  }
}

///|
fn WatchState::dir_index(self : WatchState, dir : String) -> Int? {
  for index, entry in self.dirs {
    if entry.path == dir {
      break Some(index)
    }
  } nobreak {
    None
  }
}

///|
fn WatchState::ensure_dir(self : WatchState, dir : String) -> Int {
  match self.dir_index(dir) {
    Some(index) => index
    None => {
      self.dirs.push({ path: dir, children: [] })
      self.dirs.length() - 1
    }
  }
}

///|
fn WatchState::add_child(
  self : WatchState,
  dir : String,
  child : String,
) -> Unit {
  let index = self.ensure_dir(dir)
  let entry = self.dirs[index]
  if !entry.children.contains(child) {
    entry.children.push(child)
    entry.children.sort()
  }
}

///|
fn WatchState::remove_child(
  self : WatchState,
  dir : String,
  child : String,
) -> Unit {
  match self.dir_index(dir) {
    Some(index) => {
      let entry = self.dirs[index]
      entry.children = entry.children.filter(item => item != child)
      if entry.children.is_empty() {
        self.dirs.remove(index) |> ignore
      }
    }
    None => ()
  }
}

///|
fn WatchState::get_watched(self : WatchState) -> Map[String, Array[String]] {
  let watched = Map::new()
  for entry in self.dirs {
    watched[entry.path] = entry.children.copy()
  }
  watched
}

///|
fn WatchState::remember_root(self : WatchState, root : String) -> Unit {
  if !self.root_paths.contains(root) {
    self.root_paths.push(root)
  }
}

///|
fn WatchState::set_snapshot(
  self : WatchState,
  root : String,
  entries : Array[SnapshotEntry],
) -> Unit {
  self.snapshots[root] = entries
}

///|
fn WatchState::snapshot_for(
  self : WatchState,
  root : String,
) -> Array[SnapshotEntry] {
  match self.snapshots.get(root) {
    Some(entries) => entries
    None => []
  }
}

///|
fn WatchState::clear(self : WatchState) -> Unit {
  self.dirs.clear()
  self.root_paths.clear()
  self.snapshots.clear()
  self.handles.clear()
  self.pending_events.clear()
  self.pending_unlinks.clear()
  self.pending_writes.clear()
  self.throttled_changes.clear()
}

///|
fn WatchState::pending_unlink_index(self : WatchState, path : String) -> Int? {
  for index, pending in self.pending_unlinks {
    if pending.event.path == path {
      break Some(index)
    }
  } nobreak {
    None
  }
}

///|
fn millis_to_uint64(millis : Int) -> UInt64 {
  if millis <= 0 {
    (0).to_uint64()
  } else {
    millis.reinterpret_as_uint().to_uint64()
  }
}

///|
fn atomic_window_millis(option : AtomicOption) -> Int {
  match option {
    AtomicOff => 0
    AtomicDefault => 100
    AtomicWindow(window) => window
  }
}

///|
fn await_write_threshold_millis(option : AwaitWriteFinish) -> Int {
  match option {
    AwaitWriteFinishOff => 0
    AwaitWriteFinishOn(config) => config.stability_threshold
  }
}

///|
fn timestamp_expired(started_at : UInt64, window_millis : Int) -> Bool {
  @env.now() >= started_at + millis_to_uint64(window_millis)
}

///|
fn WatchState::normalize_atomic(
  self : WatchState,
  event : Event,
) -> Array[Event] {
  match self.options.atomic_option {
    AtomicOff => [event]
    AtomicDefault | AtomicWindow(_) =>
      if event.name == Unlink {
        if is_dot_swap_file(event.path) {
          return []
        }
        self.pending_unlinks.push({ event, created_at: @env.now() })
        []
      } else if event.name == Add {
        match self.pending_unlink_index(event.path) {
          Some(index) => {
            let pending = self.pending_unlinks.remove(index)
            if timestamp_expired(
                pending.created_at,
                atomic_window_millis(self.options.atomic_option),
              ) {
              [pending.event, event]
            } else {
              [Event::{ ..event, name: Change }]
            }
          }
          None => [event]
        }
      } else {
        [event]
      }
  }
}

///|
fn WatchState::flush_expired_pending_unlinks(self : WatchState) -> Array[Event] {
  let events : Array[Event] = []
  let window = atomic_window_millis(self.options.atomic_option)
  let mut index = 0
  while index < self.pending_unlinks.length() {
    let pending = self.pending_unlinks[index]
    if timestamp_expired(pending.created_at, window) {
      events.push(pending.event)
      self.pending_unlinks.remove(index) |> ignore
    } else {
      index += 1
    }
  }
  events
}

///|
fn WatchState::allow_event(
  self : WatchState,
  name : EventName,
  path : String,
) -> Bool {
  if name != Change {
    true
  } else if self.throttled_changes.contains(path) {
    false
  } else {
    self.throttled_changes.push(path)
    true
  }
}

///|
fn WatchState::clear_throttles(self : WatchState) -> Unit {
  self.throttled_changes.clear()
}

///|
fn WatchState::pending_write_index(self : WatchState, path : String) -> Int? {
  for index, pending in self.pending_writes {
    if pending.event.path == path {
      break Some(index)
    }
  } nobreak {
    None
  }
}

///|
fn event_needs_write_stability(event : Event) -> Bool {
  event.name == Add || event.name == Change
}

///|
fn WatchState::normalize_await_write(
  self : WatchState,
  event : Event,
) -> Array[Event] {
  match self.options.await_write_finish {
    AwaitWriteFinishOff => [event]
    AwaitWriteFinishOn(_) =>
      if !event_needs_write_stability(event) {
        [event]
      } else {
        match event.stats {
          None => []
          Some(stats) => {
            let now = @env.now()
            match self.pending_write_index(event.path) {
              Some(index) => {
                let pending = self.pending_writes[index]
                let poll_interval = match self.options.await_write_finish {
                  AwaitWriteFinishOn(config) => config.poll_interval
                  AwaitWriteFinishOff => 0
                }
                if !timestamp_expired(pending.last_checked, poll_interval) {
                  []
                } else {
                  pending.last_checked = now
                  pending.event = event
                  if pending.stats == stats {
                    if timestamp_expired(
                        pending.stable_since,
                        await_write_threshold_millis(
                          self.options.await_write_finish,
                        ),
                      ) {
                      self.pending_writes.remove(index) |> ignore
                      [event]
                    } else {
                      []
                    }
                  } else {
                    pending.stats = stats
                    pending.stable_since = now
                    []
                  }
                }
              }
              None => {
                self.pending_writes.push({
                  event,
                  stats,
                  stable_since: now,
                  last_checked: now,
                })
                []
              }
            }
          }
        }
      }
  }
}

///|
fn WatchState::flush_stable_writes(self : WatchState) -> Array[Event] {
  let events : Array[Event] = []
  let threshold = await_write_threshold_millis(self.options.await_write_finish)
  let mut index = 0
  while index < self.pending_writes.length() {
    let pending = self.pending_writes[index]
    if timestamp_expired(pending.stable_since, threshold) {
      events.push(pending.event)
      self.pending_writes.remove(index) |> ignore
    } else {
      index += 1
    }
  }
  events
}