///|
/// Per-file fingerprint: (mtime sec, mtime nsec). nsec is essential — many
/// filesystems give nanosecond-resolution mtime, so successive writes within
/// the same second still differ. Falling back to sec-only would silently
/// drop those Modified events.
priv struct Fingerprint {
  sec : Int64
  nsec : Int
} derive(Eq)

///|
priv struct PollingState {
  roots : Array[String]
  interval_ms : Int
  exclude : (String) -> Bool
  mut snapshot : Map[String, Fingerprint]
}

///|
async fn polling_start(
  roots : Array[String],
  interval_ms : Int,
  exclude : (String) -> Bool,
) -> PollingState {
  let initial = polling_snapshot(roots, exclude)
  PollingState::{ roots, interval_ms, exclude, snapshot: initial }
}

///|
async fn polling_next(state : PollingState) -> Array[Event] {
  @async.sleep(state.interval_ms)
  let fresh = polling_snapshot(state.roots, state.exclude)
  let events = polling_diff(state.snapshot, fresh)
  state.snapshot = fresh
  events
}

///|
async fn polling_snapshot(
  roots : Array[String],
  exclude : (String) -> Bool,
) -> Map[String, Fingerprint] {
  let snap : Map[String, Fingerprint] = Map([])
  for root in roots {
    if !@xfs.exists(root) {
      continue
    }
    @xfs.walk(root, max_concurrency=4, exclude~, async fn(dir, names) {
      for name in names {
        let full = dir + "/" + name
        if exclude(full) {
          continue
        }
        let k = try @xfs.kind(full) catch {
          err => Err(err)
        } noraise {
          value => Ok(value)
        }
        match k {
          Ok(Regular) => {
            let m = try @xfs.mtime(full) catch {
              err => Err(err)
            } noraise {
              value => Ok(value)
            }
            match m {
              Ok((sec, nsec)) => snap[full] = Fingerprint::{ sec, nsec }
              Err(_) => ()
            }
          }
          _ => ()
        }
      }
    })
  }
  snap
}

///|
fn polling_diff(
  old : Map[String, Fingerprint],
  fresh : Map[String, Fingerprint],
) -> Array[Event] {
  let events : Array[Event] = []
  for path, fp in fresh {
    match old.get(path) {
      None => events.push(Event::{ path, kind: Created })
      Some(prev) =>
        if prev != fp {
          events.push(Event::{ path, kind: Modified })
        }
    }
  }
  for path, _ in old {
    if !fresh.contains(path) {
      events.push(Event::{ path, kind: Removed })
    }
  }
  events
}