use regex::Regex;
use std::path::{Path, PathBuf};

pub enum FilterType {
    Line(u32),
    Branch(u32),
    Both(u32),
}

#[derive(Default)]
pub struct FileFilter {
    excl_line: Option<Regex>,
    excl_start: Option<Regex>,
    excl_stop: Option<Regex>,
    excl_br_line: Option<Regex>,
    excl_br_start: Option<Regex>,
    excl_br_stop: Option<Regex>,
    pub(crate) incl_explicit: Option<PathBuf>,
}

impl FileFilter {
    pub fn new(
        excl_line: Option<Regex>,
        excl_start: Option<Regex>,
        excl_stop: Option<Regex>,
        excl_br_line: Option<Regex>,
        excl_br_start: Option<Regex>,
        excl_br_stop: Option<Regex>,
        incl_explicit: Option<PathBuf>,
    ) -> Self {
        Self {
            excl_line,
            excl_start,
            excl_stop,
            excl_br_line,
            excl_br_start,
            excl_br_stop,
            incl_explicit,
        }
    }

    pub fn create(&self, file: &Path) -> Vec<FilterType> {
        if self.excl_line.is_none()
            && self.excl_start.is_none()
            && self.excl_br_line.is_none()
            && self.excl_br_start.is_none()
        {
            return Vec::new();
        }

        let file = std::fs::read_to_string(file);
        let file = if let Ok(file) = file {
            file
        } else {
            return Vec::new();
        };

        let mut ignore_br = false;
        let mut ignore = false;

        file.split('\n')
            .enumerate()
            .filter_map(move |(number, line)| {
                // Line numbers are 1-based.
                let number = (number + 1) as u32;

                // The file is split on \n, which may result in a trailing \r
                // on Windows. Remove it.
                let line = line.strip_suffix('\r').unwrap_or(line);

                // End a branch ignore region. Region endings are exclusive.
                if ignore_br && self.excl_br_stop.as_ref().is_some_and(|f| f.is_match(line)) {
                    ignore_br = false
                }

                // End a line ignore region. Region endings are exclusive.
                if ignore && self.excl_stop.as_ref().is_some_and(|f| f.is_match(line)) {
                    ignore = false
                }

                // Start a branch ignore region. Region starts are inclusive.
                if !ignore_br
                    && self
                        .excl_br_start
                        .as_ref()
                        .is_some_and(|f| f.is_match(line))
                {
                    ignore_br = true;
                }

                // Start a line ignore region. Region starts are inclusive.
                if !ignore && self.excl_start.as_ref().is_some_and(|f| f.is_match(line)) {
                    ignore = true;
                }

                if ignore_br {
                    // Consuming code has to eliminate each of these
                    // individually, so it has to know when both are ignored vs.
                    // either.
                    if ignore {
                        Some(FilterType::Both(number))
                    } else {
                        Some(FilterType::Branch(number))
                    }
                } else if ignore {
                    Some(FilterType::Line(number))
                } else if self.excl_br_line.as_ref().is_some_and(|f| f.is_match(line)) {
                    // Single line exclusion. If single line exclusions occur
                    // inside a region they are meaningless (would be applied
                    // anway), so they are lower priority.
                    if self.excl_line.as_ref().is_some_and(|f| f.is_match(line)) {
                        Some(FilterType::Both(number))
                    } else {
                        Some(FilterType::Branch(number))
                    }
                } else if self.excl_line.as_ref().is_some_and(|f| f.is_match(line)) {
                    Some(FilterType::Line(number))
                } else {
                    None
                }
            })
            .collect()
    }
}
