/// a displayable representation of where
/// the needle was found, with some text around
#[derive(Debug, Clone)]
pub struct ContentMatch {
    pub extract: String,
    pub needle_start: usize, // position in the extract, in bytes
    pub needle_end: usize,   // length in bytes
}

impl ContentMatch {
    pub fn build(
        hay: &[u8],
        pos: usize, // position in the hay
        needle: &str,
        desired_len: usize, // max length of the extract in bytes
    ) -> Self {
        if hay.is_empty() {
            // this happens if you search `cr/.*` and a file starts with an empty line
            return Self {
                extract: String::new(),
                needle_start: 0,
                needle_end: 0,
            };
        }
        let mut extract_start = pos;
        let mut extract_end = pos + needle.len(); // not included
        loop {
            if extract_start == 0 || extract_end - extract_start >= desired_len / 2 {
                break;
            }
            let c = hay[extract_start - 1];
            if c < 32 {
                break;
            }
            extract_start -= 1;
        }
        // left trimming
        while (hay[extract_start] == 32) && extract_start < pos {
            extract_start += 1;
        }
        loop {
            if extract_end == hay.len() || extract_end - extract_start >= desired_len {
                break;
            }
            let c = hay[extract_end];
            if c < 32 {
                break;
            }
            extract_end += 1;
        }
        // at this point we're unsure whether we start at a correct char boundary, hence
        // the from_utf8_lossy
        let extract = String::from_utf8_lossy(&hay[extract_start..extract_end]).to_string();
        let needle_start = extract.find(needle).unwrap_or(0);
        Self {
            extract,
            needle_start,
            needle_end: needle_start + needle.len(),
        }
    }
}
