use crate::{
    lines,
    matcher::Matcher,
    searcher::{Config, Range, Searcher},
    sink::{Sink, SinkError, SinkFinish, SinkMatch},
};

#[derive(Debug)]
pub(crate) struct Core<'s, M: 's, S> {
    config: &'s Config,
    matcher: M,
    searcher: &'s Searcher,
    sink: S,
    pos: usize,
    absolute_byte_offset: u64,
    line_number: Option<u64>,
    last_line_counted: usize,
    last_line_visited: usize,
}

impl<'s, M: Matcher, S: Sink> Core<'s, M, S> {
    pub(crate) fn new(searcher: &'s Searcher, matcher: M, sink: S) -> Core<'s, M, S> {
        let line_number = if searcher.config.line_number {
            Some(1)
        } else {
            None
        };
        Core {
            config: &searcher.config,
            matcher,
            searcher,
            sink,
            pos: 0,
            absolute_byte_offset: 0,
            line_number,
            last_line_counted: 0,
            last_line_visited: 0,
        }
    }

    pub(crate) fn pos(&self) -> usize {
        self.pos
    }

    pub(crate) fn set_pos(&mut self, pos: usize) {
        self.pos = pos;
    }

    pub(crate) fn matched(&mut self, buf: &[u8], range: &Range) -> Result<bool, S::Error> {
        self.sink_matched(buf, range)
    }

    pub(crate) fn find(&mut self, slice: &[u8]) -> Result<Option<Range>, S::Error> {
        self.matcher.find(slice).map_err(S::Error::error_message)
    }

    pub(crate) fn begin(&mut self) -> Result<bool, S::Error> {
        self.sink.begin(self.searcher)
    }

    pub(crate) fn finish(&mut self, byte_count: u64) -> Result<(), S::Error> {
        self.sink.finish(self.searcher, &SinkFinish { byte_count })
    }

    pub(crate) fn match_by_line(&mut self, buf: &[u8]) -> Result<bool, S::Error> {
        while !buf[self.pos()..].is_empty() {
            if let Some(line) = self.find_by_line(buf)? {
                self.set_pos(line.end());
                if !self.sink_matched(buf, &line)? {
                    return Ok(false);
                }
            } else {
                break;
            }
        }
        self.set_pos(buf.len());
        Ok(true)
    }

    #[inline(always)]
    fn find_by_line(&mut self, buf: &[u8]) -> Result<Option<Range>, S::Error> {
        let mut pos = self.pos();
        while !buf[pos..].is_empty() {
            let mat = match self
                .matcher
                .find(&buf[pos..])
                .map_err(S::Error::error_message)?
            {
                None => return Ok(None),
                Some(m) => m,
            };
            let line = lines::locate(
                buf,
                self.config.line_term.as_byte(),
                Range::zero(mat.start()).offset(pos),
            );
            if line.start() == buf.len() {
                pos = buf.len();
                continue;
            }
            return Ok(Some(line));
        }
        Ok(None)
    }

    #[inline(always)]
    fn sink_matched(&mut self, buf: &[u8], range: &Range) -> Result<bool, S::Error> {
        self.count_lines(buf, range.start());
        let offset = self.absolute_byte_offset + range.start() as u64;
        let linebuf = &buf[*range];
        let keepgoing = self.sink.matched(
            self.searcher,
            &SinkMatch {
                bytes: linebuf,
                absolute_byte_offset: offset,
                line_number: self.line_number,
                buffer: buf,
                bytes_range_in_buffer: range.start()..range.end(),
            },
        )?;
        if !keepgoing {
            return Ok(false);
        }
        self.last_line_visited = range.end();
        Ok(true)
    }

    fn count_lines(&mut self, buf: &[u8], upto: usize) {
        if let Some(ref mut line_number) = self.line_number {
            if self.last_line_counted >= upto {
                return;
            }
            let slice = &buf[self.last_line_counted..upto];
            let count = lines::count(slice, self.config.line_term.as_byte());
            *line_number += count;
            self.last_line_counted = upto;
        }
    }
}
