//! Terminal UI components and rendering.
//!
//! This module provides the terminal user interface components for skim,
//! including the application state, event handling, rendering widgets,
//! and layout management.

use std::num::ParseIntError;

pub use app::App;
pub use event::Event;
pub use preview::PreviewCallback;
use thiserror::Error;
pub use widget::{SkimRender, SkimWidget};
mod app;
mod backend;
pub(crate) mod util;
#[cfg(windows)]
mod windows;
pub use backend::Tui;
/// Action definitions, catalog and parsing
pub mod actions;
/// Event handling
pub mod event;
/// Header display components
pub mod header;
mod input;
/// Item list display and management
pub mod item_list;
/// Single item rendering
pub(crate) mod item_renderer;
/// Pre-computed widget layout areas
pub mod layout;
/// TUI-specific options and configuration
pub mod options;
mod preview;
/// Status line display
pub mod statusline;
/// Widget rendering utilities
pub mod widget;

/// Number of heartbeats per second
pub const TICK_RATE: u32 = 120;

/// Represents a size value, either as a percentage or fixed value
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Size {
    /// Size as a percentage (0-100)
    Percent(u16),
    /// Fixed size in terminal cells
    Fixed(u16),
    /// Negative size is substracted from term height
    Neg(u16),
}

/// Direction for movement or layout
#[derive(PartialEq, Eq, Clone, Debug, Copy)]
pub enum Direction {
    /// Upward direction
    Up,
    /// Downward direction
    Down,
    /// Left direction
    Left,
    /// Right direction
    Right,
}

impl TryFrom<&str> for Direction {
    type Error = &'static str;
    fn try_from(value: &str) -> Result<Self, Self::Error> {
        match value.to_lowercase().as_str() {
            "up" => Ok(Self::Up),
            "down" => Ok(Self::Down),
            "left" => Ok(Self::Left),
            "right" => Ok(Self::Right),
            _ => Err("Unknown direction {value}"),
        }
    }
}

/// Error type for parsing size values
#[derive(Error, Debug, PartialEq, Eq)]
pub enum SizeParseError {
    /// Error parsing the size string
    #[error("Error parsing {0}: {1:?}")]
    ParseError(String, ParseIntError),
    /// Percentage value exceeds 100
    #[error("Invalid percentage {0}")]
    InvalidPercent(u16),
}

impl TryFrom<&str> for Size {
    type Error = SizeParseError;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        if value.ends_with('%') {
            let percent = value
                .strip_suffix("%")
                .unwrap_or_default()
                .parse::<u16>()
                .map_err(|e| SizeParseError::ParseError(value.to_string(), e))?;
            if percent > 100 {
                return Err(SizeParseError::InvalidPercent(percent));
            }
            Ok(Self::Percent(percent))
        } else if let Some(neg) = value.strip_prefix('-') {
            Ok(Self::Neg(
                neg.parse::<u16>()
                    .map_err(|e| SizeParseError::ParseError(value.to_string(), e))?,
            ))
        } else {
            Ok(Self::Fixed(
                value
                    .parse::<u16>()
                    .map_err(|e| SizeParseError::ParseError(value.to_string(), e))?,
            ))
        }
    }
}

impl Default for Size {
    fn default() -> Self {
        Self::Percent(100)
    }
}

impl std::fmt::Display for Size {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Percent(p) => f.write_fmt(format_args!("{p}%")),
            Self::Fixed(s) => f.write_fmt(format_args!("{s}")),
            Self::Neg(s) => f.write_fmt(format_args!("-{s}")),
        }
    }
}

/// This mirrors Ratatui's border type
///
/// We need it so that we can properly use `ValueEnum`
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
#[allow(missing_docs)]
pub enum BorderType {
    /// `ForceOff` disables borders around popups too
    /// set with `no_border`
    ForceOff,
    #[default]
    None,
    Plain,
    Rounded,
    Double,
    Thick,

    LightDoubleDashed,
    HeavyDoubleDashed,

    LightTripleDashed,
    HeavyTripleDashed,

    LightQuadrupleDashed,
    HeavyQuadrupleDashed,

    QuadrantInside,
    QuadrantOutside,
}

impl BorderType {
    fn is_none(self) -> bool {
        matches!(self, BorderType::None | BorderType::ForceOff)
    }
    fn is_some(self) -> bool {
        !self.is_none()
    }
    fn into_ratatui(self) -> Option<ratatui::widgets::BorderType> {
        if self.is_none() {
            return None;
        }

        Some(match self {
            BorderType::Plain => ratatui::widgets::BorderType::Plain,
            BorderType::Rounded => ratatui::widgets::BorderType::Rounded,
            BorderType::Double => ratatui::widgets::BorderType::Double,
            BorderType::Thick => ratatui::widgets::BorderType::Thick,
            BorderType::LightDoubleDashed => ratatui::widgets::BorderType::LightDoubleDashed,
            BorderType::HeavyDoubleDashed => ratatui::widgets::BorderType::HeavyDoubleDashed,
            BorderType::LightTripleDashed => ratatui::widgets::BorderType::LightTripleDashed,
            BorderType::HeavyTripleDashed => ratatui::widgets::BorderType::HeavyTripleDashed,
            BorderType::LightQuadrupleDashed => ratatui::widgets::BorderType::LightQuadrupleDashed,
            BorderType::HeavyQuadrupleDashed => ratatui::widgets::BorderType::HeavyQuadrupleDashed,
            BorderType::QuadrantInside => ratatui::widgets::BorderType::QuadrantInside,
            BorderType::QuadrantOutside => ratatui::widgets::BorderType::QuadrantOutside,
            BorderType::None | BorderType::ForceOff => unreachable!(),
        })
    }
}

#[cfg(feature = "cli")]
impl std::str::FromStr for BorderType {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        use clap::ValueEnum as _;
        for variant in Self::value_variants() {
            if variant.to_possible_value().unwrap().matches(s, false) {
                return Ok(*variant);
            }
        }
        Ok(Self::Plain)
    }
}
#[cfg(test)]
mod size_test {
    use super::*;
    use std::num::IntErrorKind;
    #[test]
    fn fixed_success() {
        assert_eq!(Size::try_from("10"), Ok(Size::Fixed(10u16)));
    }
    #[test]
    fn percent_success() {
        assert_eq!(Size::try_from("10%"), Ok(Size::Percent(10u16)));
    }
    #[test]
    fn fixed_neg() {
        assert_eq!(Size::try_from("-10"), Ok(Size::Neg(10)));
    }
    #[test]
    fn percent_neg() {
        let SizeParseError::ParseError(err_value, internal_error) = Size::try_from("-10%").unwrap_err() else {
            panic!();
        };
        assert_eq!(internal_error.kind(), &IntErrorKind::InvalidDigit);
        assert_eq!(err_value, String::from("-10%"));
    }
    #[test]
    fn percent_over_100() {
        let SizeParseError::InvalidPercent(internal_error) = Size::try_from("110%").unwrap_err() else {
            panic!();
        };
        assert_eq!(internal_error, 110u16);
    }
    #[test]
    fn fixed_invalid_char() {
        let SizeParseError::ParseError(value, internal_error) = Size::try_from("1-0").unwrap_err() else {
            panic!();
        };
        assert_eq!(internal_error.kind(), &IntErrorKind::InvalidDigit);
        assert_eq!(value, String::from("1-0"));
    }
    #[test]
    fn percent_invalid_char() {
        let SizeParseError::ParseError(value, internal_error) = Size::try_from("1-0%").unwrap_err() else {
            panic!();
        };
        assert_eq!(internal_error.kind(), &IntErrorKind::InvalidDigit);
        assert_eq!(value, String::from("1-0%"));
    }
    #[test]
    fn percent_empty() {
        let SizeParseError::ParseError(value, internal_error) = Size::try_from("%").unwrap_err() else {
            panic!();
        };
        assert_eq!(internal_error.kind(), &IntErrorKind::Empty);
        assert_eq!(value, String::from("%"));
    }

    #[test]
    fn default_is_full_percent() {
        assert_eq!(Size::default(), Size::Percent(100));
    }

    #[test]
    fn display_formats_each_variant() {
        assert_eq!(Size::Percent(50).to_string(), "50%");
        assert_eq!(Size::Fixed(20).to_string(), "20");
        assert_eq!(Size::Neg(5).to_string(), "-5");
    }

    #[test]
    fn direction_try_from_parses_each() {
        assert_eq!(Direction::try_from("up"), Ok(Direction::Up));
        assert_eq!(Direction::try_from("DOWN"), Ok(Direction::Down));
        assert_eq!(Direction::try_from("Left"), Ok(Direction::Left));
        assert_eq!(Direction::try_from("right"), Ok(Direction::Right));
        assert!(Direction::try_from("sideways").is_err());
    }

    #[test]
    fn border_type_none_and_some() {
        assert!(BorderType::None.is_none());
        assert!(BorderType::ForceOff.is_none());
        assert!(!BorderType::Plain.is_none());
        assert!(BorderType::Rounded.is_some());

        assert_eq!(BorderType::None.into_ratatui(), None);
        assert_eq!(
            BorderType::Plain.into_ratatui(),
            Some(ratatui::widgets::BorderType::Plain)
        );
    }
}
