//! iced-viewer spike
//!
//! Goal: prove iced can deliver the ristretto core experience.
//! - Open a folder of images
//! - Display the current image scaled to fit
//! - Scroll wheel = prev/next photo
//! - Arrow keys also navigate
//! - Dark theme
//!
//! Non-goals for the spike: thumbnail bar, zoom/pan, right-click menu, fullscreen.
//! Those come after we pick a winner.

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

use iced::widget::{container, image, text};
use iced::{
    Element, Event, Length, Subscription, Task, Theme, keyboard, mouse,
};

fn main() -> iced::Result {
    iced::application("image-viewer (iced spike)", Viewer::update, Viewer::view)
        .theme(|_| Theme::Dark)
        .subscription(Viewer::subscription)
        .run()
}

#[derive(Debug, Clone)]
enum Message {
    /// User picked a folder via file dialog
    FolderSelected(Option<PathBuf>),
    /// Index changed (prev/next)
    Navigate(i32),
    /// Image finished loading
    ImageLoaded(Arc<image::Handle>),
    /// Window event we care about (scroll, resize, etc.)
    WindowEvent(Event),
    /// Open folder dialog
    OpenFolder,
    /// Errors
    Error(String),
}

struct Viewer {
    /// Sorted list of image paths in the current folder
    images: Vec<PathBuf>,
    /// Current index into `images`
    current: usize,
    /// Loaded image handle (None while loading)
    handle: Option<Arc<image::Handle>>,
    /// Status bar text
    status: String,
    /// True when an image is currently being decoded
    loading: bool,
}

impl Default for Viewer {
    fn default() -> Self {
        Self {
            images: Vec::new(),
            current: 0,
            handle: None,
            status: "Press O to open a folder".into(),
            loading: false,
        }
    }
}

impl Viewer {
    fn update(&mut self, message: Message) -> Task<Message> {
        match message {
            Message::OpenFolder => {
                self.status = "Opening dialog...".into();
                Task::perform(
                    async {
                        rfd::AsyncFileDialog::new()
                            .set_title("Pick a folder of images")
                            .pick_folder()
                            .await
                            .map(|h| h.path().to_path_buf())
                    },
                    |path| Message::FolderSelected(path),
                )
            }
            Message::FolderSelected(Some(path)) => {
                self.load_folder(path);
                if self.images.is_empty() {
                    self.status = "No images in that folder".into();
                    Task::none()
                } else {
                    self.current = 0;
                    self.load_current()
                }
            }
            Message::FolderSelected(None) => {
                self.status = "Cancelled".into();
                Task::none()
            }
            Message::Navigate(delta) => {
                if self.images.is_empty() {
                    return Task::none();
                }
                let len = self.images.len() as i32;
                let new_idx =
                    ((self.current as i32 + delta).rem_euclid(len)) as usize;
                if new_idx != self.current {
                    self.current = new_idx;
                    return self.load_current();
                }
                Task::none()
            }
            Message::ImageLoaded(handle) => {
                self.handle = Some(handle);
                self.loading = false;
                if let Some(p) = self.images.get(self.current) {
                    self.status = format!(
                        "{}  ({}/{})",
                        p.file_name()
                            .map(|s| s.to_string_lossy())
                            .unwrap_or_default(),
                        self.current + 1,
                        self.images.len()
                    );
                }
                Task::none()
            }
            Message::WindowEvent(event) => {
                self.handle_event(event)
            }
            Message::Error(msg) => {
                self.status = format!("Error: {msg}");
                self.loading = false;
                Task::none()
            }
        }
    }

    fn view(&self) -> Element<Message> {
        let img_view: Element<Message> = if let Some(h) = &self.handle {
            image(h.as_ref().clone())
                .content_fit(iced::ContentFit::Contain)
                .width(Length::Fill)
                .height(Length::Fill)
                .into()
        } else {
            text(if self.loading {
                "Loading..."
            } else {
                "No image"
            })
            .size(24)
            .into()
        };

        let content = container(img_view)
            .width(Length::Fill)
            .height(Length::Fill)
            .align_x(iced::Alignment::Center)
            .align_y(iced::Alignment::Center)
            .padding(8);

        let status_bar = container(text(self.status.clone()).size(13))
            .style(|_theme| container::Style {
                background: Some(iced::Color::from_rgb(0.08, 0.08, 0.10).into()),
                text_color: Some(iced::Color::from_rgb(0.75, 0.75, 0.78)),
                ..Default::default()
            })
            .padding(6)
            .width(Length::Fill)
            .align_x(iced::Alignment::Center);

        iced::widget::column![content, status_bar]
            .width(Length::Fill)
            .height(Length::Fill)
            .into()
    }

    fn subscription(&self) -> Subscription<Message> {
        iced::event::listen().map(Message::WindowEvent)
    }

    /// Handle keyboard + scroll events
    fn handle_event(&mut self, event: Event) -> Task<Message> {
        match event {
            Event::Keyboard(keyboard::Event::KeyPressed {
                key,
                modifiers: _,
                ..
            }) => match key {
                keyboard::Key::Named(keyboard::key::Named::ArrowRight)
                | keyboard::Key::Character(_)
                    if matches!(
                        key,
                        keyboard::Key::Character(ref c)
                            if c == "l" || c == "L"
                    ) =>
                {
                    self.update(Message::Navigate(1))
                }
                keyboard::Key::Named(keyboard::key::Named::ArrowLeft)
                | keyboard::Key::Character(_)
                    if matches!(
                        key,
                        keyboard::Key::Character(ref c)
                            if c == "h" || c == "H"
                    ) =>
                {
                    self.update(Message::Navigate(-1))
                }
                keyboard::Key::Character(ref c)
                    if c == "o" || c == "O" =>
                {
                    self.update(Message::OpenFolder)
                }
                keyboard::Key::Character(ref c)
                    if c == "q" || c == "Q" =>
                {
                    iced::exit()
                }
                _ => Task::none(),
            },
            Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
                let dy = match delta {
                    mouse::ScrollDelta::Lines { y, .. } => y,
                    mouse::ScrollDelta::Pixels { y, .. } => y / 16.0,
                };
                if dy.abs() > 0.01 {
                    // Natural scrolling: scroll up = previous, scroll down = next
                    // (matches ristretto's default)
                    self.update(Message::Navigate(if dy > 0.0 { -1 } else { 1 }))
                } else {
                    Task::none()
                }
            }
            _ => Task::none(),
        }
    }

    /// Scan a folder for image files, sort lexicographically
    fn load_folder(&mut self, path: PathBuf) {
        let mut images: Vec<PathBuf> = Vec::new();
        if let Ok(entries) = std::fs::read_dir(&path) {
            for entry in entries.flatten() {
                let p = entry.path();
                if p.is_file() && is_supported_image(&p) {
                    images.push(p);
                }
            }
        }
        images.sort();
        self.images = images;
    }

    /// Kick off async decode of the current image
    fn load_current(&mut self) -> Task<Message> {
        let path = match self.images.get(self.current) {
            Some(p) => p.clone(),
            None => return Task::none(),
        };
        self.loading = true;
        self.status = format!(
            "Loading {}",
            path.file_name()
                .map(|s| s.to_string_lossy())
                .unwrap_or_default()
        );
        Task::perform(
            async move {
                let bytes = std::fs::read(&path)
                    .map_err(|e| e.to_string())?;
                Ok::<_, String>(image::Handle::from_bytes(bytes))
            },
            |res| match res {
                Ok(h) => Message::ImageLoaded(Arc::new(h)),
                Err(e) => Message::Error(e),
            },
        )
    }
}

fn is_supported_image(p: &Path) -> bool {
    let ext = match p.extension().and_then(|e| e.to_str()) {
        Some(e) => e.to_ascii_lowercase(),
        None => return false,
    };
    matches!(
        ext.as_str(),
        "png" | "jpg" | "jpeg" | "gif" | "webp"
            | "bmp" | "ico" | "cur" | "avif"
            | "apng" | "jfif"
    )
}
