//! `fm` is a simple non-backtracking fuzzy text matcher useful for matching multi-line patterns
//! and text. At its most basic the wildcard operator (`...` by default) can be used in the
//! following ways:
//!
//!   * If a line consists solely of `...` it means "match zero or more lines of text".
//!   * If a line starts with `...`, the search is not anchored to the start of the line.
//!   * If a line ends with `...`, the search is not anchored to the end of the line.
//!
//! Note that `...` can appear both at the start and end of a line and if a line consists of
//! `......` (i.e. starts and ends with the wildcard with nothing inbetween), it will match exactly
//! one line. If the wildcard operator appears in any other locations, it is matched literally.
//! Wildcard matching does not backtrack, so if a line consists solely of `...` then the next
//! matching line anchors the remainder of the search.
//!
//! The following examples show `fm` in action using its defaults (i.e. `...` as the wildcard
//! operator, and leading/trailing whitespace ignored):
//!
//! ```rust
//! use fm::FMatcher;
//!
//! assert!(FMatcher::new("a").unwrap().matches("a").is_ok());
//! assert!(FMatcher::new(" a ").unwrap().matches("a").is_ok());
//! assert!(FMatcher::new("a").unwrap().matches("b").is_err());
//! assert!(FMatcher::new("a\n...\nb").unwrap().matches("a\na\nb").is_ok());
//! assert!(FMatcher::new("a\n...\nb").unwrap().matches("a\na\nb\nb").is_err());
//! ```
//!
//! When a match fails, the matcher returns an error indicating the line number at which the match
//! failed. The error can be formatted for human comprehension using the provided `Display`
//! implementation.
//!
//! If you want to use non-default options, you will first need to use `FMBuilder` before having
//! access to an `FMatcher`. For example, to use "name matching" (where you specify that the same
//! chunk of text must appear at multiple points in the text, but without specifying exactly what
//! the chunk must contain) you can set options as follows:
//!
/// ```rust
/// use {fm::FMBuilder, regex::Regex};
///
/// let ptn_re = Regex::new(r"\$.+?\b").unwrap();
/// let text_re = Regex::new(r".+?\b").unwrap();
/// let matcher = FMBuilder::new("$1 $1")
///                         .unwrap()
///                         .name_matcher(ptn_re, text_re)
///                         .build()
///                         .unwrap();
/// assert!(matcher.matches("a a").is_ok());
/// assert!(matcher.matches("a b").is_err());
/// ```
use std::{
    collections::hash_map::{Entry, HashMap},
    default::Default,
    error::Error,
    fmt,
    str::Lines,
};

use regex::Regex;

const ERROR_CONTEXT: usize = 3;
const WILDCARD: &str = "...";
const ERROR_MARKER: &str = ">>";

#[derive(Debug)]
struct FMOptions {
    name_matchers: Vec<(Regex, Regex)>,
    distinct_name_matching: bool,
    ignore_leading_whitespace: bool,
    ignore_trailing_whitespace: bool,
    ignore_surrounding_blank_lines: bool,
}

impl Default for FMOptions {
    fn default() -> Self {
        FMOptions {
            name_matchers: Vec::new(),
            distinct_name_matching: false,
            ignore_leading_whitespace: true,
            ignore_trailing_whitespace: true,
            ignore_surrounding_blank_lines: true,
        }
    }
}

/// Build up a `FMatcher` allowing the setting of options.
///
/// ```rust
/// use {fm::FMBuilder, regex::Regex};
///
/// let ptn_re = Regex::new(r"\$.+?\b").unwrap();
/// let text_re = Regex::new(r".+?\b").unwrap();
/// let matcher = FMBuilder::new("$1 $1")
///                         .unwrap()
///                         .name_matcher(ptn_re, text_re)
///                         .build()
///                         .unwrap();
/// assert!(matcher.matches("a a").is_ok());
/// assert!(matcher.matches("a b").is_err());
/// ```
#[derive(Debug)]
pub struct FMBuilder<'a> {
    ptn: &'a str,
    options: FMOptions,
}

impl<'a> FMBuilder<'a> {
    /// Create a new `FMBuilder` with default options.
    pub fn new(ptn: &'a str) -> Result<Self, Box<dyn Error>> {
        Ok(FMBuilder {
            ptn,
            options: FMOptions::default(),
        })
    }

    /// Add a name matcher `(ptn_re, text_re)`. Name matchers allow you to ensure that different
    /// parts of the text match without specifying precisely what they match. For example, if you
    /// have output where you want to ensure that two locations always match the same name, but the
    /// name is non-deterministic you can allow the use of `$` wildcards in your pattern:
    ///
    /// ```rust
    /// use {fm::FMBuilder, regex::Regex};
    ///
    /// let ptn_re = Regex::new(r"\$.+?\b").unwrap();
    /// let text_re = Regex::new(r".+?\b").unwrap();
    /// let matcher = FMBuilder::new("$1 b $1")
    ///                         .unwrap()
    ///                         .name_matcher(ptn_re, text_re)
    ///                         .build()
    ///                         .unwrap();
    /// assert!(matcher.matches("a b a").is_ok());
    /// assert!(matcher.matches("a b b").is_err());
    /// ```
    ///
    /// Note that if a line in the pattern uses name matching, it can *only* use the wildcard
    /// operator at the end of the line (so, for the above name matcher, `$1...` is allowed but
    /// `...$1` or `...$1...` is not allowed). Invalid combinations of wildcards and name matching
    /// are caught when a pattern is built.
    ///
    /// Multiple name matchers are allowed: they are matched in the order they were added to
    /// `FMBuilder`.
    pub fn name_matcher(mut self, ptn_re: Regex, text_re: Regex) -> Self {
        self.options.name_matchers.push((ptn_re, text_re));
        self
    }

    /// If `yes`, then different names cannot match the same text value. For example if `$1` binds
    /// to `a` then `$2` will refuse to match against `a` (though `$1` will continue to match
    /// against only `a`). Defaults to `false`.
    pub fn distinct_name_matching(mut self, yes: bool) -> Self {
        self.options.distinct_name_matching = yes;
        self
    }

    /// If `yes`, then each line's leading whitespace will be ignored in both pattern and text;
    /// otherwise leading whitespace must match. Defaults to `true`.
    pub fn ignore_leading_whitespace(mut self, yes: bool) -> Self {
        self.options.ignore_leading_whitespace = yes;
        self
    }

    /// If `yes`, then each line's trailing whitespace will be ignored in both pattern and text;
    /// otherwise trailing whitespace must match. Defaults to `true`.
    pub fn ignore_trailing_whitespace(mut self, yes: bool) -> Self {
        self.options.ignore_trailing_whitespace = yes;
        self
    }

    /// If `yes`, blank lines at the start and end of both the pattern and text are ignored for
    /// matching purposes.
    pub fn ignore_surrounding_blank_lines(mut self, yes: bool) -> Self {
        self.options.ignore_surrounding_blank_lines = yes;
        self
    }

    /// Turn this `FMBuilder` into a `FMatcher`.
    pub fn build(self) -> Result<FMatcher<'a>, Box<dyn Error>> {
        self.validate()?;
        Ok(FMatcher {
            ptn: self.ptn,
            options: self.options,
        })
    }

    fn validate(&self) -> Result<(), Box<dyn Error>> {
        for (ref ptn_re, _) in &self.options.name_matchers {
            for (i, l) in self.ptn.lines().enumerate() {
                let l = l.trim();
                if l.starts_with("...") && ptn_re.is_match(l) {
                    return Err(Box::<dyn Error>::from(format!(
                        "Can't mix name matching with wildcards at start of line {}.",
                        i + 1
                    )));
                }
            }
        }
        Ok(())
    }
}

/// The fuzzy matcher.
#[derive(Debug)]
pub struct FMatcher<'a> {
    ptn: &'a str,
    options: FMOptions,
}

impl<'a> FMatcher<'a> {
    /// A convenience method that automatically builds a pattern for you using `FMBuilder`'s
    /// default options.
    pub fn new(ptn: &'a str) -> Result<FMatcher, Box<dyn Error>> {
        FMBuilder::new(ptn)?.build()
    }

    /// Does this fuzzy matcher match `text`?
    pub fn matches(&self, text: &str) -> Result<(), FMatchError> {
        let mut names = HashMap::new();
        let mut ptn_lines = self.ptn.lines();
        let (mut ptnl, mut ptn_lines_off) = self.skip_blank_lines(&mut ptn_lines, None);
        ptn_lines_off += 1;
        let mut text_lines = text.lines();
        let (mut textl, mut text_lines_off) = self.skip_blank_lines(&mut text_lines, None);
        text_lines_off += 1;
        loop {
            match (ptnl, textl) {
                (Some(x), Some(y)) => {
                    if x.trim() == WILDCARD {
                        ptnl = ptn_lines.next();
                        ptn_lines_off += 1;
                        match ptnl {
                            Some(x) => {
                                while let Some(y) = textl {
                                    text_lines_off += 1;
                                    if self.match_line(&mut names, x, y) {
                                        break;
                                    }
                                    textl = text_lines.next();
                                }
                                text_lines_off -= 1;
                            }
                            None => return Ok(()),
                        }
                    } else if self.match_line(&mut names, x, y) {
                        ptnl = ptn_lines.next();
                        ptn_lines_off += 1;
                        textl = text_lines.next();
                        text_lines_off += 1;
                    } else {
                        return Err(FMatchError {
                            ptn: self.ptn.to_owned(),
                            text: text.to_owned(),
                            ptn_line_off: ptn_lines_off,
                            text_line_off: text_lines_off,
                        });
                    }
                }
                (None, None) => return Ok(()),
                (Some(x), None) => {
                    if x.trim() == WILDCARD {
                        while let Some(ptnl) = ptn_lines.next() {
                            ptn_lines_off += 1;
                            if !self.match_line(&mut names, ptnl, "") {
                                return Err(FMatchError {
                                    ptn: self.ptn.to_owned(),
                                    text: text.to_owned(),
                                    ptn_line_off: ptn_lines_off,
                                    text_line_off: text_lines_off,
                                });
                            }
                        }
                        return Ok(());
                    } else {
                        match self.skip_blank_lines(&mut ptn_lines, Some(x)) {
                            (Some(_), skipped) => {
                                return Err(FMatchError {
                                    ptn: self.ptn.to_owned(),
                                    text: text.to_owned(),
                                    ptn_line_off: ptn_lines_off + skipped,
                                    text_line_off: text_lines_off,
                                });
                            }
                            (None, _) => return Ok(()),
                        }
                    }
                }
                (None, Some(x)) => {
                    let (x, skipped) = self.skip_blank_lines(&mut text_lines, Some(x));
                    if x.is_none() {
                        return Ok(());
                    }
                    return Err(FMatchError {
                        ptn: self.ptn.to_owned(),
                        text: text.to_owned(),
                        ptn_line_off: ptn_lines_off,
                        text_line_off: text_lines_off + skipped,
                    });
                }
            }
        }
    }

    /// Skip blank lines in the input if `options.ignore_surrounding_blank_lines` is set. If `line`
    /// is `Some(...)` that is taken as the first line of the input and after that is processesd
    /// the `lines` iterator is used. The contents of the first non-blank line are returned as well
    /// as the number of lines skipped. Notice that this is intended *only* to skip blank lines at
    /// the start and end of a string, as it is predicated on the `ignore_surrounding_blank_lines`
    /// option (i.e. don't use this to skip blank lines in the middle of the input, because that
    /// will fail if the user sets `ignore_surrounding_blank_lines` to `false`!).
    #[allow(clippy::while_let_on_iterator)]
    fn skip_blank_lines(
        &self,
        lines: &mut Lines<'a>,
        line: Option<&'a str>,
    ) -> (Option<&'a str>, usize) {
        if !self.options.ignore_surrounding_blank_lines {
            if line.is_some() {
                return (line, 0);
            }
            return (lines.next(), 0);
        }
        let mut trimmed = 0;
        if let Some(l) = line {
            if !l.trim().is_empty() {
                return (Some(l), 0);
            }
            trimmed += 1;
        }
        while let Some(l) = lines.next() {
            if !l.trim().is_empty() {
                return (Some(l), trimmed);
            }
            trimmed += 1;
        }
        (None, trimmed)
    }

    /// Try matching `ptn` against `text`. If, and only if, the match is successful, `names` will
    /// be updated with matched names.
    fn match_line<'b>(
        &self,
        names: &mut HashMap<&'a str, &'b str>,
        mut ptn: &'a str,
        mut text: &'b str,
    ) -> bool {
        if self.options.ignore_leading_whitespace {
            ptn = ptn.trim_start();
            text = text.trim_start();
        }

        if self.options.ignore_trailing_whitespace {
            ptn = ptn.trim_end();
            text = text.trim_end();
        }

        let sww = ptn.starts_with(WILDCARD);
        let eww = ptn.ends_with(WILDCARD);
        if sww && eww {
            text.contains(&ptn[WILDCARD.len()..ptn.len() - WILDCARD.len()])
        } else if sww {
            text.ends_with(&ptn[WILDCARD.len()..])
        } else if self.options.name_matchers.is_empty() {
            if eww {
                text.starts_with(&ptn[..ptn.len() - WILDCARD.len()])
            } else {
                ptn == text
            }
        } else {
            let mut new_names = HashMap::new();
            loop {
                let mut matched = false;
                for (ref ptn_re, ref text_re) in &self.options.name_matchers {
                    if let Some(ptnm) = ptn_re.find(ptn) {
                        matched = true;
                        if ptnm.start() == ptnm.end() {
                            panic!("Name pattern matched the empty string.");
                        }
                        #[allow(clippy::suspicious_operation_groupings)]
                        if ptnm.start() > text.len() || ptn[..ptnm.start()] != text[..ptnm.start()]
                        {
                            return false;
                        }
                        ptn = &ptn[ptnm.end()..];
                        text = &text[ptnm.start()..];
                        if let Some(textm) = text_re.find(text) {
                            if self.options.distinct_name_matching {
                                for (x, y) in names.iter().chain(new_names.iter()) {
                                    if x != &ptnm.as_str() && y == &textm.as_str() {
                                        return false;
                                    }
                                }
                            }
                            if textm.start() == textm.end() {
                                panic!("Text pattern matched the empty string.");
                            }
                            match names.entry(ptnm.as_str()) {
                                Entry::Occupied(e) => {
                                    if e.get() != &textm.as_str() {
                                        return false;
                                    }
                                }
                                Entry::Vacant(_) => match new_names.entry(ptnm.as_str()) {
                                    Entry::Occupied(e) => {
                                        if e.get() != &textm.as_str() {
                                            return false;
                                        }
                                    }
                                    Entry::Vacant(e) => {
                                        e.insert(textm.as_str());
                                    }
                                },
                            }
                            text = &text[textm.end()..];
                        } else {
                            return false;
                        }
                    }
                }
                if !matched {
                    break;
                }
            }
            if (eww && text.starts_with(&ptn[..ptn.len() - WILDCARD.len()])) || ptn == text {
                names.extend(new_names);
                true
            } else {
                false
            }
        }
    }
}

/// An error indicating a failed match.
/// The pattern and text are copied in so that the error isn't tied to their lifetimes.
#[derive(PartialEq)]
pub struct FMatchError {
    ptn: String,
    text: String,
    ptn_line_off: usize,
    text_line_off: usize,
}

impl FMatchError {
    pub fn ptn_line_off(&self) -> usize {
        self.ptn_line_off
    }

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

impl fmt::Display for FMatchError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        // Figure out how many characters are required for the line numbers margin.
        let err_mk_chars = ERROR_MARKER.chars().count() + ' '.len_utf8();

        let display_lines = |f: &mut fmt::Formatter, s: &str, mark_line: usize| -> fmt::Result {
            let mut i = 1;
            if mark_line.checked_sub(ERROR_CONTEXT + 2).is_some() {
                writeln!(f, "{}...", " ".repeat(err_mk_chars))?;
            }
            for line in s.lines() {
                if let Some(j) = mark_line.checked_sub(ERROR_CONTEXT) {
                    if i < j {
                        i += 1;
                        continue;
                    }
                }
                if mark_line == i {
                    write!(f, "{} ", ERROR_MARKER)?;
                } else {
                    write!(f, "{}", " ".repeat(err_mk_chars))?;
                }
                if line.is_empty() {
                    writeln!(f, "|")?;
                } else {
                    writeln!(f, "|{}", line)?;
                }
                i += 1;
                if let Some(j) = mark_line.checked_add(ERROR_CONTEXT) {
                    if i > j {
                        break;
                    }
                }
            }
            if mark_line == i {
                writeln!(f, "{}", ERROR_MARKER)?;
            } else if let Some(j) = mark_line.checked_add(ERROR_CONTEXT) {
                if i > j {
                    writeln!(f, "{}...", " ".repeat(err_mk_chars))?;
                }
            }
            Ok(())
        };

        writeln!(f, "Pattern (error at line {}):", self.ptn_line_off)?;
        display_lines(f, &self.ptn, self.ptn_line_off)?;
        writeln!(f, "\nText (error at line {}):", self.text_line_off)?;
        display_lines(f, &self.text, self.text_line_off)
    }
}

/// A short error message. We don't reuse the longer message from `Display` as a Rust panic
/// uses `Debug` and doesn't interpret formatting characters when printing the panic message.
impl fmt::Debug for FMatchError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Failed to match at line {}", self.text_line_off)
    }
}

impl Error for FMatchError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        None
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn defaults() {
        fn helper(ptn: &str, text: &str) -> bool {
            FMatcher::new(ptn).unwrap().matches(text).is_ok()
        }
        assert!(helper("", ""));
        assert!(helper("\n", ""));
        assert!(helper("", "\n"));
        assert!(helper("a", "a"));
        assert!(!helper("a", "ab"));
        assert!(helper("...", ""));
        assert!(helper("...", "a"));
        assert!(helper("...", "a\nb"));
        assert!(helper("...\na", "a"));
        assert!(helper("...\na\n...", "a"));
        assert!(helper("a\n...", "a"));
        assert!(helper("a\n...\nd", "a\nd"));
        assert!(helper("a\n...\nd", "a\nb\nc\nd"));
        assert!(!helper("a\n...\nd", "a\nb\nc"));
        assert!(helper("a\n...\nc\n...\ne", "a\nb\nc\nd\ne"));
        assert!(helper("a\n...\n...b", "a\nb"));
        assert!(helper("a\n...\nb...", "a\nb"));
        assert!(helper("a\n...\nb...", "a\nbc"));
        assert!(helper("a\nb...", "a\nbc"));
        assert!(!helper("a\nb...", "a\nb\nc"));
        assert!(helper("a\n...b...", "a\nb"));
        assert!(helper("a\n...b...", "a\nxbz"));
        assert!(helper("a\n...b...", "a\nbz"));
        assert!(helper("a\n...b...", "a\nxb"));
        assert!(!helper("a\n...b...", "a\nxb\nc"));
        assert!(!helper("a", "a\nb"));
        assert!(!helper("a\nb", "a"));
        assert!(!helper("a\n...\nb", "a"));
        assert!(helper("a\n", "a\n"));
        assert!(helper("a\n", "a"));
        assert!(helper("a", "a\n"));
        assert!(helper("a\n\n", "a\n\n"));
        assert!(helper("a\n\n", "a"));
        assert!(helper("a", "a\n\n"));
        assert!(!helper("a\n\nb", "a\n"));
        assert!(!helper("a\n", "a\n\nb"));
    }

    #[test]
    fn dont_ignore_surrounding_blank_lines() {
        fn helper(ptn: &str, text: &str) -> bool {
            FMBuilder::new(ptn)
                .unwrap()
                .ignore_surrounding_blank_lines(false)
                .build()
                .unwrap()
                .matches(text)
                .is_ok()
        }
        assert!(helper("", ""));
        assert!(!helper("\n", ""));
        assert!(!helper("", "\n"));
        assert!(helper("a\n", "a\n"));
        assert!(helper("a\n", "a"));
        assert!(helper("a", "a\n"));
        assert!(helper("a\n\n", "a\n\n"));
        assert!(!helper("a\n\n", "a"));
        assert!(!helper("a", "a\n\n"));
        assert!(!helper("a\n\nb", "a\n"));
        assert!(!helper("a\n", "a\n\nb"));
    }

    #[test]
    fn name_matcher() {
        let nameptn_re = Regex::new(r"\$.+?\b").unwrap();
        let name_re = Regex::new(r".+?\b").unwrap();
        let helper = |ptn: &str, text: &str| -> bool {
            FMBuilder::new(ptn)
                .unwrap()
                .name_matcher(nameptn_re.clone(), name_re.clone())
                .build()
                .unwrap()
                .matches(text)
                .is_ok()
        };
        assert!(helper("", ""));
        assert!(helper("a", "a"));
        assert!(!helper("a", "ab"));
        assert!(helper("...", ""));
        assert!(helper("...", "a"));
        assert!(helper("......", "a"));
        assert!(!helper("......", ""));
        assert!(helper("...", "a\nb"));
        assert!(!helper("......", "a\nb"));
        assert!(helper("...\na", "a"));
        assert!(helper("...\na\n...", "a"));
        assert!(helper("a\n...", "a"));
        assert!(helper("a\n...\nd", "a\nd"));
        assert!(helper("a\n...\nd", "a\nb\nc\nd"));
        assert!(!helper("a\n...\nd", "a\nb\nc"));
        assert!(helper("a\n...\nc\n...\ne", "a\nb\nc\nd\ne"));
        assert!(helper("a\n...\n...b", "a\nb"));
        assert!(helper("a\n...\nb...", "a\nb"));
        assert!(helper("a\n...\nb...", "a\nbc"));
        assert!(helper("a\nb...", "a\nbc"));
        assert!(!helper("a\nb...", "a\nb\nc"));
        assert!(helper("a\n...b...", "a\nb"));
        assert!(helper("a\n...b...", "a\nxbz"));
        assert!(helper("a\n...b...", "a\nbz"));
        assert!(helper("a\n...b...", "a\nxb"));
        assert!(!helper("a\n...b...", "a\nxb\nc"));

        assert!(!helper("$1", ""));
        assert!(helper("$1", "a"));
        assert!(helper("$1, $1", "a, a"));
        assert!(!helper("$1, $1", "a, b"));
        assert!(helper("$1, a, $1", "a, a, a"));
        assert!(!helper("$1, a, $1", "a, b, a"));
        assert!(!helper("$1, a, $1", "a, a, b"));
        assert!(helper("$1, $1, a", "a, a, a"));
        assert!(!helper("$1, $1, a", "a, a, b"));
        assert!(!helper("$1, $1, a", "a, b, a"));
        assert!(helper("$1 $2\n...\n$3 $2", "a X\nb Y\nc X"));
        assert!(!helper("ab$a", "a"));
        assert!(helper("$1\n$1...", "a\na b c"));
        assert!(!helper("$1\n$1...", "a\nb b c"));
        assert!(helper("$1\n$1...", "a\na b c"));
        assert!(helper("$1\n$1 b...", "a\na b c"));
        assert!(helper("$1\n$1 b c...", "a\na b c"));
        assert!(!helper("$1\n$1 b c...\n$1", "a\na b c"));
        assert!(!helper("$1\n$1 b c...\n$1", "a\na b c\na\nb"));
    }

    #[test]
    fn multiple_name_matchers() {
        let nameptn1_re = Regex::new(r"\$.+?\b").unwrap();
        let nameptn2_re = Regex::new(r"\&.+?\b").unwrap();
        let name_re = Regex::new(r".+?\b").unwrap();
        let helper = |ptn: &str, text: &str| -> bool {
            FMBuilder::new(ptn)
                .unwrap()
                .name_matcher(nameptn1_re.clone(), name_re.clone())
                .name_matcher(nameptn2_re.clone(), name_re.clone())
                .build()
                .unwrap()
                .matches(text)
                .is_ok()
        };

        assert!(!helper("$1", ""));
        assert!(helper("$1", "a"));
        assert!(helper("$1, $1", "a, a"));
        assert!(!helper("$1, $1", "a, b"));
        assert!(helper("$1, a, $1", "a, a, a"));
        assert!(!helper("$1, a, $1", "a, b, a"));
        assert!(!helper("$1, a, $1", "a, a, b"));
        assert!(helper("$1, $1, a", "a, a, a"));
        assert!(!helper("$1, $1, a", "a, a, b"));
        assert!(!helper("$1, $1, a", "a, b, a"));
        assert!(helper("$1 $2\n...\n$3 $2", "a X\nb Y\nc X"));
        assert!(!helper("ab$a", "a"));
        assert!(helper("$1\n$1...", "a\na b c"));
        assert!(!helper("$1\n$1...", "a\nb b c"));
        assert!(helper("$1\n$1...", "a\na b c"));
        assert!(helper("$1\n$1 b...", "a\na b c"));
        assert!(helper("$1\n$1 b c...", "a\na b c"));
        assert!(!helper("$1\n$1 b c...\n$1", "a\na b c"));
        assert!(!helper("$1\n$1 b c...\n$1", "a\na b c\na\nb"));

        assert!(!helper("&1", ""));
        assert!(helper("&1", "a"));
        assert!(helper("&1, &1", "a, a"));
        assert!(!helper("&1, &1", "a, b"));
        assert!(helper("&1, a, &1", "a, a, a"));
        assert!(!helper("&1, a, &1", "a, b, a"));
        assert!(!helper("&1, a, &1", "a, a, b"));
        assert!(helper("&1, &1, a", "a, a, a"));
        assert!(!helper("&1, &1, a", "a, a, b"));
        assert!(!helper("&1, &1, a", "a, b, a"));
        assert!(helper("&1 &2\n...\n&3 &2", "a X\nb Y\nc X"));
        assert!(!helper("ab&a", "a"));
        assert!(helper("&1\n&1...", "a\na b c"));
        assert!(!helper("&1\n&1...", "a\nb b c"));
        assert!(helper("&1\n&1...", "a\na b c"));
        assert!(helper("&1\n&1 b...", "a\na b c"));
        assert!(helper("&1\n&1 b c...", "a\na b c"));
        assert!(!helper("&1\n&1 b c...\n&1", "a\na b c"));
        assert!(!helper("&1\n&1 b c...\n&1", "a\na b c\na\nb"));

        assert!(helper("$1 &1", "a a"));
        assert!(helper("$1 &1", "a b"));
        assert!(helper("$1 &1 $1", "a b a"));
        assert!(helper("$1 &1 &1", "a b b"));
        assert!(!helper("$1 &1 &1", "a b a"));
        assert!(helper("$1 &2\n...\n$3 &2", "a X\nb Y\nc X"));
        assert!(helper("$1 &1\n$1 &1...", "a b\na b c d"));
        assert!(helper("$1 &1\n$1 &1...", "a b\na b"));
        assert!(!helper("$1 &1\n$1 &1...", "a b\na a c d"));
        assert!(!helper("$1 &1\n$1 &1 c...\n$1", "a b\na b c"));
        assert!(!helper("$1 &1\n$1 &1 c...\n$1", "a b\na b c\na\nb"));
    }

    #[test]
    fn error_lines() {
        let ptn_re = Regex::new("\\$.+?\\b").unwrap();
        let text_re = Regex::new(".+?\\b").unwrap();
        let helper = |ptn: &str, text: &str| -> (usize, usize) {
            let err = FMBuilder::new(ptn)
                .unwrap()
                .name_matcher(ptn_re.clone(), text_re.clone())
                .build()
                .unwrap()
                .matches(text)
                .unwrap_err();
            (err.ptn_line_off(), err.text_line_off())
        };

        assert_eq!(helper("a\n...\nd", "a\nb\nc"), (3, 3));
        assert_eq!(helper("a\nb...", "a\nb\nc"), (3, 3));
        assert_eq!(helper("a\n...b...", "a\nxb\nc"), (3, 3));

        assert_eq!(helper("a\n\nb", "a\n"), (3, 2));
        assert_eq!(helper("a\n", "a\n\nb"), (2, 3));

        assert_eq!(helper("$1", ""), (1, 1));
        assert_eq!(helper("$1, $1", "a, b"), (1, 1));
        assert_eq!(helper("$1, a, $1", "a, b, a"), (1, 1));
        assert_eq!(helper("$1, a, $1", "a, a, b"), (1, 1));
        assert_eq!(helper("$1, $1, a", "a, a, b"), (1, 1));
        assert_eq!(helper("$1, $1, a", "a, b, a"), (1, 1));

        assert_eq!(helper("$1", ""), (1, 1));
        assert_eq!(helper("$1\n$1", "a\nb"), (2, 2));
        assert_eq!(helper("$1\na\n$1", "a\nb\na"), (2, 2));
        assert_eq!(helper("$1\na\n$1", "a\na\nb"), (3, 3));
        assert_eq!(helper("$1\n$1\na", "a\na\nb"), (3, 3));
        assert_eq!(helper("$1\n$1\na", "a\nb\na"), (2, 2));

        assert_eq!(helper("...\nb\nc\nd\n", "a\nb\nc\n0\ne"), (4, 4));
        assert_eq!(helper("...\nc\nd\n", "a\nb\nc\n0\ne"), (3, 4));
        assert_eq!(helper("...\nd\n", "a\nb\nc\n0\ne"), (2, 5));
    }

    #[test]
    #[should_panic]
    fn empty_name_pattern() {
        let ptn_re = Regex::new("").unwrap();
        let text_re = Regex::new(".+?\\b").unwrap();
        FMBuilder::new("$1")
            .unwrap()
            .name_matcher(ptn_re, text_re)
            .build()
            .unwrap()
            .matches("x")
            .unwrap();
    }

    #[test]
    #[should_panic]
    fn empty_text_pattern() {
        let ptn_re = Regex::new("\\$.+?\\b").unwrap();
        let text_re = Regex::new("").unwrap();
        FMBuilder::new("$1")
            .unwrap()
            .name_matcher(ptn_re, text_re)
            .build()
            .unwrap()
            .matches("x")
            .unwrap();
    }

    #[test]
    fn wildcards_and_names() {
        let ptn_re = Regex::new("\\$.+?\\b").unwrap();
        let text_re = Regex::new("").unwrap();
        let builder = FMBuilder::new("$1\n...$1abc")
            .unwrap()
            .name_matcher(ptn_re, text_re);
        assert_eq!(
            &(*(builder.build().unwrap_err())).to_string(),
            "Can't mix name matching with wildcards at start of line 2."
        );
    }

    #[test]
    fn distinct_names() {
        let nameptn_re = Regex::new(r"\$.+?\b").unwrap();
        let name_re = Regex::new(r".+?\b").unwrap();
        let helper = |ptn: &str, text: &str| -> bool {
            FMBuilder::new(ptn)
                .unwrap()
                .name_matcher(nameptn_re.clone(), name_re.clone())
                .distinct_name_matching(true)
                .build()
                .unwrap()
                .matches(text)
                .is_ok()
        };

        assert!(helper("$1 $1", "a a"));
        assert!(!helper("$1 $1", "a b"));
        assert!(!helper("$1 $2", "a a"));
    }

    #[test]
    fn error_display() {
        let ptn_re = Regex::new("\\$.+?\\b").unwrap();
        let text_re = Regex::new(".+?\\b").unwrap();
        let helper = |ptn: &str, text: &str| -> String {
            let err = FMBuilder::new(ptn)
                .unwrap()
                .name_matcher(ptn_re.clone(), text_re.clone())
                .build()
                .unwrap()
                .matches(text)
                .unwrap_err();
            format!("{}", err)
        };

        assert_eq!(
            helper("a\nb\nc\nd\n", "a\nb\nc\nz\nd\n"),
            "Pattern (error at line 4):
   |a
   |b
   |c
>> |d

Text (error at line 4):
   |a
   |b
   |c
>> |z
   |d
"
        );

        assert_eq!(
            helper("a\n", "a\n\nb"),
            "Pattern (error at line 2):
   |a
>>

Text (error at line 3):
   |a
   |
>> |b
"
        );

        let mut ptn = String::new();
        let mut text = String::new();
        for i in 1..1000 {
            ptn.push_str(&format!("a{}\n", i));
            text.push_str(&format!("a{}\n", i));
        }
        for i in 1000..1100 {
            ptn.push_str(&format!("a{}\n", i));
            text.push_str(&format!("a{}\n", i + 1));
        }
        assert_eq!(
            helper(&ptn, &text),
            "Pattern (error at line 1000):
   ...
   |a997
   |a998
   |a999
>> |a1000
   |a1001
   |a1002
   |a1003
   ...

Text (error at line 1000):
   ...
   |a997
   |a998
   |a999
>> |a1001
   |a1002
   |a1003
   |a1004
   ...
"
        );
    }

    #[test]
    fn test_allow_whitespace() {
        let helper = |ptn: &str, text: &str| -> bool {
            FMBuilder::new(ptn)
                .unwrap()
                .ignore_leading_whitespace(false)
                .ignore_trailing_whitespace(false)
                .build()
                .unwrap()
                .matches(text)
                .is_ok()
        };

        assert!(helper("a\na", "a\na"));

        assert!(helper("a\n a", "a\n a"));
        assert!(!helper("a\n a", "a\na"));
        assert!(!helper("a\na", "a\n a"));

        assert!(helper("a\na ", "a\na "));
        assert!(!helper("a\na", "a\na "));
        assert!(!helper("a\na ", "a\na"));
    }
}
