///|
priv struct SnapshotEntry {
  path : String
  kind : FileKind
  stats : FileStats?
}

///|
fn file_kind_from_readdirp(kind : @readdirp.FileKind) -> FileKind {
  match kind {
    @readdirp.File => File
    @readdirp.Directory => Directory
    @readdirp.Symlink => Symlink
    @readdirp.Other => Other
  }
}

///|
#borrow(path)
extern "C" fn native_modified_nanos(
  path : Bytes,
  follow_symlink : Int,
) -> Int64 = "chokidar_scanner_modified_nanos"

///|
#borrow(path)
extern "C" fn native_stat_valid(
  path : Bytes,
  follow_symlink : Int,
) -> Int = "chokidar_scanner_stat_valid"

///|
#borrow(path)
extern "C" fn native_stat_size(
  path : Bytes,
  follow_symlink : Int,
) -> Int64 = "chokidar_scanner_stat_size"

///|
#borrow(path)
extern "C" fn native_stat_mode(
  path : Bytes,
  follow_symlink : Int,
) -> Int = "chokidar_scanner_stat_mode"

///|
fn scanner_path_to_bytes(path : String) -> Bytes {
  scanner_string_to_utf8_bytes(path)
}

///|
fn scanner_string_to_utf8_bytes(value : String) -> Bytes {
  let byte_length = value
    .iter()
    .fold(init=0, (length, char) => {
      let code = char.to_int()
      if code < 0x80 {
        length + 1
      } else if code < 0x800 {
        length + 2
      } else if code < 0x10000 {
        length + 3
      } else {
        length + 4
      }
    })
  let bytes = FixedArray::make(byte_length, b'\x00')
  ignore(
    value
    .iter()
    .fold(init=0, (offset, char) => offset + bytes.set_utf8_char(offset, char)),
  )
  scanner_fixed_byte_array_to_bytes(bytes)
}

///|
fn scanner_fixed_byte_array_to_bytes(bytes : FixedArray[Byte]) -> Bytes = "%identity"

///|
fn modified_nanos_for(path : String, follow_symlink : Bool) -> Int64 {
  native_modified_nanos(
    scanner_path_to_bytes(path),
    if follow_symlink {
      1
    } else {
      0
    },
  )
}

///|
fn stats_from_readdirp(
  entry : @readdirp.EntryInfo,
  follow_symlink : Bool,
) -> FileStats? {
  match entry.stats {
    Some(value) =>
      Some({
        size: value.size,
        mode: value.mode,
        modified_nanos: modified_nanos_for(entry.full_path, follow_symlink),
      })
    None => None
  }
}

///|
fn readdirp_type() -> @readdirp.EntryType {
  @readdirp.FilesAndDirectories
}

///|
fn warnings_to_error(
  warnings : Array[@readdirp.ReaddirpWarning],
  options : Options,
) -> ChokidarError? {
  for warning in warnings {
    if warning.kind != @readdirp.RecursiveSymlink &&
      !(options.ignore_permission_errors &&
        warning.kind == @readdirp.PermissionDenied) {
      break Some(
        ChokidarError(
          "scan warning for " + warning.path + ": " + warning.message,
        ),
      )
    }
  } nobreak {
    None
  }
}

///|
fn compare_paths(left : String, right : String) -> Int {
  let left_bytes = scanner_string_to_utf8_bytes(left)
  let right_bytes = scanner_string_to_utf8_bytes(right)
  let limit = left_bytes.length().min(right_bytes.length())
  for index in 0.. right_byte {
      break 1
    }
  } nobreak {
    if left_bytes.length() < right_bytes.length() {
      -1
    } else if left_bytes.length() > right_bytes.length() {
      1
    } else {
      0
    }
  }
}

///|
fn compare_snapshot_paths(left : SnapshotEntry, right : SnapshotEntry) -> Int {
  compare_paths(left.path, right.path)
}

///|
#warnings("-unused_value")
fn scan_snapshot(
  root : String,
  options : Options,
) -> Array[SnapshotEntry] raise ChokidarError {
  let readdirp_options = @readdirp.Options::{
    type_: readdirp_type(),
    always_stat: true,
    lstat: !options.follow_symlinks,
    depth: options.depth,
    file_filter: entry => {
      let stats = stats_from_readdirp(entry, options.follow_symlinks)
      let readable = options.ignore_permission_errors ||
        stats is None ||
        (stats is Some(s) && (s.mode & 0x100) != 0)
      let matched_path = event_path_for(join_path(root, entry.path), options)
      readable &&
      !is_ignored(matched_path, stats~, options.ignored)
    },
    directory_filter: entry => {
      let matched_path = event_path_for(join_path(root, entry.path), options)
      !is_ignored(
        matched_path,
        stats=stats_from_readdirp(entry, options.follow_symlinks),
        options.ignored,
      )
    },
  }
  let report = @readdirp.readdirp_report(root, options=readdirp_options) catch {
    @readdirp.ReaddirpError(message) => raise ChokidarError(message)
  }
  match warnings_to_error(report.warnings, options) {
    Some(error) => raise error
    None => ()
  }
  let snapshot = report.entries.map(entry => SnapshotEntry::{
    path: entry.path,
    kind: file_kind_from_readdirp(entry.kind),
    stats: stats_from_readdirp(entry, options.follow_symlinks),
  })
  snapshot.sort_by(compare_snapshot_paths)
  snapshot
}

///|
fn snapshot_find(
  entries : Array[SnapshotEntry],
  path : String,
) -> SnapshotEntry? {
  for entry in entries {
    if entry.path == path {
      break Some(entry)
    }
  } nobreak {
    None
  }
}

///|
fn stats_changed(old_stats : FileStats?, new_stats : FileStats?) -> Bool {
  match (old_stats, new_stats) {
    (Some(old), Some(new)) =>
      old.size != new.size ||
      old.mode != new.mode ||
      old.modified_nanos != new.modified_nanos
    (None, Some(_)) => true
    (Some(_), None) => true
    (None, None) => false
  }
}

///|
fn event_name_for_add(kind : FileKind) -> EventName {
  match kind {
    Directory => AddDir
    File | Symlink | Other => Add
  }
}

///|
fn event_name_for_unlink(kind : FileKind) -> EventName {
  match kind {
    Directory => UnlinkDir
    File | Symlink | Other => Unlink
  }
}

///|
fn event_from_entry(name : EventName, entry : SnapshotEntry) -> Event {
  let stats = match name {
    Unlink | UnlinkDir => None
    Add | AddDir | Change | Ready | Raw | Error | All => entry.stats
  }
  { name, path: entry.path, stats, raw: None, error: None }
}

///|
#warnings("-unused_value")
fn diff_snapshots(
  old_entries : Array[SnapshotEntry],
  new_entries : Array[SnapshotEntry],
) -> Array[Event] {
  let events : Array[Event] = []
  let old_entries = old_entries.copy()
  let new_entries = new_entries.copy()
  old_entries.sort_by(compare_snapshot_paths)
  new_entries.sort_by(compare_snapshot_paths)
  for new_entry in new_entries {
    match snapshot_find(old_entries, new_entry.path) {
      Some(old_entry) =>
        if new_entry.kind != old_entry.kind {
          events.push(
            event_from_entry(event_name_for_unlink(old_entry.kind), old_entry),
          )
          events.push(
            event_from_entry(event_name_for_add(new_entry.kind), new_entry),
          )
        } else if new_entry.kind != Directory &&
          stats_changed(old_entry.stats, new_entry.stats) {
          events.push(event_from_entry(Change, new_entry))
        }
      None =>
        events.push(
          event_from_entry(event_name_for_add(new_entry.kind), new_entry),
        )
    }
  }
  for old_entry in old_entries {
    match snapshot_find(new_entries, old_entry.path) {
      Some(_) => ()
      None =>
        events.push(
          event_from_entry(event_name_for_unlink(old_entry.kind), old_entry),
        )
    }
  }
  events
}

///|
fn stat_for_path(path : String, follow_symlink : Bool) -> FileStats? {
  let follow = if follow_symlink { 1 } else { 0 }
  let bytes = scanner_path_to_bytes(path)
  if native_stat_valid(bytes, follow) == 0 {
    None
  } else {
    Some({
      size: native_stat_size(bytes, follow),
      mode: native_stat_mode(bytes, follow),
      modified_nanos: native_modified_nanos(bytes, follow),
    })
  }
}