//! Navigation layer — folder walking, current index, scroll handling.
//!
//! The [`Navigator`] owns the list of images in the current folder and the
//! current index. It does NOT own the decoded image — that's the app layer's
//! job. The Navigator is a pure data structure: pass it a folder path, it
//! gives you back a sorted list of supported image paths and helps you move
//! the index around.

pub mod folder;

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

pub use folder::scan_folder;

/// The navigation state for the current folder.
#[derive(Debug, Clone, Default)]
pub struct Navigator {
    /// Sorted list of image paths in the current folder.
    /// Empty when no folder is open.
    images: Vec<PathBuf>,
    /// Current index into `images`. Wraps around modulo `images.len()`.
    current: usize,
}

impl Navigator {
    pub fn new() -> Self {
        Self::default()
    }

    /// Replace the image list with a fresh folder scan.
    /// Resets the current index to 0.
    pub fn set_images(&mut self, images: Vec<PathBuf>) {
        self.images = images;
        self.current = 0;
    }

    pub fn len(&self) -> usize {
        self.images.len()
    }

    pub fn is_empty(&self) -> bool {
        self.images.is_empty()
    }

    pub fn current_index(&self) -> usize {
        self.current
    }

    pub fn current_path(&self) -> Option<&Path> {
        self.images.get(self.current).map(|p| p.as_path())
    }

    pub fn images(&self) -> &[PathBuf] {
        &self.images
    }

    /// Move by `delta` positions, wrapping around. Returns the new index.
    /// No-op if the list is empty.
    pub fn navigate(&mut self, delta: i32) -> usize {
        if self.images.is_empty() {
            return 0;
        }
        let len = self.images.len() as i32;
        let new = ((self.current as i32 + delta).rem_euclid(len)) as usize;
        self.current = new;
        new
    }

    /// Jump to a specific index. Returns false if out of bounds.
    pub fn jump_to(&mut self, index: usize) -> bool {
        if index >= self.images.len() {
            return false;
        }
        self.current = index;
        true
    }

    /// Pick a random image from the current folder. Sets `current` to the
    /// random index. Subsequent `navigate(±1)` calls step sequentially from
    /// the new position (step-aware behavior).
    /// Returns false if the list is empty.
    pub fn random_same_folder(&mut self) -> bool {
        if self.images.is_empty() {
            return false;
        }
        let n = pseudo_random(self.images.len());
        self.current = n;
        true
    }

    /// Pick a random image from a list of all images in the folder tree.
    /// If the picked image is in the current folder, sets `current` to its
    /// index. If it is in a different folder, replaces the image list with
    /// that folder's images and sets `current` to the picked image's index.
    /// Returns the folder path if a folder switch occurred, or None if the
    /// random image was in the current folder.
    pub fn random_from_tree(
        &mut self,
        all_tree_images: &[(PathBuf, PathBuf)],
    ) -> Option<PathBuf> {
        if all_tree_images.is_empty() {
            return None;
        }
        let n = pseudo_random(all_tree_images.len());
        let (folder, image) = &all_tree_images[n];

        // Check if the image is in the current folder.
        if let Some(idx) = self.images.iter().position(|p| p == image) {
            self.current = idx;
            return None;
        }

        // The image is in a different folder. Load that folder's images.
        // Extract just the images from the same folder.
        let same_folder: Vec<PathBuf> = all_tree_images
            .iter()
            .filter(|(f, _)| f == folder)
            .map(|(_, p)| p.clone())
            .collect();
        let new_idx = same_folder.iter().position(|p| p == image).unwrap_or(0);
        self.images = same_folder;
        self.current = new_idx;
        Some(folder.clone())
    }

    /// Jump to the first image. No-op if empty.
    pub fn first(&mut self) {
        if !self.images.is_empty() {
            self.current = 0;
        }
    }

    /// Jump to the last image. No-op if empty.
    pub fn last(&mut self) {
        if !self.images.is_empty() {
            self.current = self.images.len() - 1;
        }
    }
}

/// Pseudo-random index in [0, max). Uses SystemTime nanos as the entropy
/// source — sufficient for an image viewer's shuffle feature without
/// pulling in the `rand` crate as a direct dependency.
fn pseudo_random(max: usize) -> usize {
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.subsec_nanos() as usize)
        .unwrap_or(0);
    nanos % max
}

#[cfg(test)]
mod tests {
    use super::*;

    fn make_paths(n: usize) -> Vec<PathBuf> {
        (0..n).map(|i| PathBuf::from(format!("/fake/img{i:03}.png"))).collect()
    }

    #[test]
    fn empty_navigator_is_safe() {
        let mut nav = Navigator::new();
        assert!(nav.is_empty());
        assert_eq!(nav.current_path(), None);
        nav.navigate(1); // should not panic
        nav.first();
        nav.last();
    }

    #[test]
    fn navigate_wraps_forward() {
        let mut nav = Navigator::new();
        nav.set_images(make_paths(3));
        assert_eq!(nav.current_index(), 0);
        nav.navigate(1);
        assert_eq!(nav.current_index(), 1);
        nav.navigate(1);
        assert_eq!(nav.current_index(), 2);
        nav.navigate(1);
        assert_eq!(nav.current_index(), 0); // wraps
    }

    #[test]
    fn navigate_wraps_backward() {
        let mut nav = Navigator::new();
        nav.set_images(make_paths(3));
        nav.navigate(-1);
        assert_eq!(nav.current_index(), 2); // wraps backward
    }

    #[test]
    fn first_and_last() {
        let mut nav = Navigator::new();
        nav.set_images(make_paths(5));
        nav.last();
        assert_eq!(nav.current_index(), 4);
        nav.first();
        assert_eq!(nav.current_index(), 0);
    }

    #[test]
    fn jump_to_respects_bounds() {
        let mut nav = Navigator::new();
        nav.set_images(make_paths(5));
        assert!(nav.jump_to(3));
        assert_eq!(nav.current_index(), 3);
        assert!(!nav.jump_to(5));
        assert_eq!(nav.current_index(), 3); // unchanged
    }
}
