use std::borrow::Cow;
use std::collections::BTreeSet;
use std::fs;
use std::num::{NonZeroU8, NonZeroUsize};
use std::str::{Utf8Error, from_utf8};
use std::sync::Arc;

use aho_corasick::AhoCorasick;
use bstr::{ByteSlice, ByteVec};
use levenshtein_automata::{Distance as LevenshteinDistance, LevenshteinAutomatonBuilder};
use pariter::IteratorExt;
use regex::bytes::{Regex, RegexBuilder};
use regex_automata::{meta::Regex as RegexSet, util::syntax};
use simd_csv::ByteRecord;

use crate::CliError;
use crate::CliResult;
use crate::collections::{ContextBuffer, HashMap, IndexMap, new_index_map};
use crate::config::{Config, Delimiter};
use crate::select::SelectedColumns;
use crate::urls::{LRUStems, LRUTrieMap, TaggedUrl};
use crate::util;

#[inline]
fn try_any<'c, F, E>(cells: impl Iterator<Item = &'c [u8]>, predicate: F) -> Result<bool, E>
where
    F: Fn(&'c [u8]) -> Result<bool, E>,
{
    for cell in cells {
        if predicate(cell)? {
            return Ok(true);
        }
    }

    Ok(false)
}

#[inline]
fn try_all<'c, F, E>(cells: impl Iterator<Item = &'c [u8]>, predicate: F) -> Result<bool, E>
where
    F: Fn(&'c [u8]) -> Result<bool, E>,
{
    for cell in cells {
        if !predicate(cell)? {
            return Ok(false);
        }
    }

    Ok(true)
}

#[inline]
fn try_sum<'c, F, E>(cells: impl Iterator<Item = &'c [u8]>, callback: F) -> Result<usize, E>
where
    F: Fn(&'c [u8]) -> Result<usize, E>,
{
    let mut total: usize = 0;

    for cell in cells {
        total += callback(cell)?;
    }

    Ok(total)
}

trait LevenshteinDistanceExt {
    fn is_match(&self) -> bool;
}

impl LevenshteinDistanceExt for LevenshteinDistance {
    fn is_match(&self) -> bool {
        matches!(self, LevenshteinDistance::Exact(_))
    }
}

fn count_overlapping_matches(regex: &Regex, haystack: &[u8]) -> usize {
    let mut count: usize = 0;
    let mut offset: usize = 0;

    while let Some(m) = regex.find_at(haystack, offset) {
        count += 1;

        if m.start() == offset {
            offset += 1;
        } else {
            offset = m.end();
        }
    }

    count
}

fn regex_set_replace_all<'a>(
    regex: &RegexSet,
    cell: &'a [u8],
    replacements: &'a [Vec<u8>],
) -> Cow<'a, [u8]> {
    let mut bytes = Vec::new();

    let mut last_match: Option<usize> = None;

    for captures in regex.captures_iter(cell) {
        if bytes.capacity() == 0 {
            bytes.reserve(cell.len());
        }

        let m = captures.get_match().unwrap();

        if let Some(end) = last_match {
            bytes.extend(&cell[end..m.start()]);
        } else {
            bytes.extend(&cell[..m.start()]);
        }

        captures.interpolate_bytes_into(cell, &replacements[m.pattern().as_usize()], &mut bytes);

        last_match.replace(m.end());
    }

    if let Some(end) = last_match {
        bytes.extend(&cell[end..]);
    } else {
        return Cow::Borrowed(cell);
    }

    Cow::Owned(bytes)
}

pub struct LevenshteinSet {
    dfas: Vec<levenshtein_automata::DFA>,
    case_insensitive: bool,
}

impl LevenshteinSet {
    fn query<'c>(&self, cell: &'c [u8]) -> Result<Cow<'c, str>, Utf8Error> {
        Ok(if self.case_insensitive {
            Cow::Owned(from_utf8(cell)?.to_lowercase())
        } else {
            Cow::Borrowed(from_utf8(cell)?)
        })
    }

    fn find_iter<'a>(
        &'a self,
        cell: &'a [u8],
    ) -> Result<impl Iterator<Item = usize> + 'a, Utf8Error> {
        let query = self.query(cell)?;

        Ok(self
            .dfas
            .iter()
            .enumerate()
            .filter(move |(_, dfa)| dfa.eval(query.as_bytes()).is_match())
            .map(|(i, _)| i))
    }

    fn find(&self, cell: &[u8]) -> Result<Option<usize>, Utf8Error> {
        self.find_iter(cell).map(|mut iter| iter.next())
    }

    fn is_match(&self, cell: &[u8]) -> Result<bool, Utf8Error> {
        Ok(self.find(cell)?.is_some())
    }
}

pub enum Matcher {
    Empty,
    NonEmpty,
    Substring(AhoCorasick, bool),
    Exact(Vec<u8>, bool),
    Regex(Regex),
    Regexes(Vec<Regex>),
    RegexSet(RegexSet),
    HashMap(HashMap<Vec<u8>, usize>, bool),
    UrlPrefix(LRUStems),
    UrlTrie(LRUTrieMap<usize>),
    Levenshtein(LevenshteinSet),
}

impl Matcher {
    pub fn is_match(&self, cell: &[u8]) -> Result<bool, Utf8Error> {
        Ok(match self {
            Self::Empty => cell.is_empty(),
            Self::NonEmpty => !cell.is_empty(),
            Self::Substring(pattern, case_insensitive) => {
                if *case_insensitive {
                    pattern.is_match(&cell.to_lowercase())
                } else {
                    pattern.is_match(cell)
                }
            }
            Self::Regex(pattern) => pattern.is_match(cell),
            Self::Regexes(_) => unreachable!(),
            Self::Exact(pattern, case_insensitive) => {
                if *case_insensitive {
                    &cell.to_lowercase() == pattern
                } else {
                    cell == pattern
                }
            }
            Self::RegexSet(set) => set.is_match(cell),
            Self::HashMap(patterns, case_insensitive) => {
                if *case_insensitive {
                    patterns.contains_key(&cell.to_lowercase())
                } else {
                    patterns.contains_key(cell)
                }
            }
            Self::UrlPrefix(stems) => stems.is_simplified_match(from_utf8(cell)?),
            Self::UrlTrie(trie) => trie.is_match(from_utf8(cell)?).unwrap_or(false),
            Self::Levenshtein(set) => set.is_match(cell)?,
        })
    }

    fn count(&self, cell: &[u8], overlapping: bool) -> Result<usize, Utf8Error> {
        Ok(match self {
            Self::Empty => {
                if cell.is_empty() {
                    1
                } else {
                    0
                }
            }
            Self::NonEmpty => {
                if cell.is_empty() {
                    0
                } else {
                    1
                }
            }
            Self::Substring(pattern, case_insensitive) => match (*case_insensitive, overlapping) {
                (true, false) => pattern.find_iter(&cell.to_lowercase()).count(),
                (false, false) => pattern.find_iter(cell).count(),
                (true, true) => pattern.find_overlapping_iter(&cell.to_lowercase()).count(),
                (false, true) => pattern.find_overlapping_iter(cell).count(),
            },
            Self::Regex(pattern) => {
                if !overlapping {
                    pattern.find_iter(cell).count()
                } else {
                    count_overlapping_matches(pattern, cell)
                }
            }
            Self::Exact(pattern, case_insensitive) => {
                if *case_insensitive {
                    if &cell.to_lowercase() == pattern {
                        1
                    } else {
                        0
                    }
                } else if cell == pattern {
                    1
                } else {
                    0
                }
            }
            Self::RegexSet(set) => {
                if overlapping {
                    unreachable!()
                }

                set.find_iter(cell).count()
            }
            Self::Regexes(patterns) => patterns
                .iter()
                .map(|pattern| count_overlapping_matches(pattern, cell))
                .sum(),
            Self::HashMap(patterns, case_insensitive) => {
                if *case_insensitive {
                    patterns.contains_key(&cell.to_lowercase()) as usize
                } else {
                    patterns.contains_key(cell) as usize
                }
            }
            Self::UrlPrefix(stems) => stems.is_simplified_match(from_utf8(cell)?) as usize,
            Self::UrlTrie(trie) => trie.is_match(from_utf8(cell)?).unwrap_or(false) as usize,
            Self::Levenshtein(set) => set.find_iter(cell)?.count(),
        })
    }

    fn breakdown(
        &self,
        cell: &[u8],
        overlapping: bool,
        counts: &mut [usize],
    ) -> Result<bool, Utf8Error> {
        let mut is_match = false;

        match self {
            Self::Empty
            | Self::NonEmpty
            | Self::Regex(_)
            | Self::Exact(_, _)
            | Self::UrlPrefix(_) => {
                unreachable!()
            }

            Self::Substring(pattern, case_insensitive) => match (*case_insensitive, overlapping) {
                (true, false) => {
                    for m in pattern.find_iter(&cell.to_lowercase()) {
                        counts[m.pattern().as_usize()] += 1;
                        is_match = true;
                    }
                }
                (false, false) => {
                    for m in pattern.find_iter(cell) {
                        counts[m.pattern().as_usize()] += 1;
                        is_match = true;
                    }
                }
                (true, true) => {
                    for m in pattern.find_overlapping_iter(&cell.to_lowercase()) {
                        counts[m.pattern().as_usize()] += 1;
                        is_match = true;
                    }
                }
                (false, true) => {
                    for m in pattern.find_overlapping_iter(cell) {
                        counts[m.pattern().as_usize()] += 1;
                        is_match = true;
                    }
                }
            },
            Self::RegexSet(set) => {
                if overlapping {
                    unreachable!()
                }

                for m in set.find_iter(cell) {
                    counts[m.pattern().as_usize()] += 1;
                    is_match = true;
                }
            }
            Self::Regexes(patterns) => {
                for (i, pattern) in patterns.iter().enumerate() {
                    counts[i] += count_overlapping_matches(pattern, cell);
                    is_match = true;
                }
            }
            Self::HashMap(patterns, case_insensitive) => {
                if *case_insensitive {
                    if let Some(id) = patterns.get(&cell.to_lowercase()) {
                        counts[*id] += 1;
                        is_match = true;
                    }
                } else if let Some(id) = patterns.get(cell) {
                    counts[*id] += 1;
                    is_match = true;
                }
            }
            Self::UrlTrie(trie) => {
                if let Ok(Some(id)) = trie.longest_matching_prefix_value(from_utf8(cell)?) {
                    counts[*id] += 1;
                    is_match = true;
                }
            }
            Self::Levenshtein(set) => {
                for id in set.find_iter(cell)? {
                    counts[id] += 1;
                    is_match = true;
                }
            }
        }

        Ok(is_match)
    }

    fn unique_matches(
        &self,
        cell: &[u8],
        overlapping: bool,
        matches: &mut BTreeSet<usize>,
    ) -> Result<(), Utf8Error> {
        match self {
            Self::Empty
            | Self::NonEmpty
            | Self::Regex(_)
            | Self::Exact(_, _)
            | Self::UrlPrefix(_) => {
                unreachable!()
            }

            Self::Substring(pattern, case_insensitive) => match (*case_insensitive, overlapping) {
                (true, false) => {
                    for m in pattern.find_iter(&cell.to_lowercase()) {
                        matches.insert(m.pattern().as_usize());
                    }
                }
                (false, false) => {
                    for m in pattern.find_iter(cell) {
                        matches.insert(m.pattern().as_usize());
                    }
                }
                (true, true) => {
                    for m in pattern.find_overlapping_iter(&cell.to_lowercase()) {
                        matches.insert(m.pattern().as_usize());
                    }
                }
                (false, true) => {
                    for m in pattern.find_overlapping_iter(cell) {
                        matches.insert(m.pattern().as_usize());
                    }
                }
            },
            Self::RegexSet(set) => {
                if overlapping {
                    unreachable!()
                }

                for m in set.find_iter(cell) {
                    matches.insert(m.pattern().as_usize());
                }
            }
            Self::Regexes(patterns) => {
                for (i, pattern) in patterns.iter().enumerate() {
                    if pattern.is_match(cell) {
                        matches.insert(i);
                    }
                }
            }
            Self::HashMap(patterns, case_insensitive) => {
                if *case_insensitive {
                    if let Some(id) = patterns.get(&cell.to_lowercase()) {
                        matches.insert(*id);
                    }
                } else if let Some(id) = patterns.get(cell) {
                    matches.insert(*id);
                }
            }
            Self::UrlTrie(trie) => {
                if let Ok(Some(id)) = trie.longest_matching_prefix_value(from_utf8(cell)?) {
                    matches.insert(*id);
                }
            }
            Self::Levenshtein(set) => {
                for id in set.find_iter(cell)? {
                    matches.insert(id);
                }
            }
        }

        Ok(())
    }

    fn replace<'a>(
        &self,
        cell: &'a [u8],
        replacements: &'a [Vec<u8>],
    ) -> Result<Cow<'a, [u8]>, Utf8Error> {
        Ok(match self {
            Self::Empty => {
                if cell.is_empty() {
                    Cow::Borrowed(&replacements[0])
                } else {
                    Cow::Borrowed(cell)
                }
            }
            Self::NonEmpty => {
                if cell.is_empty() {
                    Cow::Borrowed(cell)
                } else {
                    Cow::Borrowed(&replacements[0])
                }
            }
            Self::Substring(pattern, case_insensitive) => {
                if *case_insensitive {
                    unreachable!()
                } else {
                    Cow::Owned(pattern.replace_all_bytes(cell, replacements))
                }
            }
            Self::Regex(pattern) => pattern.replace_all(cell, &replacements[0]),
            Self::Exact(pattern, case_insensitive) => {
                if *case_insensitive {
                    if &cell.to_lowercase() == pattern {
                        Cow::Borrowed(&replacements[0])
                    } else {
                        Cow::Borrowed(cell)
                    }
                } else if cell == pattern {
                    Cow::Borrowed(&replacements[0])
                } else {
                    Cow::Borrowed(cell)
                }
            }
            Self::RegexSet(set) => regex_set_replace_all(set, cell, replacements),
            Self::Regexes(_) => unreachable!(),
            Self::HashMap(patterns, case_insensitive) => {
                if *case_insensitive {
                    if let Some(i) = patterns.get(&cell.to_lowercase()) {
                        Cow::Borrowed(&replacements[*i])
                    } else {
                        Cow::Borrowed(cell)
                    }
                } else if let Some(i) = patterns.get(&cell.to_lowercase()) {
                    Cow::Borrowed(&replacements[*i])
                } else {
                    Cow::Borrowed(cell)
                }
            }
            Self::UrlPrefix(stems) => match from_utf8(cell).ok() {
                None => Cow::Borrowed(cell),
                Some(url) => {
                    if stems.is_simplified_match(url) {
                        Cow::Borrowed(&replacements[0])
                    } else {
                        Cow::Borrowed(cell)
                    }
                }
            },
            Self::UrlTrie(trie) => {
                if let Ok(Some(i)) = trie.longest_matching_prefix_value(from_utf8(cell)?) {
                    Cow::Borrowed(&replacements[*i])
                } else {
                    Cow::Borrowed(cell)
                }
            }
            Self::Levenshtein(set) => {
                if let Some(i) = set.find(cell)? {
                    Cow::Borrowed(&replacements[i])
                } else {
                    Cow::Borrowed(cell)
                }
            }
        })
    }
}

// NOTE: a -U, --unbuffered flag that flushes on each match does not solve
// early termination when piping to `xan slice` because flush won't get a broken
// pipe when writing nothing.
static USAGE: &str = "
Search for (or replace) patterns in CSV data.

This command has several flags to select the way to perform a match:

    * (default): matching a substring (e.g. \"john\" in \"My name is john\")
    * -e, --exact: exact match
    * -r, --regex: using a regular expression
    * -u, --url-prefix: matching by url prefix (e.g. \"lemonde.fr/business\")
    * -L, --levenshtein <k>: matching using Levenshtein distance
    * -D, --damerau-levenshtein <k>: matching using Damerau-Levenshtein distance
    * -N, --non-empty: finding non-empty cells (does not need a pattern)
    * -E, --empty: finding empty cells (does not need a pattern)

Searching for rows with any column containing \"john\":

    $ xan search \"john\" file.csv > matches.csv

Searching for rows where any column has *exactly* the value \"john\":

    $ xan search -e \"john\" file.csv > matches.csv

Keeping only rows where selection is not fully empty:

    $ xan search -s user_id --non-empty file.csv > users-with-id.csv

Keeping only rows where selection has any empty column:

    $ xan search -s user_id --empty file.csv > users-without-id.csv

When using a regular expression, be sure to mind bash escape rules (prefer single
quotes around your expression and don't forget to use backslashes when needed):

    $ xan search -r '\\bfran[cç]' file.csv

To restrict the columns that will be searched you can use the -s, --select flag.

All search modes (except -u/--url-prefix) can also be case-insensitive
using -i, --ignore-case.

# Searching multiple patterns at once

This command is also able to search for multiple patterns at once.
To do so, you can either use the -P, --add-pattern flag or feed a text file
with one pattern per line to the --patterns flag. You can also feed a CSV file
to the --patterns flag, in which case you will need to indicate the column
containing the patterns using the --pattern-column flag.

Giving additional patterns:

    $ xan search disc -P tape -P vinyl file.csv > matches.csv

One pattern per line of text file:

    $ xan search --patterns patterns.txt file.csv > matches.csv

CSV column containing patterns:

    $ xan search --patterns people.csv --pattern-column name tweets.csv > matches.csv

Feeding patterns through stdin (using \"-\"):

    $ cat patterns.txt | xan search --patterns - file.csv > matches.csv

Feeding CSV column as patterns through stdin (using \"-\"):

    $ xan slice -l 10 people.csv | xan search --patterns - --pattern-column name file.csv > matches.csv

# Further than just filtering

Now this command is also able to perform search-adjacent operations:

    - Replacing matches with -R/--replace or --replacement-column
    - Reporting in a new column whether a match was found with -f/--flag
    - Reporting the total number of matches in a new column with -c/--count
    - Reporting a breakdown of number of matches per query given through --patterns
      with -b/--breakdown.
    - Reporting unique matches of multiple queries given through --patterns
      using -U/--unique-matches.

For instance:

Reporting whether a match was found (instead of filtering):

    $ xan search -s headline -i france -f france_match file.csv

Reporting number of matches:

    $ xan search -s headline -i france -c france_count file.csv

Cleaning thousands separators (usually commas \",\" in English) from numerical columns:

    $ xan search , --replace . -s 'count_*' file.csv

Replacing color names to their French counterpart:

    $ echo 'english,french\\nred,rouge\\ngreen,vert' | \\
    $ xan search -e \\
    $   --patterns - --pattern-column english --replacement-column french \\
    $   -s color file.csv > translated.csv

Computing a breakdown of matches per query:

    $ xan search -b -s headline --patterns queries.csv \\
    $   --pattern-column query --name-column name file.csv > breakdown.csv

Reporting unique matches per query in a new column:

    $ xan search -U matches -s headline,text --patterns queries.csv \\
    $   --pattern-column query --name-column name file.csv > matches.csv

# Regarding performance

*Parsing*

Note that this command has a -Z/--fast-parser able to leverage a faster, zero-copy
parser. This parser however does not bother unescaping CSV cells and you will need
to pay attention to CSV delimiters and/or quotes when writing your patterns (don't
worry, this is inconsequential most of the time).

The parser actually used will depend on whether you are targeting a selection of
columns through the -s/--select flag or not. The game here is that you need to
balance the cost of peforming a match over longer strings vs. the cost of parsing
the CSV stream.

Ultimately your mileage may vary, so bench away and see what is faster for your
actual use-case.

Also, this might seem counterintuitive but if your CSV data is never quoted and
your rows are small enough a dedicated tool like `ripgrep` should be even faster:

https://github.com/burntsushi/ripgrep

*Mulitple patterns*

This command goes to great length ensuring multiple patterns search are not a
simple loop testing each pattern in turn. Here is what is used for each mode:

    * (default): a unique Aho-Corasick automaton
    * -e, --exact: a hashmap
    * -r, --regex: a unique regex automaton
    * -u, --url-prefix: a specialized trie
    * -L, --levenshtein <k>: a set of levenshtein automata
    * -D, --damerau-levenshtein <k>: ditto

This also means that, even if Rust's regex engine is very clever, sometimes it
is faster to search for a set of substrings vs. a regex pattern.

*Parallelization*

Finally, this command can leverage multithreading to run faster using
the -p/--parallel or -t/--threads flags. This said, the boost given by
parallelization might differ a lot and depends on the complexity and number of
patterns and also on the size of the haystacks. That is to say `xan search --empty`
would not be significantly faster when parallelized whereas `xan search -i eternity`
definitely would.

Also, you might want to try `xan parallel cat` instead because it could be
faster in some scenarios at the cost of an increase in memory usage (with the
caveat that it cannot work on a single compressed files nor on streams).

For instance, the following `search` command:

    $ xan search -i eternity -p file.csv

Would directly translate to:

    $ xan parallel cat -P 'search -i eternity' file.csv

# Regarding encoding

This command usually does not care about the input's encoding. However, some
search modes need to operate on unicode characters directely and therefore expect
valid UTF-8. This is only the case for -u/--url-prefix, -L/--levenshtein
and -D/--damerau-levenshtein.

Usage:
    xan search [options] --non-empty [<input>]
    xan search [options] --empty [<input>]
    xan search [options] --pattern-file <path> [<input>]
    xan search [options] --patterns <index> [<input>]
    xan search [options] <pattern> [-P <pattern>...] [<input>]
    xan search --help

search mode options:
    -e, --exact            Perform an exact match.
    -r, --regex            Use a regex to perform the match.
    -E, --empty            Search for empty cells, i.e. filter out
                           any completely non-empty selection.
    -N, --non-empty        Search for non-empty cells, i.e. filter out
                           any completely empty selection.
    -u, --url-prefix       Match by url prefix, i.e. cells must contain urls
                           matching the searched url prefix. Urls are first
                           reordered using a scheme called a LRU, that you can
                           read about here:
                           https://github.com/medialab/ural?tab=readme-ov-file#about-lrus
    -L, --levenshtein <k>  Match if Levensthein distance between cell & pattern is
                           less than or equal to given <k> threshold. For performance
                           reasons and to avoid absurd memory consumption, <k> is
                           disallowed to be greater than 5.
    -D, --damerau-levenshtein <k>
                           Same as -L/--levenshtein, but set transposition cost
                           to 1 instead of 2. Useful to match more usual typos without
                           increasing <k> too much.

search options:
    -i, --ignore-case          Case insensitive search.
    -v, --invert-match         Select only rows that did not match
    -s, --select <arg>         Select the columns to search. See 'xan select -h'
                               for the full syntax.
    --every-column             Only output a row when every selected column matches
                               a pattern, instead of the default behavior outputting
                               a row if any selected column matches.
    -f, --flag <column>        Instead of filtering rows, add a new column indicating if any match
                               was found.
    -c, --count <column>       Report the number of non-overlapping pattern matches in a new column with
                               given name. Will still filter out rows with 0 matches, unless --left
                               is used. Does not work with -v/--invert-match.
    --overlapping              When used with -c/--count or -b/--breakdown, return the count of
                               overlapping matches. Note that this can sometimes be one order of
                               magnitude slower that counting non-overlapping matches.
    -R, --replace <with>       If given, the command will not filter rows but will instead
                               replace matches with the given replacement.
                               Does not work with --replacement-column.
                               Regex replacement string syntax can be found here:
                               https://docs.rs/regex/latest/regex/struct.Regex.html#replacement-string-syntax
    -l, --limit <n>            Maximum of number rows to return. Useful to avoid downstream
                               buffering some times (e.g. when searching for very few
                               rows in a big file before piping to `view` or `flatten`).
    -B, --before-context <n>   Number of rows to keep before a matching one.
    -A, --after-context <n>    Number of rows to keep after a matching one.
    --left                     Rows without any matches will be kept in the output when
                               using -U/--unique-matches, or -b/--breakdown, or -c/--count.
    -x, --pattern-file <path>  Read pattern from given file <path> and trims it or toggle verbose mode `(?x)`
                               when using -r/--regex (this means the regex can use whitespace and comments).
                               This can be useful your pattern is complex and unwieldy. Must not be confused
                               with --patterns that should be used for multiple patterns.
    -Z, --fast-parser          Use a faster, zero-copy parser when searching the file.
                               Note that no normalization of the input format will be applied when
                               used without -s/--select. Also, this can only work using this command's
                               default mode, i.e. filtering, and does not work with parallelization.
                               Note that this parser is also unable to unescape CSV cells. This means
                               you must take care of considering quotes to be doubled, and when using
                               this flag without -s/--select, the command will attempt to match the
                               whole row at once, so it may contain raw delimiters & newlines.
    -p, --parallel             Whether to use parallelization to speed up computation.
                               Will automatically select a suitable number of threads to use
                               based on your number of cores. Use -t, --threads if you want to
                               indicate the number of threads yourself.
    -t, --threads <threads>    Parellize computations using this many threads. Use -p, --parallel
                               if you want the number of threads to be automatically chosen instead.

multiple patterns options:
    -P, --add-pattern <pattern>  Manually add patterns to query without needing to feed a file
                                 to the --patterns flag.
    -b, --breakdown              When used with --patterns, will count the total number of
                                 non-overlapping matches per pattern and write this count in
                                 one additional column per pattern. Added column will be given
                                 the pattern as name, unless you provide the --name-column flag.
                                 Will not include rows that have no matches in the output, unless
                                 the --left flag is used. You might want to use it with --overlapping
                                 sometimes when your patterns are themselves overlapping or you might
                                 be surprised by the tallies.
    -U, --unique-matches <name>  When used with --patterns, will add a column containing a list of
                                 unique matched patterns for each row, separated by the --sep character.
                                 Will not include rows that have no matches in the output unless
                                 the --left flag is used. Patterns can also be given a name through
                                 the --name-column flag.
    --sep <char>                 Character to use to join pattern matches when using -U/--unique-matches.
                                 [default: |]
    --patterns <path>            Path to a text file (use \"-\" for stdin), containing multiple
                                 patterns, one per line, to search at once.
    --pattern-column <name>      When given a column name, --patterns file will be considered a CSV
                                 and patterns to search will be extracted from the given column.
    --replacement-column <name>  When given with both --patterns & --pattern-column, indicates the
                                 column containing a replacement when a match occurs. Does not
                                 work with -R/--replace.
                                 Regex replacement string syntax can be found here:
                                 https://docs.rs/regex/latest/regex/struct.Regex.html#replacement-string-syntax
    --name-column <name>         When given with -b/--breakdown, --patterns & --pattern-column,
                                 indicates the column containing a pattern's name that will be used
                                 as column name in the appended breakdown.

Common options:
    -h, --help             Display this message
    -o, --output <file>    Write output to <file> instead of stdout.
    -n, --no-headers       When set, the first row will not be interpreted
                           as headers. (i.e., They are not searched, analyzed,
                           sliced, etc.)
    -d, --delimiter <arg>  The field delimiter for reading CSV data.
                           Must be a single character.
";

#[derive(Deserialize)]
struct Args {
    arg_input: Option<String>,
    arg_pattern: Option<String>,
    flag_select: Option<SelectedColumns>,
    flag_output: Option<String>,
    flag_no_headers: bool,
    flag_delimiter: Option<Delimiter>,
    flag_invert_match: bool,
    flag_overlapping: bool,
    flag_every_column: bool,
    flag_ignore_case: bool,
    flag_empty: bool,
    flag_non_empty: bool,
    flag_exact: bool,
    flag_regex: bool,
    flag_url_prefix: bool,
    flag_levenshtein: Option<NonZeroU8>,
    flag_damerau_levenshtein: Option<NonZeroU8>,
    flag_flag: Option<String>,
    flag_count: Option<String>,
    flag_replace: Option<String>,
    flag_limit: Option<NonZeroUsize>,
    flag_breakdown: bool,
    flag_unique_matches: Option<String>,
    flag_sep: String,
    flag_left: bool,
    flag_patterns: Option<String>,
    flag_pattern_column: Option<SelectedColumns>,
    flag_replacement_column: Option<SelectedColumns>,
    flag_add_pattern: Vec<String>,
    flag_name_column: Option<SelectedColumns>,
    flag_parallel: bool,
    flag_threads: Option<usize>,
    flag_fast_parser: bool,
    flag_before_context: Option<NonZeroUsize>,
    flag_after_context: Option<NonZeroUsize>,
    flag_pattern_file: Option<String>,
}

#[inline(always)]
fn check_limit(limit_opt: Option<NonZeroUsize>, is_match: bool, matches_count: &mut usize) -> bool {
    if let Some(limit) = limit_opt {
        if is_match {
            *matches_count += 1;
        }

        if *matches_count >= limit.get() {
            return false;
        }
    }

    true
}

impl Args {
    fn resolve(&mut self) -> CliResult<()> {
        if let Some(path) = &self.flag_pattern_file {
            self.arg_pattern = Some(fs::read_to_string(path)?.trim().to_string());
        }

        Ok(())
    }

    fn is_replacing(&self) -> bool {
        self.flag_replace.is_some() || self.flag_replacement_column.is_some()
    }

    fn build_matcher(&self, patterns: &Option<Vec<String>>) -> Result<Matcher, CliError> {
        if self.flag_non_empty {
            return Ok(Matcher::NonEmpty);
        }

        if self.flag_empty {
            return Ok(Matcher::Empty);
        }

        let verbose_regex = self.flag_pattern_file.is_some();

        match patterns {
            None => {
                let pattern = self.arg_pattern.as_ref().unwrap();

                Ok(if self.flag_exact {
                    if self.flag_ignore_case {
                        Matcher::Exact(pattern.as_bytes().to_lowercase(), true)
                    } else {
                        Matcher::Exact(pattern.as_bytes().to_vec(), false)
                    }
                } else if self.flag_regex {
                    Matcher::Regex(
                        RegexBuilder::new(pattern)
                            .case_insensitive(self.flag_ignore_case)
                            .ignore_whitespace(verbose_regex)
                            .build()?,
                    )
                } else if self.flag_url_prefix {
                    let tagged_url = pattern.parse::<TaggedUrl>()?;

                    Matcher::UrlPrefix(LRUStems::from_tagged_url(&tagged_url, true))
                } else if let Some(k) = self.flag_levenshtein.or(self.flag_damerau_levenshtein) {
                    Matcher::Levenshtein(LevenshteinSet {
                        dfas: vec![
                            LevenshteinAutomatonBuilder::new(
                                k.get(),
                                self.flag_damerau_levenshtein.is_some(),
                            )
                            .build_dfa(
                                &(if self.flag_ignore_case {
                                    pattern.to_lowercase()
                                } else {
                                    pattern.to_string()
                                }),
                            ),
                        ],
                        case_insensitive: self.flag_ignore_case,
                    })
                } else if self.is_replacing() {
                    Matcher::Regex(
                        RegexBuilder::new(&regex::escape(pattern))
                            .case_insensitive(self.flag_ignore_case)
                            .ignore_whitespace(verbose_regex)
                            .build()?,
                    )
                } else {
                    Matcher::Substring(
                        AhoCorasick::new([if self.flag_ignore_case {
                            pattern.to_lowercase()
                        } else {
                            pattern.to_string()
                        }])?,
                        self.flag_ignore_case,
                    )
                })
            }
            Some(patterns) => Ok(if self.flag_exact {
                let mut map = HashMap::with_capacity(patterns.len());

                for (i, pattern) in patterns.iter().enumerate() {
                    map.insert(
                        if self.flag_ignore_case {
                            pattern.to_lowercase().into_bytes()
                        } else {
                            pattern.to_string().into_bytes()
                        },
                        i,
                    );
                }

                Matcher::HashMap(map, self.flag_ignore_case)
            } else if self.flag_regex {
                if self.flag_overlapping {
                    Matcher::Regexes(
                        patterns
                            .iter()
                            .map(|pattern| {
                                RegexBuilder::new(pattern)
                                    .case_insensitive(self.flag_ignore_case)
                                    .ignore_whitespace(verbose_regex)
                                    .build()
                                    .map_err(CliError::from)
                            })
                            .collect::<Result<Vec<_>, _>>()?,
                    )
                } else {
                    Matcher::RegexSet(
                        RegexSet::builder()
                            .syntax(
                                syntax::Config::new()
                                    .case_insensitive(self.flag_ignore_case)
                                    .ignore_whitespace(verbose_regex),
                            )
                            .build_many(&patterns.iter().collect::<Vec<_>>())?,
                    )
                }
            } else if self.flag_url_prefix {
                let mut trie = LRUTrieMap::new_simplified();

                for (i, url) in patterns.iter().enumerate() {
                    trie.insert(url, i)?;
                }

                Matcher::UrlTrie(trie)
            } else if let Some(k) = self.flag_levenshtein.or(self.flag_damerau_levenshtein) {
                Matcher::Levenshtein(LevenshteinSet {
                    dfas: patterns
                        .iter()
                        .map(|pattern| {
                            LevenshteinAutomatonBuilder::new(
                                k.get(),
                                self.flag_damerau_levenshtein.is_some(),
                            )
                            .build_dfa(
                                &(if self.flag_ignore_case {
                                    pattern.to_lowercase()
                                } else {
                                    pattern.to_string()
                                }),
                            )
                        })
                        .collect(),
                    case_insensitive: self.flag_ignore_case,
                })
            } else if self.is_replacing() {
                Matcher::RegexSet(
                    RegexSet::builder()
                        .syntax(
                            syntax::Config::new()
                                .case_insensitive(self.flag_ignore_case)
                                .ignore_whitespace(verbose_regex),
                        )
                        .build_many(
                            &patterns
                                .iter()
                                .map(|pattern| regex::escape(pattern))
                                .collect::<Vec<_>>(),
                        )?,
                )
            } else {
                Matcher::Substring(
                    AhoCorasick::new(
                        patterns
                            .iter()
                            .map(|pattern| {
                                if self.flag_ignore_case {
                                    pattern.to_lowercase()
                                } else {
                                    pattern.to_string()
                                }
                            })
                            .collect::<Vec<_>>(),
                    )?,
                    self.flag_ignore_case,
                )
            }),
        }
    }
}

pub fn run(argv: &[&str]) -> CliResult<()> {
    let mut args: Args = util::get_args(USAGE, argv)?;

    args.resolve()?;

    let matchers_count: u8 = args.flag_exact as u8
        + args.flag_regex as u8
        + args.flag_non_empty as u8
        + args.flag_empty as u8
        + args.flag_url_prefix as u8
        + args.flag_levenshtein.is_some() as u8
        + args.flag_damerau_levenshtein.is_some() as u8;

    if matchers_count > 1 {
        Err(
            "must select only one of -e/--exact, -N/--non-empty, -E/--empty, -u/--url-prefix, -L/--levenshtein, -D/--damerau-levenshtein or -r/--regex!",
        )?;
    }

    if args.flag_overlapping
        && args.flag_count.is_none()
        && !args.flag_breakdown
        && args.flag_unique_matches.is_none()
    {
        Err("--overlapping only works with -c/--count, -U/--unique-matches or -b/--breakdown!")?;
    }

    if (args.flag_count.is_some()
        || args.flag_flag.is_some()
        || args.flag_replace.is_some()
        || args.flag_breakdown
        || args.flag_unique_matches.is_some())
        && args.flag_invert_match
    {
        Err(
            "-c/--count, -f,--flag, -R/--replace, -b/--breakdown & -U/--unique-matches do not work with -v/--invert-match!",
        )?;
    }

    if (args.flag_empty || args.flag_non_empty)
        && (args.flag_patterns.is_some() || args.arg_pattern.is_some())
    {
        Err("-N/--non-empty & -E/--empty do not allow a pattern!")?;
    }

    if args.flag_ignore_case && args.flag_url_prefix {
        Err("-u/--url-prefix & -i/--ignore-case are not compatible!")?;
    }

    if args.flag_replacement_column.is_some()
        && (args.flag_patterns.is_none() || args.flag_pattern_column.is_none())
    {
        Err("--replacement-column requires both --patterns & --pattern-column!")?;
    }

    if args.flag_name_column.is_some()
        && (args.flag_patterns.is_none() || args.flag_pattern_column.is_none())
    {
        Err("--name-column requires both --patterns & --pattern-column!")?;
    }

    if !args.flag_add_pattern.is_empty() && args.flag_patterns.is_some() {
        Err("-P/--add-pattern is incompatible with --patterns!")?;
    }

    if matches!(args.flag_levenshtein, Some(n) if n.get() > 5) {
        Err(
            "for performance reasons, -L/--levenshtein does not allow k to be greater than 5 yet!",
        )?;
    }

    let actions_count: u8 = args.flag_count.is_some() as u8
        + args.flag_replace.is_some() as u8
        + args.flag_breakdown as u8
        + args.flag_replacement_column.is_some() as u8
        + args.flag_unique_matches.is_some() as u8
        + args.flag_flag.is_some() as u8;

    if actions_count > 1 {
        Err(
            "must use only one of -R/--replace, --replacement-column, -b/--breakdown, -c/--count, -f/--flag or -U/--unique-matches!",
        )?;
    }

    if actions_count > 0 && args.flag_fast_parser {
        Err(
            "-Z/--fast-parser only work with the command default mode.\nNo -R/--replace, --replacement-column, -b/--breakdown, -c/--count, -f/--flag nor -U/--unique-matches!",
        )?;
    }

    let tracking_context = args.flag_before_context.is_some() || args.flag_after_context.is_some();

    if actions_count > 0 && tracking_context {
        Err(
            "-B/--before-context & -A/--after-context only work with the command default mode.\nNo -R/--replace, --replacement-column, -b/--breakdown, -c/--count, -f/--flag nor -U/--unique-matches!",
        )?;
    }

    if args.flag_every_column && actions_count > 0 {
        Err(
            "--every-column does not work with -R/--replace, --replacement-column, -b/--breakdown, -c/--count nor -U/--unique-matches!",
        )?;
    }

    let threads = util::parallelization(args.flag_parallel, args.flag_threads);

    if threads.is_some() {
        if args.flag_fast_parser {
            Err("-Z/--fast-parser does not work with -p/--parallel nor -t/--threads!")?;
        }

        if tracking_context {
            Err(
                "-B/--before-context & -A/--after-context does not work with -p/--parallel nor -t/--threads!",
            )?;
        }
    }

    let mut pairs = args
        .flag_patterns
        .as_ref()
        .map(|path| {
            Config::new(&Some(path.clone()))
                .delimiter(args.flag_delimiter)
                .pairs((
                    &args.flag_pattern_column,
                    &args
                        .flag_replacement_column
                        .clone()
                        .or(args.flag_name_column.clone()),
                ))?
                .collect::<Result<Vec<_>, _>>()
        })
        .transpose()?
        .map(|pairs| {
            let (patterns, associated): (Vec<_>, Vec<_>) = pairs.into_iter().unzip();

            let associated = (args.flag_replacement_column.is_some()
                || args.flag_name_column.is_some())
            .then(|| {
                associated
                    .into_iter()
                    .map(|o| o.unwrap().into_bytes())
                    .collect::<Vec<_>>()
            });

            (patterns, associated)
        });

    if !args.flag_add_pattern.is_empty() {
        let mut additional_patterns = vec![args.arg_pattern.clone().unwrap()];

        for pattern in args.flag_add_pattern.iter() {
            additional_patterns.push(pattern.to_string());
        }

        pairs = Some((additional_patterns, None));
    }

    let (patterns, associated) = pairs.unzip();

    let matcher = args.build_matcher(&patterns)?;
    let patterns_len = patterns.as_ref().map(|p| p.len()).unwrap_or(1);

    let associated = associated.flatten().or_else(|| {
        args.flag_replace
            .as_ref()
            .map(|replacement| vec![replacement.clone().into_bytes(); patterns_len])
    });

    let consolidated_patterns = associated.as_ref().and_then(|names| {
        let mut map: IndexMap<&[u8], Vec<usize>> = new_index_map();

        for (i, name) in names.iter().enumerate() {
            map.entry(name)
                .and_modify(|indices| indices.push(i))
                .or_insert(vec![i]);
        }

        let groups = map.into_values().collect::<Vec<_>>();

        if groups.iter().all(|group| group.len() == 1) {
            None
        } else {
            Some(groups)
        }
    });

    let rconfig = Config::new(&args.arg_input)
        .delimiter(args.flag_delimiter)
        .no_headers(args.flag_no_headers)
        .select(args.flag_select.clone().unwrap_or_default());

    let mut wtr = Config::new(&args.flag_output).simd_writer()?;
    let mut matches_count: usize = 0;

    // Fast parser path, without selection
    if args.flag_fast_parser && args.flag_select.is_none() {
        let mut splitter = rconfig.simd_splitter()?;

        let mut context_buffer_opt = tracking_context.then(|| {
            ContextBuffer::<Vec<u8>>::new(args.flag_before_context, args.flag_after_context)
        });

        if !rconfig.no_headers {
            wtr.write_splitted_record(splitter.byte_headers()?)?;
        }

        while let Some(record) = splitter.split_record()? {
            let mut is_match = matcher.is_match(record)?;

            if args.flag_invert_match {
                is_match = !is_match;
            }

            if let Some(context_buffer) = context_buffer_opt.as_mut() {
                context_buffer.try_process_bytes(is_match, record, |r| -> CliResult<()> {
                    wtr.write_splitted_record(r)?;
                    Ok(())
                })?;
            } else if is_match {
                wtr.write_splitted_record(record)?;
            }

            if !check_limit(args.flag_limit, is_match, &mut matches_count) {
                break;
            }
        }

        return Ok(wtr.flush()?);
    }

    // Fast parser path, with selection
    if args.flag_fast_parser && args.flag_select.is_some() {
        if tracking_context {
            Err(
                "-B/--before-context & -A/--after-context does not work with -Z/--fast-parser & -s/--select as it negates its performance benefit.",
            )?;
        }

        let mut rdr = rconfig.simd_zero_copy_reader()?;
        let headers = rdr.byte_headers()?.clone();

        let sel = rconfig.selection(&headers)?;

        if !rconfig.no_headers {
            wtr.write_byte_record(&headers)?;
        }

        while let Some(record) = rdr.read_byte_record()? {
            let mut is_match = if args.flag_every_column {
                try_all(sel.select(&record), |cell| matcher.is_match(cell))?
            } else {
                try_any(sel.select(&record), |cell| matcher.is_match(cell))?
            };

            if args.flag_invert_match {
                is_match = !is_match;
            }

            if is_match {
                wtr.write_zero_copy_byte_record(rconfig.delimiter, &record)?;
            }

            if !check_limit(args.flag_limit, is_match, &mut matches_count) {
                break;
            }
        }

        return Ok(wtr.flush()?);
    }

    let mut rdr = rconfig.simd_reader()?;

    let mut headers = rdr.byte_headers()?.clone();
    let sel = rconfig.selection(&headers)?;
    let sel_mask = sel.mask(headers.len());

    if let Some(column_name) = args.flag_count.as_ref().or(args.flag_flag.as_ref()) {
        headers.push_field(column_name.as_bytes());
    } else if args.flag_breakdown {
        if let Some(consolidated) = consolidated_patterns.as_ref() {
            for group in consolidated.iter() {
                let i = *group.first().unwrap();
                headers.push_field(&associated.as_ref().unwrap()[i]);
            }
        } else if let Some(column_names) = &associated {
            for column_name in column_names {
                headers.push_field(column_name);
            }
        } else {
            for pattern in patterns.as_ref().unwrap().iter() {
                headers.push_field(pattern.as_bytes());
            }
        }
    } else if let Some(column_name) = &args.flag_unique_matches {
        headers.push_field(column_name.as_bytes());
    }

    if !rconfig.no_headers {
        wtr.write_byte_record(&headers)?;
    }

    // Parallel path
    if let Some(t) = threads {
        let matcher = Arc::new(matcher);

        for result in rdr.into_byte_records().parallel_map_custom(
            |o| o.threads(t),
            move |result| -> CliResult<(bool, Option<ByteRecord>)> {
                let mut record = result?;

                let mut record_to_write_opt = None;
                let mut is_match = false;

                // Breakdown
                if args.flag_breakdown {
                    let mut counts = vec![0; patterns_len];

                    for cell in sel.select(&record) {
                        is_match |= matcher.breakdown(cell, args.flag_overlapping, &mut counts)?;
                    }

                    if is_match || args.flag_left {
                        if let Some(consolidated) = consolidated_patterns.as_ref() {
                            for group in consolidated.iter() {
                                let consolidated_count: usize =
                                    group.iter().copied().map(|i| counts[i]).sum();
                                record.push_field(consolidated_count.to_string().as_bytes());
                            }
                        } else {
                            for count in counts.iter_mut() {
                                record.push_field(count.to_string().as_bytes());
                            }
                        }

                        record_to_write_opt.replace(record);
                    }
                }
                // Unique matches
                else if args.flag_unique_matches.is_some() {
                    let mut matches = BTreeSet::<usize>::new();

                    for cell in sel.select(&record) {
                        matcher.unique_matches(cell, args.flag_overlapping, &mut matches)?;
                    }

                    is_match = !matches.is_empty();

                    if !is_match {
                        if args.flag_left {
                            record.push_field(b"");
                            record_to_write_opt.replace(record);
                        }
                    } else {
                        let mut matches_field: Vec<u8> = vec![];

                        for (i, m) in matches.iter().copied().enumerate() {
                            if let Some(names) = &associated {
                                matches_field.push_str(&names[m]);
                            } else if let Some(p) = &patterns {
                                matches_field.push_str(&p[m]);
                            } else {
                                matches_field.push_str(args.arg_pattern.as_ref().unwrap());
                            }

                            if i < matches.len() - 1 {
                                matches_field.push_str(&args.flag_sep);
                            }
                        }

                        record.push_field(&matches_field);
                        record_to_write_opt.replace(record);
                    }
                }
                // Replace
                else if let Some(replacements) = &associated {
                    let mut replaced_record =
                        csv::ByteRecord::with_capacity(record.as_slice().len(), record.len());

                    for (cell, should_replace) in record.iter().zip(sel_mask.iter().copied()) {
                        if should_replace {
                            let replaced_cell = matcher.replace(cell, replacements)?;
                            replaced_record.push_field(&replaced_cell);

                            if args.flag_limit.is_some() && cell != replaced_cell.as_ref() {
                                is_match = true;
                            }
                        } else {
                            replaced_record.push_field(cell);
                        }
                    }

                    record_to_write_opt.replace(record);
                }
                // Count
                else if args.flag_count.is_some() {
                    let count = try_sum(sel.select(&record), |cell| {
                        matcher.count(cell, args.flag_overlapping)
                    })?;

                    if count > 0 {
                        is_match = true;
                    } else if !args.flag_left {
                        return Ok((false, record_to_write_opt));
                    }

                    record.push_field(count.to_string().as_bytes());

                    record_to_write_opt.replace(record);
                }
                // Filter
                else {
                    is_match = if args.flag_every_column {
                        try_all(sel.select(&record), |cell| matcher.is_match(cell))?
                    } else {
                        try_any(sel.select(&record), |cell| matcher.is_match(cell))?
                    };

                    if args.flag_invert_match {
                        is_match = !is_match;
                    }

                    if args.flag_flag.is_some() {
                        record.push_field(if is_match { b"true" } else { b"false" });
                        record_to_write_opt.replace(record);
                    } else if is_match {
                        record_to_write_opt.replace(record);
                    }
                }

                Ok((is_match, record_to_write_opt))
            },
        ) {
            let (is_match, record_to_write_opt) = result?;

            if let Some(record) = record_to_write_opt {
                wtr.write_byte_record(&record)?;
            }

            if !check_limit(args.flag_limit, is_match, &mut matches_count) {
                break;
            }
        }

        return Ok(());
    }

    // Single-threaded path
    let mut record = ByteRecord::new();
    let mut replaced_record = ByteRecord::new();
    let mut matches = BTreeSet::<usize>::new();
    let mut counts = vec![0; patterns_len];

    let mut context_buffer_opt = tracking_context.then(|| {
        ContextBuffer::<ByteRecord>::new(args.flag_before_context, args.flag_after_context)
    });

    while rdr.read_byte_record(&mut record)? {
        let mut is_match: bool = false;

        // Breakdown
        if args.flag_breakdown {
            for cell in sel.select(&record) {
                is_match |= matcher.breakdown(cell, args.flag_overlapping, &mut counts)?;
            }

            if is_match || args.flag_left {
                if let Some(consolidated) = consolidated_patterns.as_ref() {
                    for group in consolidated.iter() {
                        let consolidated_count: usize = group
                            .iter()
                            .copied()
                            .map(|i| {
                                let count = counts[i];
                                counts[i] = 0;
                                count
                            })
                            .sum();
                        record.push_field(consolidated_count.to_string().as_bytes());
                    }
                } else {
                    for count in counts.iter_mut() {
                        record.push_field(count.to_string().as_bytes());
                        *count = 0;
                    }
                }

                wtr.write_byte_record(&record)?;
            }
        }
        // Unique matches
        else if args.flag_unique_matches.is_some() {
            matches.clear();

            for cell in sel.select(&record) {
                matcher.unique_matches(cell, args.flag_overlapping, &mut matches)?;
            }

            is_match = !matches.is_empty();

            if !is_match {
                if args.flag_left {
                    record.push_field(b"");
                    wtr.write_byte_record(&record)?;
                }
            } else {
                let mut matches_field: Vec<u8> = vec![];

                for (i, m) in matches.iter().copied().enumerate() {
                    if let Some(names) = &associated {
                        matches_field.push_str(&names[m]);
                    } else if let Some(p) = &patterns {
                        matches_field.push_str(&p[m]);
                    } else {
                        matches_field.push_str(args.arg_pattern.as_ref().unwrap());
                    }

                    if i < matches.len() - 1 {
                        matches_field.push_str(&args.flag_sep);
                    }
                }

                record.push_field(&matches_field);
                wtr.write_byte_record(&record)?;
            }
        }
        // Replace
        else if let Some(replacements) = &associated {
            replaced_record.clear();

            for (cell, should_replace) in record.iter().zip(sel_mask.iter().copied()) {
                if should_replace {
                    let replaced_cell = matcher.replace(cell, replacements)?;
                    replaced_record.push_field(&replaced_cell);

                    if args.flag_limit.is_some() && cell != replaced_cell.as_ref() {
                        is_match = true;
                    }
                } else {
                    replaced_record.push_field(cell);
                }
            }

            wtr.write_byte_record(&replaced_record)?;
        }
        // Count
        else if args.flag_count.is_some() {
            let count = try_sum(sel.select(&record), |cell| {
                matcher.count(cell, args.flag_overlapping)
            })?;

            if count > 0 {
                is_match = true;
            } else if !args.flag_left {
                continue;
            }

            record.push_field(count.to_string().as_bytes());
            wtr.write_byte_record(&record)?;
        }
        // Filter
        else {
            is_match = if args.flag_every_column {
                try_all(sel.select(&record), |cell| matcher.is_match(cell))?
            } else {
                try_any(sel.select(&record), |cell| matcher.is_match(cell))?
            };

            if args.flag_invert_match {
                is_match = !is_match;
            }

            if let Some(context_buffer) = context_buffer_opt.as_mut() {
                context_buffer.try_process(is_match, &record, |r| -> CliResult<()> {
                    wtr.write_byte_record(r)?;

                    Ok(())
                })?;
            } else if args.flag_flag.is_some() {
                record.push_field(if is_match { b"true" } else { b"false" });
                wtr.write_byte_record(&record)?;
            } else if is_match {
                wtr.write_byte_record(&record)?;
            }
        }

        if !check_limit(args.flag_limit, is_match, &mut matches_count) {
            break;
        }
    }

    Ok(wtr.flush()?)
}
