use {
    super::{
        CropWriter,
        SPACE_FILLING,
    },
    crate::pattern::NameMatch,
    termimad::{
        CompoundStyle,
        StrFit,
        minimad::Alignment,
    },
    unicode_width::{
        UnicodeWidthChar,
        UnicodeWidthStr,
    },
};

pub struct MatchedString<'a> {
    pub name_match: Option<NameMatch>,
    pub string: &'a str,
    pub base_style: &'a CompoundStyle,
    pub match_style: &'a CompoundStyle,
    pub display_width: Option<usize>,
    pub align: Alignment,
}

impl<'a> MatchedString<'a> {
    pub fn new(
        name_match: Option<NameMatch>,
        string: &'a str,
        base_style: &'a CompoundStyle,
        match_style: &'a CompoundStyle,
    ) -> Self {
        Self {
            name_match,
            string,
            base_style,
            match_style,
            display_width: None,
            align: Alignment::Left,
        }
    }
    /// If the string contains sep, then cut the tail of this matched
    /// string and return it.
    /// Note: a non none `display_width` currently prevents splitting
    /// (i.e. it's not yet implemented and would involve compute width)
    pub fn split_on_last(
        &mut self,
        sep: char,
    ) -> Option<Self> {
        if self.display_width.is_some() {
            // the proper algo would need measuring the left part I guess
            None
        } else {
            self.string.rfind(sep).map(|sep_idx| {
                let right = &self.string[sep_idx + 1..];
                self.string = &self.string[..=sep_idx];
                let left_chars_count = self.string.chars().count();
                let right_name_match = self
                    .name_match
                    .as_mut()
                    .map(|nm| nm.cut_after(left_chars_count));
                MatchedString {
                    name_match: right_name_match,
                    string: right,
                    base_style: self.base_style,
                    match_style: self.match_style,
                    display_width: None,
                    align: self.align,
                }
            })
        }
    }
    pub fn fill(
        &mut self,
        width: usize,
        align: Alignment,
    ) {
        self.display_width = Some(width);
        self.align = align;
    }
    pub fn width(&self) -> usize {
        UnicodeWidthStr::width(self.string)
    }
    /// Remove characters left so that the visible width is equal or
    /// less to the required width
    pub fn cut_left_to_fit(
        &mut self,
        max_width: usize,
    ) -> usize {
        let mut removed_char_count = 0;
        let mut break_idx = 0;
        let mut width = self.width();
        for (idx, c) in self.string.char_indices() {
            if width <= max_width {
                break;
            }
            break_idx = idx + c.len_utf8();
            let char_width = c.width().unwrap_or(0);
            if char_width > width {
                warn!("inconsistent char/str widths");
                break;
            }
            width -= char_width;
            removed_char_count += 1;
        }
        if removed_char_count > 0 {
            self.string = &self.string[break_idx..];
            self.name_match = self
                .name_match
                .take()
                .map(|mut nm| nm.cut_after(removed_char_count));
        }
        removed_char_count
    }
    pub fn queue_on<W>(
        &self,
        cw: &mut CropWriter<'_, W>,
    ) -> Result<(), termimad::Error>
    where
        W: std::io::Write,
    {
        if let Some(m) = &self.name_match {
            let mut pos_idx: usize = 0;
            let mut combined_style = *self.base_style;
            combined_style.overwrite_with(self.match_style);
            let mut right_filling = 0;
            let mut s = self.string;
            if let Some(dw) = self.display_width {
                let w = unicode_width::UnicodeWidthStr::width(s);
                #[allow(clippy::comparison_chain)]
                if w > dw {
                    let (count_bytes, _) = StrFit::count_fitting(s, dw);
                    s = &s[0..count_bytes];
                } else if w < dw {
                    match self.align {
                        Alignment::Right => {
                            cw.repeat(self.base_style, &SPACE_FILLING, dw - w)?;
                        }
                        Alignment::Center => {
                            right_filling = (dw - w) / 2;
                            cw.repeat(self.base_style, &SPACE_FILLING, dw - w - right_filling)?;
                        }
                        _ => {
                            right_filling = dw - w;
                        }
                    }
                }
            }
            // we might call queue_char more than allowed but that's okay
            // because the cropwriter will crop them
            for (cand_idx, cand_char) in s.chars().enumerate() {
                if pos_idx < m.pos.len() && m.pos[pos_idx] == cand_idx {
                    cw.queue_char(&combined_style, cand_char)?;
                    pos_idx += 1;
                } else {
                    cw.queue_char(self.base_style, cand_char)?;
                }
            }
            if right_filling > 0 {
                cw.repeat(self.base_style, &SPACE_FILLING, right_filling)?;
            }
        } else if let Some(w) = self.display_width {
            match self.align {
                Alignment::Center => {
                    cw.queue_str(self.base_style, &format!("{:^w$}", self.string, w = w))?;
                }
                Alignment::Right => {
                    cw.queue_str(self.base_style, &format!("{:>w$}", self.string, w = w))?;
                }
                _ => {
                    cw.queue_str(self.base_style, &format!("{:<w$}", self.string, w = w))?;
                }
            }
        } else {
            cw.queue_str(self.base_style, self.string)?;
        }
        Ok(())
    }
}

#[cfg(test)]
mod matched_string_tests {
    use {
        super::MatchedString,
        crate::pattern::NameMatch,
        smallvec::smallvec,
        termimad::CompoundStyle,
    };

    /// Cutting chars from the left to fit a width reindexes every surviving
    /// match position by the number of removed chars: original index `rcc`
    /// becomes display index `0`, so a match at original `p` lands at `p - rcc`.
    #[test]
    fn cut_left_to_fit_reindexes_match_positions() {
        let base_style = CompoundStyle::default();
        let match_style = CompoundStyle::default();
        // "abcdef" (width 6) is matched on 'e', 'f' (positions 4, 5).
        let mut matched_string = MatchedString::new(
            Some(NameMatch {
                score: 1,
                pos: smallvec![4, 5],
            }),
            "abcdef",
            &base_style,
            &match_style,
        );
        // Fitting to width 4 removes the 2 leftmost chars -> visible "cdef".
        let removed = matched_string.cut_left_to_fit(4);
        assert_eq!(removed, 2);
        assert_eq!(matched_string.string, "cdef");
        // 'e', 'f' are now at display positions 2, 3.
        let expected: &[usize] = &[2, 3];
        assert_eq!(
            matched_string.name_match.as_ref().unwrap().pos.as_slice(),
            expected,
        );
    }

    /// When the last removed char was itself matched, that match must be
    /// dropped instead of leaving a phantom highlight on the first visible char.
    #[test]
    fn cut_left_to_fit_drops_match_on_last_removed_char() {
        let base_style = CompoundStyle::default();
        let match_style = CompoundStyle::default();
        // "wxyzab" (width 6) is matched on 'x', 'y' (positions 1, 2).
        let mut matched_string = MatchedString::new(
            Some(NameMatch {
                score: 1,
                pos: smallvec![1, 2],
            }),
            "wxyzab",
            &base_style,
            &match_style,
        );
        // Fitting to width 4 removes 'w', 'x' -> visible "yzab". 'x' (the last
        // removed char) was matched but is gone; only 'y' survives, at index 0.
        let removed = matched_string.cut_left_to_fit(4);
        assert_eq!(removed, 2);
        assert_eq!(matched_string.string, "yzab");
        let expected: &[usize] = &[0];
        assert_eq!(
            matched_string.name_match.as_ref().unwrap().pos.as_slice(),
            expected,
        );
    }
}
