// Copyright 2026 PaiGack
// Licensed under the Apache License, Version 2.0.
// Ported from jlaffaye/ftp (ISC License), see LICENSE-THIRD-PARTY.

// Layer: IO — uses the `moonbitlang/async` stack from the root moon.pkg.

///|
/// A depth-first directory tree walker, a faithful port of upstream
/// `ftp.Walker`.
///
/// The API contract matters here: `next()` returns `false` on a listing error
/// instead of raising, and the error is readable through `err()`.
pub struct Walker {
  /// The client being walked; the walker is built on top of `client`.
  client : FTPClient
  /// The entry currently being visited.
  mut cur : Entry?
  /// Path of `cur`, always as produced by `pathutil.join`.
  mut cur_path : String
  /// Pending entries; the array tail is the stack top (LIFO).
  mut stack : Array[(Entry, String)]
  /// Whether the current directory should still be expanded.
  mut descend : Bool
  /// The root path passed to `walk`, with a trailing slash.
  root : String
  /// The first failure encountered while listing, if any.
  mut walker_err : Error?
}

///|
/// Walk the directory tree rooted at `root`.
pub fn walk(client : FTPClient, root : String) -> Walker {
  let normalized = if root.has_suffix("/") { root } else { root + "/" }
  {
    client,
    cur: None,
    cur_path: "",
    stack: [],
    descend: true,
    root: normalized,
    walker_err: None,
  }
}

///|
/// The entries currently waiting on the stack, oldest first.
pub fn Walker::entries(self : Walker) -> Array[(Entry, String)] {
  self.stack
}

///|
/// Replace the pending entry stack.
pub fn Walker::set_stack(self : Walker, stack : Array[(Entry, String)]) -> Unit {
  self.stack = stack
}

///|
/// The number of entries still to visit.
pub fn Walker::pending(self : Walker) -> Int {
  self.stack.length()
}

///|
/// Push a child entry onto the walk stack.
pub fn Walker::push(self : Walker, entry : Entry, path : String) -> Unit {
  self.set_stack(append_entry(self.entries(), (entry, path)))
}

///|
/// Drop and return the most recently pushed entry, if any.
pub fn Walker::pop(self : Walker) -> (Entry, String)? {
  guard self.pending() > 0 else { return None }
  let top = self.entries()[self.pending() - 1]
  self.set_stack(drop_last(self.entries()))
  Some(top)
}

///|
/// Return a copy of `entries` with `item` appended.
fn append_entry(
  entries : Array[(Entry, String)],
  item : (Entry, String),
) -> Array[(Entry, String)] {
  let grown : Array[(Entry, String)] = []
  for entry in entries {
    grown.push(entry)
  }
  grown.push(item)
  grown
}

///|
/// Advance to the next entry, returning `false` when the walk is finished or
/// when a listing failed.
///
/// The five steps are exactly upstream's:
/// 1. initialise `cur` to a synthetic folder entry for the root,
/// 2. list `cur` when `descend` is set, recording (not raising) failures,
/// 3. push every child except `.` and `..`,
/// 4. stop when the stack is empty,
/// 5. pop the stack top, reset `descend`, return `true`.
pub async fn Walker::next(self : Walker) -> Bool {
  if self.cur is None {
    self.cur = Some(make_entry("", EntryType::Folder))
    self.cur_path = self.root
    self.descend = true
  }
  if self.descend {
    let path = self.path()
    let entries = list(self.client, path) catch {
      err => {
        self.walker_err = Some(err)
        return false
      }
    }
    for entry in entries {
      if entry.name == "." || entry.name == ".." {
        continue
      }
      self.push(entry, join(path, entry.name))
    }
  }
  match self.pop() {
    None => false
    Some(top) => {
      self.cur = Some(top.0)
      self.cur_path = top.1
      self.descend = true
      true
    }
  }
}

///|
/// Do not descend into the directory returned by the last `next()`.
pub fn Walker::skip_dir(self : Walker) -> Unit {
  self.descend = false
}

///|
/// The error that stopped the walk, if any.
pub fn Walker::err(self : Walker) -> Error? {
  self.walker_err
}

///|
/// The entry currently being visited.
pub fn Walker::stat(self : Walker) -> Entry? {
  self.cur
}

///|
/// The path of the current entry.
pub fn Walker::path(self : Walker) -> String {
  if self.cur is None {
    self.root
  } else {
    self.cur_path
  }
}

///|
/// Return a copy of `entries` without its last element, which is how the walk
/// stack is popped (LIFO on the array tail).
fn drop_last(entries : Array[(Entry, String)]) -> Array[(Entry, String)] {
  let kept : Array[(Entry, String)] = []
  for i = 0; i < entries.length() - 1; i = i + 1 {
    kept.push(entries[i])
  }
  kept
}