// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html

use std::borrow::Cow;
use std::sync::LazyLock;

use anki_proto::search::search_node::FieldSearchMode as FieldSearchModeProto;
use nom::branch::alt;
use nom::bytes::complete::escaped;
use nom::bytes::complete::is_not;
use nom::bytes::complete::tag;
use nom::character::complete::alphanumeric1;
use nom::character::complete::anychar;
use nom::character::complete::char;
use nom::character::complete::none_of;
use nom::character::complete::one_of;
use nom::combinator::map;
use nom::combinator::recognize;
use nom::combinator::verify;
use nom::error::ErrorKind as NomErrorKind;
use nom::multi::many0;
use nom::sequence::preceded;
use nom::sequence::separated_pair;
use nom::Parser;
use regex::Captures;
use regex::Regex;

use crate::error::ParseError;
use crate::error::Result;
use crate::error::SearchErrorKind as FailKind;
use crate::prelude::*;
type IResult<'a, O> = std::result::Result<(&'a str, O), nom::Err<ParseError<'a>>>;
type ParseResult<'a, O> = std::result::Result<O, nom::Err<ParseError<'a>>>;

fn parse_failure(input: &str, kind: FailKind) -> nom::Err<ParseError<'_>> {
    nom::Err::Failure(ParseError::Anki(input, kind))
}

fn parse_error(input: &str) -> nom::Err<ParseError<'_>> {
    nom::Err::Error(ParseError::Anki(input, FailKind::Other { info: None }))
}

#[derive(Debug, PartialEq, Clone)]
pub enum Node {
    And,
    Or,
    Not(Box<Node>),
    Group(Vec<Node>),
    Search(SearchNode),
}

#[derive(Copy, Debug, PartialEq, Eq, Clone)]
pub enum FieldSearchMode {
    Normal,
    Regex,
    NoCombining,
}

impl From<FieldSearchModeProto> for FieldSearchMode {
    fn from(mode: FieldSearchModeProto) -> Self {
        match mode {
            FieldSearchModeProto::Normal => Self::Normal,
            FieldSearchModeProto::Regex => Self::Regex,
            FieldSearchModeProto::Nocombining => Self::NoCombining,
        }
    }
}

#[derive(Debug, PartialEq, Clone)]
pub enum SearchNode {
    // text without a colon
    UnqualifiedText(String),
    // foo:bar, where foo doesn't match a term below
    SingleField {
        field: String,
        text: String,
        mode: FieldSearchMode,
    },
    AddedInDays(u32),
    EditedInDays(u32),
    CardTemplate(TemplateKind),
    Deck(String),
    /// Matches cards in a list of deck ids. Cards are matched even if they are
    /// in a filtered deck.
    DeckIdsWithoutChildren(String),
    /// Matches cards in a deck or its children (original_deck_id is not
    /// checked, so filtered cards are not matched).
    DeckIdWithChildren(DeckId),
    IntroducedInDays(u32),
    NotetypeId(NotetypeId),
    Notetype(String),
    Rated {
        days: u32,
        ease: RatingKind,
    },
    Tag {
        tag: String,
        mode: FieldSearchMode,
    },
    Duplicates {
        notetype_id: NotetypeId,
        text: String,
    },
    State(StateKind),
    Flag(u8),
    NoteIds(String),
    CardIds(String),
    Property {
        operator: String,
        kind: PropertyKind,
    },
    WholeCollection,
    Regex(String),
    NoCombining(String),
    StripClozes(String),
    WordBoundary(String),
    CustomData(String),
    Preset(String),
}

#[derive(Debug, PartialEq, Clone)]
pub enum PropertyKind {
    Due(i32),
    Interval(u32),
    Reps(u32),
    Lapses(u32),
    Ease(f32),
    Position(u32),
    Rated(i32, RatingKind),
    Stability(f32),
    Difficulty(f32),
    Retrievability(f32),
    CustomDataNumber { key: String, value: f32 },
    CustomDataString { key: String, value: String },
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub enum StateKind {
    New,
    Review,
    Learning,
    Due,
    Buried,
    UserBuried,
    SchedBuried,
    Suspended,
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub enum TemplateKind {
    Ordinal(u16),
    Name(String),
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub enum RatingKind {
    AnswerButton(u8),
    AnyAnswerButton,
    ManualReschedule,
}

/// Parse the input string into a list of nodes.
pub fn parse(input: &str) -> Result<Vec<Node>> {
    let normalized = normalize_whitespace(input);
    let input = normalized.trim();
    if input.is_empty() {
        return Ok(vec![Node::Search(SearchNode::WholeCollection)]);
    }

    match group_inner(input) {
        Ok(("", nodes)) => Ok(nodes),
        // unmatched ) is only char not consumed by any node parser
        Ok((remaining, _)) => Err(parse_failure(remaining, FailKind::UnopenedGroup).into()),
        Err(err) => Err(err.into()),
    }
}

fn normalize_whitespace(input: &str) -> Cow<'_, str> {
    static WHITESPACE_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\s").unwrap());
    WHITESPACE_RE.replace_all(input, " ")
}

/// Zero or more nodes inside brackets, eg 'one OR two -three'.
/// Empty vec must be handled by caller.
fn group_inner(input: &str) -> IResult<'_, Vec<Node>> {
    let mut remaining = input;
    let mut nodes = vec![];

    loop {
        match node(remaining) {
            Ok((rem, node)) => {
                remaining = rem;

                if nodes.len() % 2 == 0 {
                    // before adding the node, if the length is even then the node
                    // must not be a boolean
                    if node == Node::And {
                        return Err(parse_failure(input, FailKind::MisplacedAnd));
                    } else if node == Node::Or {
                        return Err(parse_failure(input, FailKind::MisplacedOr));
                    }
                } else {
                    // if the length is odd, the next item must be a boolean. if it's
                    // not, add an implicit and
                    if !matches!(node, Node::And | Node::Or) {
                        nodes.push(Node::And);
                    }
                }
                nodes.push(node);
            }
            Err(e) => match e {
                nom::Err::Error(_) => break,
                _ => return Err(e),
            },
        };
    }

    if let Some(last) = nodes.last() {
        match last {
            Node::And => return Err(parse_failure(input, FailKind::MisplacedAnd)),
            Node::Or => return Err(parse_failure(input, FailKind::MisplacedOr)),
            _ => (),
        }
    }
    let (remaining, _) = whitespace0(remaining)?;

    Ok((remaining, nodes))
}

fn whitespace0(s: &str) -> IResult<'_, Vec<char>> {
    many0(one_of(" \u{3000}")).parse(s)
}

/// Optional leading space, then a (negated) group or text
fn node(s: &str) -> IResult<'_, Node> {
    preceded(whitespace0, alt((negated_node, group, text))).parse(s)
}

fn negated_node(s: &str) -> IResult<'_, Node> {
    map(preceded(char('-'), alt((group, text))), |node| {
        Node::Not(Box::new(node))
    })
    .parse(s)
}

/// One or more nodes surrounded by brackets, eg (one OR two)
fn group(s: &str) -> IResult<'_, Node> {
    let (opened, _) = char('(')(s)?;
    let (tail, inner) = group_inner(opened)?;
    if let Some(remaining) = tail.strip_prefix(')') {
        if inner.is_empty() {
            Err(parse_failure(s, FailKind::EmptyGroup))
        } else {
            Ok((remaining, Node::Group(inner)))
        }
    } else {
        Err(parse_failure(s, FailKind::UnclosedGroup))
    }
}

/// Either quoted or unquoted text
fn text(s: &str) -> IResult<'_, Node> {
    alt((quoted_term, partially_quoted_term, unquoted_term)).parse(s)
}

/// Quoted text, including the outer double quotes.
fn quoted_term(s: &str) -> IResult<'_, Node> {
    let (remaining, term) = quoted_term_str(s)?;
    Ok((remaining, Node::Search(search_node_for_text(term)?)))
}

/// eg deck:"foo bar" - quotes must come after the :
fn partially_quoted_term(s: &str) -> IResult<'_, Node> {
    let (remaining, (key, val)) = separated_pair(
        escaped(is_not("\"(): \u{3000}\\"), '\\', none_of(" \u{3000}")),
        char(':'),
        quoted_term_str,
    )
    .parse(s)?;
    Ok((
        remaining,
        Node::Search(search_node_for_text_with_argument(key, val)?),
    ))
}

/// Unquoted text, terminated by whitespace or unescaped ", ( or )
fn unquoted_term(s: &str) -> IResult<'_, Node> {
    match escaped(is_not("\"() \u{3000}\\"), '\\', none_of(" \u{3000}"))(s) {
        Ok((tail, term)) => {
            if term.is_empty() {
                Err(parse_error(s))
            } else if term.eq_ignore_ascii_case("and") {
                Ok((tail, Node::And))
            } else if term.eq_ignore_ascii_case("or") {
                Ok((tail, Node::Or))
            } else {
                Ok((tail, Node::Search(search_node_for_text(term)?)))
            }
        }
        Err(err) => {
            if let nom::Err::Error((c, NomErrorKind::NoneOf)) = err {
                Err(parse_failure(
                    s,
                    FailKind::UnknownEscape {
                        provided: format!("\\{c}"),
                    },
                ))
            } else if "\"() \u{3000}".contains(s.chars().next().unwrap()) {
                Err(parse_error(s))
            } else {
                // input ends in an odd number of backslashes
                Err(parse_failure(
                    s,
                    FailKind::UnknownEscape {
                        provided: '\\'.to_string(),
                    },
                ))
            }
        }
    }
}

/// Non-empty string delimited by unescaped double quotes.
fn quoted_term_str(s: &str) -> IResult<'_, &str> {
    let (opened, _) = char('"')(s)?;
    if let Ok((tail, inner)) =
        escaped::<_, ParseError, _, _>(is_not(r#""\"#), '\\', anychar).parse(opened)
    {
        if let Ok((remaining, _)) = char::<_, ParseError>('"')(tail) {
            Ok((remaining, inner))
        } else {
            Err(parse_failure(s, FailKind::UnclosedQuote))
        }
    } else {
        Err(parse_failure(
            s,
            match opened.chars().next().unwrap() {
                '"' => FailKind::EmptyQuote,
                // no unescaped " and a trailing \
                _ => FailKind::UnclosedQuote,
            },
        ))
    }
}

/// Determine if text is a qualified search, and handle escaped chars.
/// Expect well-formed input: unempty and no trailing \.
fn search_node_for_text(s: &str) -> ParseResult<'_, SearchNode> {
    // leading : is only possible error for well-formed input
    let (tail, head) = verify(escaped(is_not(r":\"), '\\', anychar), |t: &str| {
        !t.is_empty()
    })
    .parse(s)
    .map_err(|_: nom::Err<ParseError>| parse_failure(s, FailKind::MissingKey))?;
    if tail.is_empty() {
        Ok(SearchNode::UnqualifiedText(unescape(head)?))
    } else {
        search_node_for_text_with_argument(head, &tail[1..])
    }
}

/// Convert a colon-separated key/val pair into the relevant search type.
fn search_node_for_text_with_argument<'a>(
    key: &'a str,
    val: &'a str,
) -> ParseResult<'a, SearchNode> {
    Ok(match key.to_ascii_lowercase().as_str() {
        "deck" => SearchNode::Deck(unescape(val)?),
        "note" => SearchNode::Notetype(unescape(val)?),
        "tag" => parse_tag(val)?,
        "card" => parse_template(val)?,
        "flag" => parse_flag(val)?,
        "resched" => parse_resched(val)?,
        "prop" => parse_prop(val)?,
        "added" => parse_added(val)?,
        "edited" => parse_edited(val)?,
        "introduced" => parse_introduced(val)?,
        "rated" => parse_rated(val)?,
        "is" => parse_state(val)?,
        "did" => SearchNode::DeckIdsWithoutChildren(check_id_list(val, key)?.into()),
        "mid" => parse_mid(val)?,
        "nid" => SearchNode::NoteIds(check_id_list(val, key)?.into()),
        "cid" => SearchNode::CardIds(check_id_list(val, key)?.into()),
        "re" => SearchNode::Regex(unescape_quotes(val)),
        "nc" => SearchNode::NoCombining(unescape(val)?),
        "sc" => SearchNode::StripClozes(unescape(val)?),
        "w" => SearchNode::WordBoundary(unescape(val)?),
        "dupe" => parse_dupe(val)?,
        "has-cd" => SearchNode::CustomData(unescape(val)?),
        "preset" => SearchNode::Preset(val.into()),
        // anything else is a field search
        _ => parse_single_field(key, val)?,
    })
}

fn parse_tag(s: &str) -> ParseResult<'_, SearchNode> {
    Ok(if let Some(re) = s.strip_prefix("re:") {
        SearchNode::Tag {
            tag: unescape_quotes(re),
            mode: FieldSearchMode::Regex,
        }
    } else if let Some(nc) = s.strip_prefix("nc:") {
        SearchNode::Tag {
            tag: unescape(nc)?,
            mode: FieldSearchMode::NoCombining,
        }
    } else {
        SearchNode::Tag {
            tag: unescape(s)?,
            mode: FieldSearchMode::Normal,
        }
    })
}

fn parse_template(s: &str) -> ParseResult<'_, SearchNode> {
    Ok(SearchNode::CardTemplate(match s.parse::<u16>() {
        Ok(n) => TemplateKind::Ordinal(n.max(1) - 1),
        Err(_) => TemplateKind::Name(unescape(s)?),
    }))
}

/// flag:0-7
fn parse_flag(s: &str) -> ParseResult<'_, SearchNode> {
    if let Ok(flag) = s.parse::<u8>() {
        if flag > 7 {
            Err(parse_failure(s, FailKind::InvalidFlag))
        } else {
            Ok(SearchNode::Flag(flag))
        }
    } else {
        Err(parse_failure(s, FailKind::InvalidFlag))
    }
}

/// eg resched:3
fn parse_resched(s: &str) -> ParseResult<'_, SearchNode> {
    parse_u32(s, "resched:").map(|days| SearchNode::Rated {
        days,
        ease: RatingKind::ManualReschedule,
    })
}

/// eg prop:ivl>3, prop:ease!=2.5
fn parse_prop(prop_clause: &str) -> ParseResult<'_, SearchNode> {
    let (tail, prop) = alt((
        tag("ivl"),
        tag("due"),
        tag("reps"),
        tag("lapses"),
        tag("ease"),
        tag("pos"),
        tag("rated"),
        tag("resched"),
        tag("s"),
        tag("d"),
        tag("r"),
        recognize(preceded(tag("cdn:"), alphanumeric1)),
        recognize(preceded(tag("cds:"), alphanumeric1)),
    ))
    .parse(prop_clause)
    .map_err(|_: nom::Err<ParseError>| {
        parse_failure(
            prop_clause,
            FailKind::InvalidPropProperty {
                provided: prop_clause.into(),
            },
        )
    })?;

    let (num, operator) = alt((
        tag("<="),
        tag(">="),
        tag("!="),
        tag("="),
        tag("<"),
        tag(">"),
    ))
    .parse(tail)
    .map_err(|_: nom::Err<ParseError>| {
        parse_failure(
            prop_clause,
            FailKind::InvalidPropOperator {
                provided: prop.to_string(),
            },
        )
    })?;

    let kind = match prop {
        "ease" => PropertyKind::Ease(parse_f32(num, prop_clause)?),
        "due" => PropertyKind::Due(parse_i32(num, prop_clause)?),
        "rated" => parse_prop_rated(num, prop_clause)?,
        "resched" => PropertyKind::Rated(
            parse_negative_i32(num, prop_clause)?,
            RatingKind::ManualReschedule,
        ),
        "ivl" => PropertyKind::Interval(parse_u32(num, prop_clause)?),
        "reps" => PropertyKind::Reps(parse_u32(num, prop_clause)?),
        "lapses" => PropertyKind::Lapses(parse_u32(num, prop_clause)?),
        "pos" => PropertyKind::Position(parse_u32(num, prop_clause)?),
        "s" => PropertyKind::Stability(parse_f32(num, prop_clause)?),
        "d" => PropertyKind::Difficulty(parse_f32(num, prop_clause)?),
        "r" => PropertyKind::Retrievability(parse_f32(num, prop_clause)?),
        prop if prop.starts_with("cdn:") => PropertyKind::CustomDataNumber {
            key: prop.strip_prefix("cdn:").unwrap().into(),
            value: parse_f32(num, prop_clause)?,
        },
        prop if prop.starts_with("cds:") => PropertyKind::CustomDataString {
            key: prop.strip_prefix("cds:").unwrap().into(),
            value: num.into(),
        },
        _ => unreachable!(),
    };

    Ok(SearchNode::Property {
        operator: operator.to_string(),
        kind,
    })
}

fn parse_u32<'a>(num: &str, context: &'a str) -> ParseResult<'a, u32> {
    num.parse().map_err(|_e| {
        parse_failure(
            context,
            FailKind::InvalidPositiveWholeNumber {
                context: context.into(),
                provided: num.into(),
            },
        )
    })
}

fn parse_i32<'a>(num: &str, context: &'a str) -> ParseResult<'a, i32> {
    num.parse().map_err(|_e| {
        parse_failure(
            context,
            FailKind::InvalidWholeNumber {
                context: context.into(),
                provided: num.into(),
            },
        )
    })
}

fn parse_negative_i32<'a>(num: &str, context: &'a str) -> ParseResult<'a, i32> {
    num.parse()
        .map_err(|_| ())
        .and_then(|n| if n > 0 { Err(()) } else { Ok(n) })
        .map_err(|_| {
            parse_failure(
                context,
                FailKind::InvalidNegativeWholeNumber {
                    context: context.into(),
                    provided: num.into(),
                },
            )
        })
}

fn parse_f32<'a>(num: &str, context: &'a str) -> ParseResult<'a, f32> {
    num.parse().map_err(|_e| {
        parse_failure(
            context,
            FailKind::InvalidNumber {
                context: context.into(),
                provided: num.into(),
            },
        )
    })
}

fn parse_i64<'a>(num: &str, context: &'a str) -> ParseResult<'a, i64> {
    num.parse().map_err(|_e| {
        parse_failure(
            context,
            FailKind::InvalidWholeNumber {
                context: context.into(),
                provided: num.into(),
            },
        )
    })
}

fn parse_answer_button<'a>(num: Option<&str>, context: &'a str) -> ParseResult<'a, RatingKind> {
    Ok(if let Some(num) = num {
        RatingKind::AnswerButton(
            num.parse()
                .map_err(|_| ())
                .and_then(|n| if matches!(n, 1..=4) { Ok(n) } else { Err(()) })
                .map_err(|_| {
                    parse_failure(
                        context,
                        FailKind::InvalidAnswerButton {
                            context: context.into(),
                            provided: num.into(),
                        },
                    )
                })?,
        )
    } else {
        RatingKind::AnyAnswerButton
    })
}

fn parse_prop_rated<'a>(num: &str, context: &'a str) -> ParseResult<'a, PropertyKind> {
    let mut it = num.splitn(2, ':');
    let days = parse_negative_i32(it.next().unwrap(), context)?;
    let button = parse_answer_button(it.next(), context)?;
    Ok(PropertyKind::Rated(days, button))
}

/// eg added:1
fn parse_added(s: &str) -> ParseResult<'_, SearchNode> {
    parse_u32(s, "added:").map(|n| SearchNode::AddedInDays(n.max(1)))
}

/// eg edited:1
fn parse_edited(s: &str) -> ParseResult<'_, SearchNode> {
    parse_u32(s, "edited:").map(|n| SearchNode::EditedInDays(n.max(1)))
}

/// eg introduced:1
fn parse_introduced(s: &str) -> ParseResult<'_, SearchNode> {
    parse_u32(s, "introduced:").map(|n| SearchNode::IntroducedInDays(n.max(1)))
}

/// eg rated:3 or rated:10:2
/// second arg must be between 1-4
fn parse_rated(s: &str) -> ParseResult<'_, SearchNode> {
    let mut it = s.splitn(2, ':');
    let days = parse_u32(it.next().unwrap(), "rated:")?.max(1);
    let button = parse_answer_button(it.next(), s)?;
    Ok(SearchNode::Rated { days, ease: button })
}

/// eg is:due
fn parse_state(s: &str) -> ParseResult<'_, SearchNode> {
    use StateKind::*;
    Ok(SearchNode::State(match s {
        "new" => New,
        "review" => Review,
        "learn" => Learning,
        "due" => Due,
        "buried" => Buried,
        "buried-manually" => UserBuried,
        "buried-sibling" => SchedBuried,
        "suspended" => Suspended,
        _ => {
            return Err(parse_failure(
                s,
                FailKind::InvalidState { provided: s.into() },
            ))
        }
    }))
}

fn parse_mid(s: &str) -> ParseResult<'_, SearchNode> {
    parse_i64(s, "mid:").map(|n| SearchNode::NotetypeId(n.into()))
}

/// ensure a list of ids contains only numbers and commas, returning unchanged
/// if true used by nid: and cid:
fn check_id_list<'a>(s: &'a str, context: &str) -> ParseResult<'a, &'a str> {
    static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(\d+,)*\d+$").unwrap());
    if RE.is_match(s) {
        Ok(s)
    } else {
        Err(parse_failure(
            s,
            // id lists are undocumented, so no translation
            FailKind::Other {
                info: Some(format!("expected only digits and commas in {context}:")),
            },
        ))
    }
}

/// eg dupe:1231,hello
fn parse_dupe(s: &str) -> ParseResult<'_, SearchNode> {
    let mut it = s.splitn(2, ',');
    let ntid = parse_i64(it.next().unwrap(), s)?;
    if let Some(text) = it.next() {
        Ok(SearchNode::Duplicates {
            notetype_id: ntid.into(),
            text: unescape_quotes_and_backslashes(text),
        })
    } else {
        // this is an undocumented keyword, so no translation/help
        Err(parse_failure(
            s,
            FailKind::Other {
                info: Some("invalid 'dupe:' search".into()),
            },
        ))
    }
}

fn parse_single_field<'a>(key: &'a str, val: &'a str) -> ParseResult<'a, SearchNode> {
    Ok(if let Some(stripped) = val.strip_prefix("re:") {
        SearchNode::SingleField {
            field: unescape(key)?,
            text: unescape_quotes(stripped),
            mode: FieldSearchMode::Regex,
        }
    } else if let Some(stripped) = val.strip_prefix("nc:") {
        SearchNode::SingleField {
            field: unescape(key)?,
            text: unescape_quotes(stripped),
            mode: FieldSearchMode::NoCombining,
        }
    } else {
        SearchNode::SingleField {
            field: unescape(key)?,
            text: unescape(val)?,
            mode: FieldSearchMode::Normal,
        }
    })
}

/// For strings without unescaped ", convert \" to "
fn unescape_quotes(s: &str) -> String {
    if s.contains('"') {
        s.replace(r#"\""#, "\"")
    } else {
        s.into()
    }
}

/// For non-globs like dupe text without any assumption about the content
fn unescape_quotes_and_backslashes(s: &str) -> String {
    if s.contains('"') || s.contains('\\') {
        s.replace(r#"\""#, "\"").replace(r"\\", r"\")
    } else {
        s.into()
    }
}

/// Unescape chars with special meaning to the parser.
fn unescape(txt: &str) -> ParseResult<'_, String> {
    if let Some(seq) = invalid_escape_sequence(txt) {
        Err(parse_failure(
            txt,
            FailKind::UnknownEscape { provided: seq },
        ))
    } else {
        Ok(if is_parser_escape(txt) {
            static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"\\[\\":()-]"#).unwrap());
            RE.replace_all(txt, |caps: &Captures| match &caps[0] {
                r"\\" => r"\\",
                "\\\"" => "\"",
                r"\:" => ":",
                r"\(" => "(",
                r"\)" => ")",
                r"\-" => "-",
                _ => unreachable!(),
            })
            .into()
        } else {
            txt.into()
        })
    }
}

/// Return invalid escape sequence if any.
fn invalid_escape_sequence(txt: &str) -> Option<String> {
    // odd number of \s not followed by an escapable character
    static RE: LazyLock<Regex> = LazyLock::new(|| {
        Regex::new(
            r#"(?x)
            (?:^|[^\\])         # not a backslash
            (?:\\\\)*           # even number of backslashes
            (\\                 # single backslash
            (?:[^\\":*_()-]|$)) # anything but an escapable char
            "#,
        )
        .unwrap()
    });
    let caps = RE.captures(txt)?;

    Some(caps[1].to_string())
}

/// Check string for escape sequences handled by the parser: ":()-
fn is_parser_escape(txt: &str) -> bool {
    // odd number of \s followed by a char with special meaning to the parser
    static RE: LazyLock<Regex> = LazyLock::new(|| {
        Regex::new(
            r#"(?x)
            (?:^|[^\\])     # not a backslash
            (?:\\\\)*       # even number of backslashes
            \\              # single backslash
            [":()-]         # parser escape
            "#,
        )
        .unwrap()
    });

    RE.is_match(txt)
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::error::SearchErrorKind;

    #[test]
    fn parsing() -> Result<()> {
        use Node::*;
        use SearchNode::*;

        assert_eq!(parse("")?, vec![Search(WholeCollection)]);
        assert_eq!(parse("  ")?, vec![Search(WholeCollection)]);

        // // all whitespace should be treated as regular spaces
        assert_eq!(
            parse("foo\u{a0}bar")?,
            vec![
                Search(UnqualifiedText("foo".into())),
                And,
                Search(UnqualifiedText("bar".into()))
            ]
        );
        assert_eq!(
            parse("foo\tbar")?,
            vec![
                Search(UnqualifiedText("foo".into())),
                And,
                Search(UnqualifiedText("bar".into()))
            ]
        );
        assert_eq!(
            parse("foo\nbar")?,
            vec![
                Search(UnqualifiedText("foo".into())),
                And,
                Search(UnqualifiedText("bar".into()))
            ]
        );

        // leading/trailing/interspersed whitespace
        assert_eq!(
            parse("  t   t2  ")?,
            vec![
                Search(UnqualifiedText("t".into())),
                And,
                Search(UnqualifiedText("t2".into()))
            ]
        );

        // including in groups
        assert_eq!(
            parse("(  t   t2  )")?,
            vec![Group(vec![
                Search(UnqualifiedText("t".into())),
                And,
                Search(UnqualifiedText("t2".into()))
            ])]
        );

        assert_eq!(
            parse(r#"hello  -(world and "foo:bar baz") OR test"#)?,
            vec![
                Search(UnqualifiedText("hello".into())),
                And,
                Not(Box::new(Group(vec![
                    Search(UnqualifiedText("world".into())),
                    And,
                    Search(SingleField {
                        field: "foo".into(),
                        text: "bar baz".into(),
                        mode: FieldSearchMode::Normal,
                    })
                ]))),
                Or,
                Search(UnqualifiedText("test".into()))
            ]
        );

        assert_eq!(
            parse("foo:re:bar")?,
            vec![Search(SingleField {
                field: "foo".into(),
                text: "bar".into(),
                mode: FieldSearchMode::Regex,
            })]
        );

        assert_eq!(
            parse("foo:nc:bar")?,
            vec![Search(SingleField {
                field: "foo".into(),
                text: "bar".into(),
                mode: FieldSearchMode::NoCombining,
            })]
        );

        // escaping is independent of quotation
        assert_eq!(
            parse(r#""field:va\"lue""#)?,
            vec![Search(SingleField {
                field: "field".into(),
                text: "va\"lue".into(),
                mode: FieldSearchMode::Normal,
            })]
        );
        assert_eq!(parse(r#""field:va\"lue""#)?, parse(r#"field:"va\"lue""#)?,);
        assert_eq!(parse(r#""field:va\"lue""#)?, parse(r#"field:va\"lue"#)?,);

        // parser unescapes ":()-
        assert_eq!(
            parse(r#"\"\:\(\)\-"#)?,
            vec![Search(UnqualifiedText(r#"":()-"#.into())),]
        );

        // parser doesn't unescape unescape \*_
        assert_eq!(
            parse(r"\\\*\_")?,
            vec![Search(UnqualifiedText(r"\\\*\_".into())),]
        );

        // escaping parentheses is optional (only) inside quotes
        assert_eq!(parse(r#""\)\(""#), parse(r#"")(""#));

        // escaping : is optional if it is preceded by another :
        assert_eq!(parse("field:val:ue"), parse(r"field:val\:ue"));
        assert_eq!(parse(r#""field:val:ue""#), parse(r"field:val\:ue"));
        assert_eq!(parse(r#"field:"val:ue""#), parse(r"field:val\:ue"));

        // escaping - is optional if it cannot be mistaken for a negator
        assert_eq!(parse("-"), parse(r"\-"));
        assert_eq!(parse("A-"), parse(r"A\-"));
        assert_eq!(parse(r#""-A""#), parse(r"\-A"));
        assert_ne!(parse("-A"), parse(r"\-A"));

        // any character should be escapable on the right side of re:
        assert_eq!(
            parse(r#""re:\btest\%""#)?,
            vec![Search(Regex(r"\btest\%".into()))]
        );

        // no exceptions for escaping "
        assert_eq!(
            parse(r#"re:te\"st"#)?,
            vec![Search(Regex(r#"te"st"#.into()))]
        );

        // spaces are optional if node separation is clear
        assert_eq!(parse(r#"a"b"(c)"#)?, parse("a b (c)")?);

        assert_eq!(parse("added:3")?, vec![Search(AddedInDays(3))]);
        assert_eq!(
            parse("card:front")?,
            vec![Search(CardTemplate(TemplateKind::Name("front".into())))]
        );
        assert_eq!(
            parse("card:3")?,
            vec![Search(CardTemplate(TemplateKind::Ordinal(2)))]
        );
        // 0 must not cause a crash due to underflow
        assert_eq!(
            parse("card:0")?,
            vec![Search(CardTemplate(TemplateKind::Ordinal(0)))]
        );
        assert_eq!(parse("deck:default")?, vec![Search(Deck("default".into()))]);
        assert_eq!(
            parse("deck:\"default one\"")?,
            vec![Search(Deck("default one".into()))]
        );

        assert_eq!(
            parse("preset:default")?,
            vec![Search(Preset("default".into()))]
        );

        assert_eq!(parse("note:basic")?, vec![Search(Notetype("basic".into()))]);
        assert_eq!(
            parse("tag:hard")?,
            vec![Search(Tag {
                tag: "hard".into(),
                mode: FieldSearchMode::Normal
            })]
        );
        assert_eq!(
            parse(r"tag:re:\\")?,
            vec![Search(Tag {
                tag: r"\\".into(),
                mode: FieldSearchMode::Regex
            })]
        );
        assert_eq!(
            parse("nid:1237123712,2,3")?,
            vec![Search(NoteIds("1237123712,2,3".into()))]
        );
        assert_eq!(parse("is:due")?, vec![Search(State(StateKind::Due))]);
        assert_eq!(parse("flag:3")?, vec![Search(Flag(3))]);

        assert_eq!(
            parse("prop:ivl>3")?,
            vec![Search(Property {
                operator: ">".into(),
                kind: PropertyKind::Interval(3)
            })]
        );
        assert_eq!(
            parse("prop:ease<=3.3")?,
            vec![Search(Property {
                operator: "<=".into(),
                kind: PropertyKind::Ease(3.3)
            })]
        );
        assert_eq!(
            parse("prop:cdn:abc<=1")?,
            vec![Search(Property {
                operator: "<=".into(),
                kind: PropertyKind::CustomDataNumber {
                    key: "abc".into(),
                    value: 1.0
                }
            })]
        );
        assert_eq!(
            parse("prop:cds:abc=foo")?,
            vec![Search(Property {
                operator: "=".into(),
                kind: PropertyKind::CustomDataString {
                    key: "abc".into(),
                    value: "foo".into()
                }
            })]
        );
        assert_eq!(
            parse("\"prop:cds:abc=foo bar\"")?,
            vec![Search(Property {
                operator: "=".into(),
                kind: PropertyKind::CustomDataString {
                    key: "abc".into(),
                    value: "foo bar".into()
                }
            })]
        );
        assert_eq!(parse("has-cd:r")?, vec![Search(CustomData("r".into()))]);

        Ok(())
    }

    #[test]
    fn errors() {
        use FailKind::*;

        use crate::error::AnkiError;

        fn assert_err_kind(input: &str, kind: FailKind) {
            assert_eq!(parse(input), Err(AnkiError::SearchError { source: kind }));
        }

        fn failkind(input: &str) -> SearchErrorKind {
            if let Err(AnkiError::SearchError { source: err }) = parse(input) {
                err
            } else {
                panic!("expected search error");
            }
        }

        assert_err_kind("foo and", MisplacedAnd);
        assert_err_kind("and foo", MisplacedAnd);
        assert_err_kind("and", MisplacedAnd);

        assert_err_kind("foo or", MisplacedOr);
        assert_err_kind("or foo", MisplacedOr);
        assert_err_kind("or", MisplacedOr);

        assert_err_kind("()", EmptyGroup);
        assert_err_kind("( )", EmptyGroup);
        assert_err_kind("(foo () bar)", EmptyGroup);

        assert_err_kind(")", UnopenedGroup);
        assert_err_kind("foo ) bar", UnopenedGroup);
        assert_err_kind("(foo) bar)", UnopenedGroup);

        assert_err_kind("(", UnclosedGroup);
        assert_err_kind("foo ( bar", UnclosedGroup);
        assert_err_kind("(foo (bar)", UnclosedGroup);

        assert_err_kind(r#""""#, EmptyQuote);
        assert_err_kind(r#"foo:"""#, EmptyQuote);

        assert_err_kind(r#" " "#, UnclosedQuote);
        assert_err_kind(r#"" foo"#, UnclosedQuote);
        assert_err_kind(r#""\"#, UnclosedQuote);
        assert_err_kind(r#"foo:"bar"#, UnclosedQuote);
        assert_err_kind(r#"foo:"bar\"#, UnclosedQuote);

        assert_err_kind(":", MissingKey);
        assert_err_kind(":foo", MissingKey);
        assert_err_kind(r#":"foo""#, MissingKey);

        assert_err_kind(
            r"\",
            UnknownEscape {
                provided: r"\".to_string(),
            },
        );
        assert_err_kind(
            r"\%",
            UnknownEscape {
                provided: r"\%".to_string(),
            },
        );
        assert_err_kind(
            r"foo\",
            UnknownEscape {
                provided: r"\".to_string(),
            },
        );
        assert_err_kind(
            r"\foo",
            UnknownEscape {
                provided: r"\f".to_string(),
            },
        );
        assert_err_kind(
            r"\ ",
            UnknownEscape {
                provided: r"\".to_string(),
            },
        );
        assert_err_kind(
            r#""\ ""#,
            UnknownEscape {
                provided: r"\ ".to_string(),
            },
        );

        for term in &[
            "nid:1_2,3",
            "nid:1,2,x",
            "nid:,2,3",
            "nid:1,2,",
            "cid:1_2,3",
            "cid:1,2,x",
            "cid:,2,3",
            "cid:1,2,",
        ] {
            assert!(matches!(failkind(term), SearchErrorKind::Other { .. }));
        }

        assert_err_kind(
            "is:foo",
            InvalidState {
                provided: "foo".into(),
            },
        );
        assert_err_kind(
            "is:DUE",
            InvalidState {
                provided: "DUE".into(),
            },
        );
        assert_err_kind(
            "is:New",
            InvalidState {
                provided: "New".into(),
            },
        );
        assert_err_kind(
            "is:",
            InvalidState {
                provided: "".into(),
            },
        );
        assert_err_kind(
            r#""is:learn ""#,
            InvalidState {
                provided: "learn ".into(),
            },
        );

        assert_err_kind(r#""flag: ""#, InvalidFlag);
        assert_err_kind("flag:-0", InvalidFlag);
        assert_err_kind("flag:", InvalidFlag);
        assert_err_kind("flag:8", InvalidFlag);
        assert_err_kind("flag:1.1", InvalidFlag);

        for term in &["added", "edited", "rated", "resched"] {
            assert!(matches!(
                failkind(&format!("{term}:1.1")),
                SearchErrorKind::InvalidPositiveWholeNumber { .. }
            ));
            assert!(matches!(
                failkind(&format!("{term}:-1")),
                SearchErrorKind::InvalidPositiveWholeNumber { .. }
            ));
            assert!(matches!(
                failkind(&format!("{term}:")),
                SearchErrorKind::InvalidPositiveWholeNumber { .. }
            ));
            assert!(matches!(
                failkind(&format!("{term}:foo")),
                SearchErrorKind::InvalidPositiveWholeNumber { .. }
            ));
        }

        assert!(matches!(
            failkind("rated:1:"),
            SearchErrorKind::InvalidAnswerButton { .. }
        ));
        assert!(matches!(
            failkind("rated:2:-1"),
            SearchErrorKind::InvalidAnswerButton { .. }
        ));
        assert!(matches!(
            failkind("rated:3:1.1"),
            SearchErrorKind::InvalidAnswerButton { .. }
        ));
        assert!(matches!(
            failkind("rated:0:foo"),
            SearchErrorKind::InvalidAnswerButton { .. }
        ));

        assert!(matches!(
            failkind("dupe:"),
            SearchErrorKind::InvalidWholeNumber { .. }
        ));
        assert!(matches!(
            failkind("dupe:1.1"),
            SearchErrorKind::InvalidWholeNumber { .. }
        ));
        assert!(matches!(
            failkind("dupe:foo"),
            SearchErrorKind::InvalidWholeNumber { .. }
        ));

        assert_err_kind(
            "prop:",
            InvalidPropProperty {
                provided: "".into(),
            },
        );
        assert_err_kind(
            "prop:=1",
            InvalidPropProperty {
                provided: "=1".into(),
            },
        );
        assert_err_kind(
            "prop:DUE<5",
            InvalidPropProperty {
                provided: "DUE<5".into(),
            },
        );
        assert_err_kind(
            "prop:cdn=5",
            InvalidPropProperty {
                provided: "cdn=5".to_string(),
            },
        );
        assert_err_kind(
            "prop:cdn:=5",
            InvalidPropProperty {
                provided: "cdn:=5".to_string(),
            },
        );
        assert_err_kind(
            "prop:cds=s",
            InvalidPropProperty {
                provided: "cds=s".to_string(),
            },
        );
        assert_err_kind(
            "prop:cds:=s",
            InvalidPropProperty {
                provided: "cds:=s".to_string(),
            },
        );

        assert_err_kind(
            "prop:lapses",
            InvalidPropOperator {
                provided: "lapses".to_string(),
            },
        );
        assert_err_kind(
            "prop:pos~1",
            InvalidPropOperator {
                provided: "pos".to_string(),
            },
        );
        assert_err_kind(
            "prop:reps10",
            InvalidPropOperator {
                provided: "reps".to_string(),
            },
        );

        // unsigned

        for term in &["ivl", "reps", "lapses", "pos"] {
            assert!(matches!(
                failkind(&format!("prop:{term}>")),
                SearchErrorKind::InvalidPositiveWholeNumber { .. }
            ));
            assert!(matches!(
                failkind(&format!("prop:{term}=0.5")),
                SearchErrorKind::InvalidPositiveWholeNumber { .. }
            ));
            assert!(matches!(
                failkind(&format!("prop:{term}!=-1")),
                SearchErrorKind::InvalidPositiveWholeNumber { .. }
            ));
            assert!(matches!(
                failkind(&format!("prop:{term}<foo")),
                SearchErrorKind::InvalidPositiveWholeNumber { .. }
            ));
        }

        // signed

        assert!(matches!(
            failkind("prop:due>"),
            SearchErrorKind::InvalidWholeNumber { .. }
        ));
        assert!(matches!(
            failkind("prop:due=0.5"),
            SearchErrorKind::InvalidWholeNumber { .. }
        ));

        // float

        assert!(matches!(
            failkind("prop:ease>"),
            SearchErrorKind::InvalidNumber { .. }
        ));
        assert!(matches!(
            failkind("prop:ease!=one"),
            SearchErrorKind::InvalidNumber { .. }
        ));
        assert!(matches!(
            failkind("prop:ease<1,3"),
            SearchErrorKind::InvalidNumber { .. }
        ));
    }
}
