use ratatui::text::Line;
use std::borrow::Cow;
use std::fmt::{Debug, Display};

use crate::{AsAny, DisplayContext, ItemPreview, PreviewContext};

/// A `SkimItem` defines what's been processed(fetched, matched, previewed and returned) by skim
///
/// # Downcast Example
/// Skim will return the item back, but in `Arc<dyn SkimItem>` form. We might want a reference
/// to the concrete type instead of trait object. Skim provide a somehow "complicated" way to
/// `downcast` it back to the reference of the original concrete type.
///
/// ```rust
/// use skim::prelude::*;
///
/// struct MyItem {}
/// impl SkimItem for MyItem {
///     fn text(&self) -> Cow<str> {
///         unimplemented!()
///     }
/// }
///
/// impl MyItem {
///     pub fn mutable(&mut self) -> i32 {
///         1
///     }
///
///     pub fn immutable(&self) -> i32 {
///         0
///     }
/// }
///
/// let mut ret: Arc<dyn SkimItem> = Arc::new(MyItem{});
/// let mutable: &mut MyItem = Arc::get_mut(&mut ret)
///     .expect("item is referenced by others")
///     .as_any_mut() // cast to Any
///     .downcast_mut::<MyItem>() // downcast to (mut) concrete type
///     .expect("something wrong with downcast");
/// assert_eq!(mutable.mutable(), 1);
///
/// let immutable: &MyItem = (*ret).as_any() // cast to Any
///     .downcast_ref::<MyItem>() // downcast to concrete type
///     .expect("something wrong with downcast");
/// assert_eq!(immutable.immutable(), 0)
/// ```
pub trait SkimItem: AsAny + Send + Sync + 'static {
    /// The string to be used for matching (without color)
    fn text(&self) -> Cow<'_, str>;

    /// The content to be displayed on the item list, could contain ANSI properties
    fn display(&self, context: DisplayContext) -> Line<'_> {
        context.to_line(self.text())
    }

    /// Custom preview content, default to `ItemPreview::Global` which will use global preview
    /// setting(i.e. the command set by `preview` option)
    fn preview(&self, _context: PreviewContext) -> ItemPreview {
        ItemPreview::Global
    }

    /// Get output text(after accept), default to `text()`
    ///
    /// Note that this function is intended to be used by the caller of skim and will not be used by
    /// skim. And since skim will return the item back in `SkimOutput`, if string is not what you
    /// want, you could still use `downcast` to retain the pointer to the original struct.
    fn output(&self) -> Cow<'_, str> {
        self.text()
    }

    /// Limit the matching ranges of the `get_text` of the item.
    /// providing (`start_byte`, `end_byte`) of the range
    fn get_matching_ranges(&self) -> Option<&[(usize, usize)]> {
        None
    }

    /// Byte ranges of `text()` that are hidden from display (via `--hide-nth`).
    ///
    /// Characters inside these ranges are removed from the rendered line and ignored
    /// for match highlighting and horizontal scrolling, but stay part of `text()` so
    /// they remain searchable. Ranges are expressed as (`start_byte`, `end_byte`) and
    /// are expected to be sorted and non-overlapping. Returns `None` when nothing is
    /// hidden.
    fn hidden_ranges(&self) -> Option<&[(usize, usize)]> {
        None
    }

    /// Returns true if the item should be disabled
    /// Disabled items cannot be selected
    fn disabled(&self) -> bool {
        false
    }
}

//------------------------------------------------------------------------------
// Implement SkimItem for raw strings

impl<T: AsRef<str> + Send + Sync + 'static> SkimItem for T {
    fn text(&self) -> Cow<'_, str> {
        Cow::Borrowed(self.as_ref())
    }
}

impl Display for dyn SkimItem {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.text())
    }
}
impl Debug for dyn SkimItem {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_fmt(format_args!("SkimItem {{ text: {} }}", self.text()))
    }
}

#[cfg(test)]
#[cfg_attr(coverage, coverage(off))]
mod tests {
    use super::*;
    use std::sync::Arc;

    #[test]
    fn blanket_impl_default_methods() {
        let item = "hello".to_string();
        assert_eq!(item.text(), "hello");
        // `output` defaults to `text`.
        assert_eq!(item.output(), "hello");
        assert!(item.get_matching_ranges().is_none());
        assert!(!item.disabled());
    }

    #[test]
    fn display_and_debug_for_trait_object() {
        let item: Arc<dyn SkimItem> = Arc::new("world".to_string());
        let as_dyn: &dyn SkimItem = &*item;
        assert_eq!(format!("{as_dyn}"), "world");
        assert!(format!("{as_dyn:?}").contains("world"));
    }
}
