//! Folder scanning — find all supported images in a directory, sorted.
//!
//! Sorting is lexicographic by file name (not natural sort). This matches
//! what `ls` does and what most users expect. If we want natural sort later
//! (so `img2.png` comes before `img10.png`), it's a one-line change here.

use std::path::{Path, PathBuf};

use crate::codec::FormatRegistry;

/// Scan a directory for supported image files.
/// Returns a sorted Vec of paths. Symlinks are followed (one level).
/// Hidden files (starting with `.`) are skipped.
pub fn scan_folder(dir: &Path, registry: &FormatRegistry) -> Vec<PathBuf> {
    // Guard: unreadable directory → empty result.
    let Ok(entries) = std::fs::read_dir(dir) else {
        return Vec::new();
    };

    let mut images: Vec<PathBuf> = entries
        .flatten()
        .map(|entry| entry.path())
        .filter(|path| {
            // Skip hidden files (Unix convention).
            let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
                return false;
            };
            if name.starts_with('.') {
                return false;
            }
            // Only files (or symlinks to files).
            if !path.is_file() {
                return false;
            }
            // Anti-listed files are excluded from the folder scan — they would
            // fail at decode time anyway, and we don't want them cluttering
            // the thumbnail bar. The user gets the anti-list error only if
            // they explicitly open such a file by path.
            if registry.check_anti_list(path).is_some() {
                return false;
            }
            registry.is_supported(path)
        })
        .collect();

    images.sort();
    images
}

/// Recursively walk a directory tree and collect all supported images.
/// Returns a list of (folder_path, image_path) pairs — the folder is the
/// immediate parent of each image, used by `random_from_tree` to switch
/// folders when the random pick lands in a different subfolder.
///
/// Skips hidden directories (starting with `.`) and symlinks to avoid
/// infinite loops.
pub fn walk_folder_tree(dir: &Path, registry: &FormatRegistry) -> Vec<(PathBuf, PathBuf)> {
    let mut results = Vec::new();
    walk_folder_tree_recursive(dir, registry, &mut results);
    results
}

fn walk_folder_tree_recursive(
    dir: &Path,
    registry: &FormatRegistry,
    results: &mut Vec<(PathBuf, PathBuf)>,
) {
    let entries = match std::fs::read_dir(dir) {
        Ok(e) => e,
        Err(_) => return,
    };

    for entry in entries.flatten() {
        let path = entry.path();
        let name = match path.file_name().and_then(|n| n.to_str()) {
            Some(n) => n,
            None => continue,
        };
        if name.starts_with('.') {
            continue;
        }

        if path.is_dir() {
            // Don't follow symlinks to avoid cycles.
            if path.is_symlink() {
                continue;
            }
            walk_folder_tree_recursive(&path, registry, results);
        } else if path.is_file()
            && registry.check_anti_list(&path).is_none()
            && registry.is_supported(&path)
        {
            let folder = path.parent().unwrap_or(dir).to_path_buf();
            results.push((folder, path));
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::tempdir;

    #[test]
    fn scans_supported_files_only() {
        let dir = tempdir().unwrap();
        let dir_path = dir.path();

        // Create a mix of supported, anti-listed, and unsupported files.
        fs::write(dir_path.join("a.png"), b"fake png").unwrap();
        fs::write(dir_path.join("b.jpg"), b"fake jpg").unwrap();
        fs::write(dir_path.join("c.heic"), b"fake heic").unwrap(); // anti-listed
        fs::write(dir_path.join("d.txt"), b"not an image").unwrap();
        fs::write(dir_path.join(".hidden.png"), b"hidden").unwrap();
        fs::write(dir_path.join("e.webp"), b"fake webp").unwrap();

        let registry = FormatRegistry::new();
        let result = scan_folder(dir_path, &registry);

        let names: Vec<&str> = result
            .iter()
            .map(|p| p.file_name().unwrap().to_str().unwrap())
            .collect();
        assert_eq!(names, vec!["a.png", "b.jpg", "e.webp"]);
    }

    #[test]
    fn missing_dir_returns_empty() {
        let registry = FormatRegistry::new();
        let result = scan_folder(Path::new("/nonexistent/path/that/does/not/exist"), &registry);
        assert!(result.is_empty());
    }
}
