use bstr::{BString, ByteSlice};

use crate::ignore;

pub struct Lines<'a> {
    lines: bstr::Lines<'a>,
    line_no: usize,
}

impl<'a> Lines<'a> {
    pub fn new(buf: &'a [u8]) -> Self {
        let bom = unicode_bom::Bom::from(buf);
        Lines {
            lines: buf[bom.len()..].lines(),
            line_no: 0,
        }
    }
}

impl<'a> Iterator for Lines<'a> {
    type Item = (BString, ignore::pattern::Mode, usize);

    fn next(&mut self) -> Option<Self::Item> {
        for line in self.lines.by_ref() {
            self.line_no += 1;
            if line.first() == Some(&b'#') {
                continue;
            }
            match parse_line(line) {
                None => continue,
                Some((line, flags)) => return Some((line, flags, self.line_no)),
            }
        }
        None
    }
}

#[inline]
pub(crate) fn parse_line(mut line: &[u8]) -> Option<(BString, ignore::pattern::Mode)> {
    let mut mode = ignore::pattern::Mode::empty();
    if line.is_empty() {
        return None;
    };
    if line.first() == Some(&b'!') {
        mode |= ignore::pattern::Mode::NEGATIVE;
        line = &line[1..];
    } else if line.first() == Some(&b'\\') {
        let second = line.get(1);
        if second == Some(&b'!') || second == Some(&b'#') {
            line = &line[1..];
        }
    }
    if line.iter().all(|b| b.is_ascii_whitespace()) {
        return None;
    }
    let mut line = truncate_non_escaped_trailing_spaces(line);
    if line.last() == Some(&b'/') {
        mode |= ignore::pattern::Mode::MUST_BE_DIR;
        line.pop();
    }
    if !line.contains(&b'/') {
        mode |= ignore::pattern::Mode::NO_SUB_DIR;
    }
    if line.first() == Some(&b'*') && line[1..].find_byteset(br"*?[\").is_none() {
        mode |= ignore::pattern::Mode::ENDS_WITH;
    }
    Some((line, mode))
}

/// We always copy just because that's ultimately needed anyway, not because we always have to.
fn truncate_non_escaped_trailing_spaces(buf: &[u8]) -> BString {
    match buf.rfind_not_byteset(br"\ ") {
        Some(pos) if pos + 1 == buf.len() => buf.into(), // does not end in (escaped) whitespace
        None => buf.into(),
        Some(start_of_non_space) => {
            // This seems a bit strange but attempts to recreate the git implementation while
            // actually removing the escape characters before spaces. We leave other backslashes
            // for escapes to be handled by `glob/globset`.
            let mut res: BString = buf[..start_of_non_space + 1].into();

            let mut trailing_bytes = buf[start_of_non_space + 1..].iter();
            let mut bare_spaces = 0;
            while let Some(b) = trailing_bytes.next() {
                match b {
                    b' ' => {
                        bare_spaces += 1;
                    }
                    b'\\' => {
                        res.extend(std::iter::repeat(b' ').take(bare_spaces));
                        bare_spaces = 0;
                        // Skip what follows, like git does, but keep spaces if possible.
                        if trailing_bytes.next() == Some(&b' ') {
                            res.push(b' ');
                        }
                    }
                    _ => unreachable!("BUG: this must be either backslash or space"),
                }
            }
            res
        }
    }
}
