use crate::{
    Token,
    expr::{Expr, OwnedExprExt, SequenceExpr},
    linting::expr_linter::Chunk,
    linting::{ExprLinter, Lint, LintKind, Suggestion},
    patterns::DerivedFrom,
};

pub struct TakeMedicine {
    expr: SequenceExpr,
}

impl Default for TakeMedicine {
    fn default() -> Self {
        let eat_verb = DerivedFrom::new_from_str("eat")
            .or(DerivedFrom::new_from_str("eats"))
            .or(DerivedFrom::new_from_str("ate"))
            .or(DerivedFrom::new_from_str("eating"))
            .or(DerivedFrom::new_from_str("eaten"));

        let medication = DerivedFrom::new_from_str("antibiotic")
            .or(DerivedFrom::new_from_str("medicine"))
            .or(DerivedFrom::new_from_str("medication"))
            .or(DerivedFrom::new_from_str("pill"))
            .or(DerivedFrom::new_from_str("tablet"))
            .or(DerivedFrom::new_from_str("aspirin"))
            .or(DerivedFrom::new_from_str("paracetamol"));

        let modifiers = SequenceExpr::any_of(vec![
            Box::new(SequenceExpr::default().then_determiner()),
            Box::new(SequenceExpr::default().then_possessive_determiner()),
            Box::new(SequenceExpr::default().then_quantifier()),
        ])
        .t_ws();

        let adjectives = SequenceExpr::default().then_one_or_more_adjectives().t_ws();

        let pattern = SequenceExpr::with(eat_verb)
            .t_ws()
            .then_optional(modifiers)
            .then_optional(adjectives)
            .then(medication);

        Self { expr: pattern }
    }
}

fn replacement_for(
    verb: &Token,
    source: &[char],
    base: &str,
    third_person: &str,
    past: &str,
    past_participle: &str,
    progressive: &str,
) -> Suggestion {
    let replacement = if verb.kind.is_verb_progressive_form() {
        progressive
    } else if verb.kind.is_verb_third_person_singular_present_form() {
        third_person
    } else if verb.kind.is_verb_past_participle_form() && !verb.kind.is_verb_simple_past_form() {
        past_participle
    } else if verb.kind.is_verb_simple_past_form() {
        past
    } else {
        base
    };

    Suggestion::replace_with_match_case(replacement.chars().collect(), verb.get_ch(source))
}

impl ExprLinter for TakeMedicine {
    type Unit = Chunk;

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

    fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option<Lint> {
        let verb = matched_tokens.first()?;
        let span = verb.span;

        let suggestions = vec![
            replacement_for(verb, source, "take", "takes", "took", "taken", "taking"),
            replacement_for(
                verb,
                source,
                "swallow",
                "swallows",
                "swallowed",
                "swallowed",
                "swallowing",
            ),
        ];

        Some(Lint {
            span,
            lint_kind: LintKind::Usage,
            suggestions,
            message: "Use a verb like `take` or `swallow` with medicine instead of `eat`."
                .to_owned(),
            priority: 63,
        })
    }

    fn description(&self) -> &'static str {
        "Encourages pairing medicine-related nouns with verbs like `take` or `swallow` instead of `eat`."
    }
}

#[cfg(test)]
mod tests {
    use super::TakeMedicine;
    use crate::linting::tests::{assert_lint_count, assert_suggestion_result};

    #[test]
    fn swaps_ate_antibiotics() {
        assert_suggestion_result(
            "I ate antibiotics for a week.",
            TakeMedicine::default(),
            "I took antibiotics for a week.",
        );
    }

    #[test]
    fn swaps_eat_medicine() {
        assert_suggestion_result(
            "You should eat the medicine now.",
            TakeMedicine::default(),
            "You should take the medicine now.",
        );
    }

    #[test]
    fn swaps_eats_medication() {
        assert_suggestion_result(
            "She eats medication daily.",
            TakeMedicine::default(),
            "She takes medication daily.",
        );
    }

    #[test]
    fn swaps_eating_medicines() {
        assert_suggestion_result(
            "Are you eating medicines for that illness?",
            TakeMedicine::default(),
            "Are you taking medicines for that illness?",
        );
    }

    #[test]
    fn swaps_eaten_medication() {
        assert_suggestion_result(
            "He has eaten medication already.",
            TakeMedicine::default(),
            "He has taken medication already.",
        );
    }

    #[test]
    fn swaps_eat_pills() {
        assert_suggestion_result(
            "He ate the pills without water.",
            TakeMedicine::default(),
            "He took the pills without water.",
        );
    }

    #[test]
    fn swaps_eating_paracetamol() {
        assert_suggestion_result(
            "She is eating paracetamol for her headache.",
            TakeMedicine::default(),
            "She is taking paracetamol for her headache.",
        );
    }

    #[test]
    fn handles_possessive_modifier() {
        assert_suggestion_result(
            "Please eat my antibiotics.",
            TakeMedicine::default(),
            "Please take my antibiotics.",
        );
    }

    #[test]
    fn handles_adjectives() {
        assert_suggestion_result(
            "They ate the prescribed antibiotics.",
            TakeMedicine::default(),
            "They took the prescribed antibiotics.",
        );
    }

    #[test]
    fn supports_uppercase() {
        assert_suggestion_result(
            "Eat antibiotics with water.",
            TakeMedicine::default(),
            "Take antibiotics with water.",
        );
    }

    #[test]
    fn offers_swallow_alternative() {
        assert_suggestion_result(
            "He ate the medication without water.",
            TakeMedicine::default(),
            "He swallowed the medication without water.",
        );
    }

    #[test]
    fn ignores_correct_usage() {
        assert_lint_count(
            "She took antibiotics last winter.",
            TakeMedicine::default(),
            0,
        );
    }

    #[test]
    fn ignores_unrelated_eat() {
        assert_lint_count(
            "They ate dinner after taking medicine.",
            TakeMedicine::default(),
            0,
        );
    }
}
