use crate::{
    CharStringExt, Lint, Token, TokenKind,
    expr::{Expr, FirstMatchOf, SequenceExpr},
    linting::{ExprLinter, LintKind, Suggestion, expr_linter::Chunk},
    spell::Dictionary,
};

static NON_MODAL_AUX: &[&str] = &[
    "do", "don't", "does", "doesn't", "have", "has", "haven't", "hasn't", "dont", "doesnt",
    "havent", "hasnt",
];
static IRREGULAR: &[(&str, &str)] = &[("don't", "doesn't"), ("have", "has"), ("haven't", "hasn't")];
static SUBJUNCTIVE: &[&str] = &[
    // "if" and "that" can take the subjunctive mood: "if he go", "that he go" - as in the US constitution
    // "if" TODO: "if" is more complicated to support than "that"
    "that",
    // Verbs that take the subjunctive mood can omit the "that":
    "demanded",
    "demanding",
    "insisted",
    "insisting",
    "recommended",
    "recommending",
    "requested",
    "requesting",
    "suggested",
    "suggesting",
];
static DITRANSITIVE: &[&str] = &[
    "give", "gave", "given", "gives", "giving", "lose", "lost", "loses", "losing",
];

pub struct PronounVerbAgreement<D> {
    expr: FirstMatchOf,
    dict: D,
}

impl<D: Dictionary> PronounVerbAgreement<D> {
    pub fn new(dict: D) -> Self {
        // TODO: allowing "you" leads to false positives:
        // "8 years to give you rewards", "all I can do is give you examples"
        let non_3p_sing_pres_pron_with_3p_sing_pres_verb = SequenceExpr::default()
            .then_kind_both_but_not(
                (
                    TokenKind::is_personal_pronoun,
                    TokenKind::is_subject_pronoun,
                ),
                TokenKind::is_third_person_singular_pronoun,
            )
            .t_ws()
            // NOTE: allowing verbs that are also nouns leads to false positives:
            // "Are they colors or colours?"
            // "8 years to give you rewards"
            // "all I can do is give you examples"
            .then_verb_third_person_singular_present_form();

        // NOTE: But excluding them causes many more false positives:
        // boxes, does, drops, flies, gets, goes, likes, site, wakes
        // .then_kind_where(|k| k.is_verb_third_person_singular_present_form() && !k.is_plural_noun());

        let third_person_sing_pres_pron = |t: &Token, _: &[char]| {
            t.kind.is_subject_pronoun()
                && !t.kind.is_object_pronoun()
                && t.kind.is_personal_pronoun()
                && t.kind.is_third_person_singular_pronoun()
                && !t.kind.is_plural_pronoun()
        };

        let verb_lemma = |t: &Token, src: &[char]| {
            t.kind.is_verb_lemma()
                && !t.kind.is_verb_third_person_singular_present_form()
                && !t.kind.is_verb_simple_past_form() // eg. not "put"
                && !t.kind.is_adverb() // eg. not "even"
                && !t.kind.is_conjunction() // "and"
                && (!t.kind.is_auxiliary_verb() // "I go"≠"he goes" but "I can"="he can"
                // We don't want modals because they don't inflect, but we want the other auxiliaries.
                || t.get_ch(src).eq_any_ignore_ascii_case_str(NON_MODAL_AUX))
        };

        Self {
            expr: FirstMatchOf::new(vec![
                // One Expr for the "I walks" type:
                Box::new(non_3p_sing_pres_pron_with_3p_sing_pres_verb),
                // Two Expr's for the "he walk" type:
                Box::new(
                    SequenceExpr::with(third_person_sing_pres_pron)
                        .t_ws()
                        .then(verb_lemma),
                ),
                Box::new(SequenceExpr::word_seq(&["it", "don't"])),
            ]),
            dict,
        }
    }

    fn third_person_singular_present_to_lemma(&self, form: &[char]) -> Vec<Vec<char>> {
        let mut words: Vec<Vec<char>> = Vec::new();

        // -s
        if form.ends_with_ignore_ascii_case_chars(&['s']) {
            words.push(form[0..form.len() - 1].to_vec());

            // -es
            if form.ends_with_ignore_ascii_case_chars(&['e', 's']) {
                words.push(form[0..form.len() - 2].to_vec());

                // -ies -> -y
                if form.ends_with_ignore_ascii_case_chars(&['i', 'e', 's']) {
                    words.push(
                        format!("{}y", form[0..form.len() - 3].iter().collect::<String>())
                            .chars()
                            .collect(),
                    );
                }
            }
        }

        if let Some((lemma, _)) = IRREGULAR.iter().find(|(_, f)| form.eq_str(f)) {
            words.push(lemma.chars().collect::<Vec<char>>());
        }

        words
            .iter()
            .filter(|&w| {
                self.dict
                    .get_word_metadata(w)
                    .is_some_and(|md| md.is_verb_lemma())
            })
            .map(|w| w.to_vec())
            .collect()
    }

    fn lemma_to_third_person_singular_present(&self, input: &str) -> Vec<Vec<char>> {
        let mut words: Vec<Vec<char>> = Vec::new();

        words.push(format!("{input}s").chars().collect());
        words.push(format!("{input}es").chars().collect());

        if input.ends_with("y") {
            words.push(
                format!("{}ies", &input[0..input.len() - 1])
                    .chars()
                    .collect(),
            );
        }

        if let Some((_, form)) = IRREGULAR
            .iter()
            .find(|(lemma, _)| input.eq_ignore_ascii_case(lemma))
        {
            words.push(form.chars().collect());
        }

        words
            .iter()
            .filter(|&w| {
                self.dict
                    .get_word_metadata(w)
                    .is_some_and(|md| md.is_verb_third_person_singular_present_form())
            })
            .map(|w| w.to_vec())
            .collect()
    }
}

impl<D: Dictionary> ExprLinter for PronounVerbAgreement<D> {
    type Unit = Chunk;

    fn expr(&self) -> &dyn Expr {
        &self.expr
    }

    fn match_to_lint_with_context(
        &self,
        toks: &[Token],
        src: &[char],
        ctx: Option<(&[Token], &[Token])>,
    ) -> Option<Lint> {
        let pron_tok = &toks[0];
        let is_3psg = pron_tok.kind.is_third_person_singular_pronoun();

        // Skip when the pronoun is part of a hyphenated compound (e.g. "co-founded").
        // The tokenizer splits "co-founded" into separate tokens, but "co" as a prefix
        // is not acting as a pronoun.
        if pron_tok.span.end < src.len() && src[pron_tok.span.end] == '-' {
            return None;
        }

        let verb_tok = toks.last()?;

        if let Some((before, _)) = ctx
            && let [.., prev_word_tok, ws_tok] = before
            && ws_tok.kind.is_whitespace()
        {
            let prev_word = prev_word_tok.get_ch(src);
            let is_exempt = if is_3psg {
                prev_word_tok.kind.is_auxiliary_verb()
                    || prev_word.eq_any_ignore_ascii_case_str(SUBJUNCTIVE)
            } else if pron_tok.kind.is_subject_pronoun() {
                // Clause structure: (... in you) is ... ≠ you is
                // Look for "true" prepositions, not ones that are more like adverbial particles
                prev_word_tok.kind.is_preposition() && !prev_word.eq_str("up")
                    // When the verb is ditransitive, the pronoun is object case, the verb position is actually a noun
                    || (prev_word.eq_any_ignore_ascii_case_str(DITRANSITIVE) && verb_tok.kind.is_noun())
            } else {
                false
            };

            if is_exempt {
                return None;
            }
        }

        let verb_span = verb_tok.span;
        let verb_chars = verb_tok.get_ch(src);
        let verb_str = verb_tok.get_str(src);

        let suggs = if is_3psg {
            self.lemma_to_third_person_singular_present(&verb_str)
        } else {
            self.third_person_singular_present_to_lemma(verb_chars)
        };

        let suggestions = suggs
            .into_iter()
            .map(|s| Suggestion::replace_with_match_case(s, verb_chars))
            .collect();

        Some(Lint {
            span: verb_span,
            lint_kind: LintKind::Agreement,
            suggestions,
            message: "The form of the verb must agree in grammatical number with the pronoun."
                .to_owned(),
            ..Default::default()
        })
    }

    fn description(&self) -> &str {
        "Ensures pronouns agree with their verbs."
    }
}

#[cfg(test)]
mod lints {
    use super::PronounVerbAgreement;
    use crate::linting::create_test_pool;
    use crate::linting::tests::{assert_no_lints, assert_suggestion_result};
    use crate::spell::FstDictionary;
    use std::sync::Arc;

    create_test_pool!(
        PronounVerbAgreement,
        PronounVerbAgreement<Arc<FstDictionary>>,
        PronounVerbAgreement::new(FstDictionary::curated())
    );

    #[test]
    fn issue_233_1() {
        assert_suggestion_result("I likes this place.", test_linter(), "I like this place.");
    }

    #[test]
    fn issue_233_2() {
        assert_suggestion_result("I sits under the AC.", test_linter(), "I sit under the AC.");
    }

    #[test]
    #[ignore = "because 'like' is an adjective as well as a verb."]
    fn issue_233_1_reverse() {
        assert_suggestion_result("He like this place.", test_linter(), "He likes this place.");
    }

    #[test]
    fn why_we_cant_flag_like_yet() {
        assert_no_lints("What is he like?", test_linter());
    }

    #[test]
    fn issue_233_2_reverse() {
        assert_suggestion_result(
            "She sit under the AC.",
            test_linter(),
            "She sits under the AC.",
        );
    }

    #[test]
    fn dont_flag_correct_agreement() {
        assert_no_lints("He likes this place. I sit under the AC.", test_linter());
    }

    #[test]
    fn issue_3925_dont_flag_simple_past_forms() {
        assert_no_lints(
            "He fought tenaciously. He wrought tenaciously.",
            test_linter(),
        );
    }

    // Every pronoun systematically

    // Expected to get corrected

    #[test]
    fn fixes_i() {
        assert_suggestion_result("I wakes up.", test_linter(), "I wake up.");
    }

    #[test]
    fn fixes_we() {
        assert_suggestion_result("We gets dressed.", test_linter(), "We get dressed.");
    }

    #[test]
    fn fixes_you() {
        assert_suggestion_result(
            "You drops off the kids.",
            test_linter(),
            "You drop off the kids.",
        );
    }

    #[test]
    fn fixes_he() {
        assert_suggestion_result("He work hard.", test_linter(), "He works hard.");
    }

    #[test]
    fn fixes_she() {
        assert_suggestion_result("She study hard.", test_linter(), "She studies hard.");
    }

    #[test]
    #[ignore = "Becasue 'it' is also object case. Eg. 'watch it break down'"]
    fn we_cant_fix_it_yet() {
        assert_suggestion_result("It break down.", test_linter(), "It breaks down.");
    }

    #[test]
    fn why_we_cant_fix_it_yet() {
        assert_no_lints("I heard it break down.", test_linter());
    }

    #[test]
    fn fixes_they() {
        assert_suggestion_result("They repairs it.", test_linter(), "They repair it.")
    }

    // Correct phrases that are expected not to get corrected

    #[test]
    fn dont_flag_i() {
        assert_no_lints("I eat", test_linter());
    }

    #[test]
    fn dont_flag_we() {
        assert_no_lints("We drink", test_linter());
    }

    #[test]
    fn dont_flag_you() {
        assert_no_lints("You walk", test_linter());
    }

    #[test]
    fn dont_flag_he() {
        assert_no_lints("He runs", test_linter());
    }

    #[test]
    fn dont_flag_she() {
        assert_no_lints("She swims", test_linter());
    }

    #[test]
    fn dont_flag_it() {
        assert_no_lints("It works!", test_linter());
    }

    #[test]
    fn dont_flag_they() {
        assert_no_lints("They finish", test_linter());
    }

    // Ceck changing verb endings

    // -ies ↔ -y
    #[test]
    fn fix_flies() {
        assert_suggestion_result("I flies", test_linter(), "I fly");
    }
    #[test]
    fn fix_cry() {
        assert_suggestion_result("He cry", test_linter(), "He cries");
    }

    // -o ↔ -oes
    #[test]
    fn fix_go() {
        assert_suggestion_result("She go", test_linter(), "She goes");
    }
    #[test]
    fn fix_goes() {
        assert_suggestion_result("They goes", test_linter(), "They go");
    }

    // Check irregular changes

    // has ↔ have
    #[test]
    fn fix_has() {
        assert_suggestion_result("You has", test_linter(), "You have");
    }
    #[test]
    fn fix_have() {
        assert_suggestion_result("She have", test_linter(), "She has");
    }

    // hasn't ↔ haven't
    #[test]
    fn fix_hasnt() {
        assert_suggestion_result("You hasn't", test_linter(), "You haven't");
    }
    #[test]
    fn fix_havent() {
        assert_suggestion_result("He haven't", test_linter(), "He hasn't");
    }

    // -es
    #[test]
    fn fix_box() {
        assert_suggestion_result("He box", test_linter(), "He boxes");
    }
    #[test]
    fn fix_boxes() {
        assert_suggestion_result("You boxes", test_linter(), "You box");
    }

    // TODO: Are there any double consonant endings to change?
    // TODO: Are there any f ↔ v endings to change?

    // Negative contractions

    // doesn't ↔ don't
    #[test]
    fn fix_doesnt() {
        assert_suggestion_result("We doesn't", test_linter(), "We don't");
    }
    #[test]
    // Note: This requires a dedicated branch of the `[Expr]`
    fn fix_dont() {
        assert_suggestion_result("It don't", test_linter(), "It doesn't");
    }

    // Does do ↔ does behave differently to box ↔ boxes due to being an auxiliary verb?
    #[test]
    fn fix_do() {
        assert_suggestion_result("He do", test_linter(), "He does");
    }
    #[test]
    fn fix_does() {
        assert_suggestion_result("You does", test_linter(), "You do");
    }

    // False positives found by Elijah

    #[test]
    fn false_positive_she_consider() {
        assert_no_lints(
            "On April 10th, I suggested she consider a smaller, more intimate gathering.",
            test_linter(),
        );
    }

    #[test]
    fn false_positive_she_sell() {
        assert_no_lints(
            "I suggested she sell it and use the proceeds to help with her relocation expenses, or perhaps rent a similar camera while in Barcelona.",
            test_linter(),
        );
    }

    #[test]
    fn false_positive_she_rent() {
        assert_no_lints(
            "I suggested she sell it and use the proceeds to help with her relocation expenses, or perhaps rent a similar camera while in Barcelona.",
            test_linter(),
        );
    }

    #[test]
    fn false_positive_he_donned() {
        assert_no_lints(
            "He donned his heavy oilskins and descended the winding staircase, his boots echoing in the hollow tower.",
            test_linter(),
        );
    }

    #[test]
    fn false_positive_he_cannot() {
        assert_no_lints(
            "Surely, he cannot offer the same sum as the developers.",
            test_linter(),
        );
    }

    #[test]
    fn false_positive_insisting_she_return() {
        assert_no_lints(
            "Am I the asshole for insisting she return the dress?",
            test_linter(),
        );
    }

    #[test]
    fn false_positive_pride_in_you_is() {
        assert_no_lints(
            "It’s also important to recognize that your family's pride in you is a genuine reflection of your value.",
            test_linter(),
        );
    }

    #[test]
    fn false_positive_she_sought() {
        assert_no_lints(
            "She sought out Mrs. Hawthorne, the village’s oldest resident, a woman known for her vast knowledge of local history and her unsettlingly accurate intuition.",
            test_linter(),
        );
    }

    #[test]
    fn false_positive_lose_you_points() {
        assert_no_lints(
            "I admire your dedication to consistently drafting players who are actively trying to lose you points.",
            test_linter(),
        );
    }

    #[test]
    fn false_positive_she_hung_up() {
        assert_no_lints(
            "When I reiterated the conditions I'd previously set, she hung up on me.",
            test_linter(),
        );
    }
}
