use crate::expr::Expr;
use crate::expr::FirstMatchOf;
use crate::expr::SequenceExpr;
use crate::{Token, TokenStringExt, patterns::WordSet};

use super::{ExprLinter, Lint, LintKind, Suggestion};
use crate::linting::expr_linter::Chunk;

pub struct AmountsFor {
    expr: FirstMatchOf,
}

impl Default for AmountsFor {
    fn default() -> Self {
        let singular_context = WordSet::new(&["that", "which", "it", "this"]);

        let singular_pattern = SequenceExpr::with(singular_context)
            .then_whitespace()
            .then_fixed_phrase("amounts for");

        let singular_context = WordSet::new(&[
            "they", "can", "could", "may", "might", "must", "should", "will", "would",
        ]);

        let plural_pattern = SequenceExpr::with(singular_context)
            .then_whitespace()
            .then_fixed_phrase("amount for");

        Self {
            expr: FirstMatchOf::new(vec![Box::new(singular_pattern), Box::new(plural_pattern)]),
        }
    }
}

impl ExprLinter for AmountsFor {
    type Unit = Chunk;

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

    fn match_to_lint(&self, toks: &[Token], src: &[char]) -> Option<Lint> {
        let content = toks.span()?.get_content_string(src).to_lowercase();

        if content.ends_with("amounts for") {
            let span = toks[2..5].span()?;

            return Some(Lint {
                span,
                lint_kind: LintKind::WordChoice,
                suggestions: vec![
                    Suggestion::replace_with_match_case(
                        "amounts to".chars().collect(),
                        span.get_content(src),
                    ),
                    Suggestion::replace_with_match_case(
                        "accounts for".chars().collect(),
                        span.get_content(src),
                    ),
                ],
                message: "`amounts for` is not idiomatic English. You probably meant `amounts to` or `accounts for`.".to_owned(),
                priority: 63,
            });
        }

        if content.ends_with("amount for") {
            let span = toks[2..5].span()?;

            return Some(Lint {
                span,
                lint_kind: LintKind::WordChoice,
                suggestions: vec![
                    Suggestion::replace_with_match_case(
                        "amount to".chars().collect(),
                        span.get_content(src),
                    ),
                    Suggestion::replace_with_match_case(
                        "account for".chars().collect(),
                        span.get_content(src),
                    ),
                ],
                message: "`amounts for` is not idiomatic English. You probably meant `amounts to` or `accounts for`.".to_owned(),
                priority: 63,
            });
        }

        None
    }

    fn description(&self) -> &str {
        "Corrects `amounts for` to either `amounts to` or `accounts for`"
    }
}

#[cfg(test)]
mod tests {
    use super::AmountsFor;
    use crate::linting::tests::assert_suggestion_result;

    #[test]
    fn corrects_that_amounts_for_to_amounts_to_entire_value() {
        assert_suggestion_result(
            "Skyler stated the car wash is worth close to $800k, that amounts for the entire value of the company",
            AmountsFor::default(),
            "Skyler stated the car wash is worth close to $800k, that amounts to the entire value of the company",
        );
    }

    #[test]
    fn corrects_that_amounts_for_to_amounts_to_percent() {
        assert_suggestion_result(
            "Together, that amounts for 1157 calls or 60% of the failures.",
            AmountsFor::default(),
            "Together, that amounts to 1157 calls or 60% of the failures.",
        );
    }

    #[test]
    fn corrects_that_amounts_for_to_accounts_for_setting_up() {
        assert_suggestion_result(
            "One solution to this would be to have separate controllers but the amount of code that amounts for setting up, processing and calling",
            AmountsFor::default(),
            "One solution to this would be to have separate controllers but the amount of code that accounts for setting up, processing and calling",
        );
    }

    #[test]
    fn corrects_which_amounts_for_to_accounts_for_16k() {
        assert_suggestion_result(
            "It has an offset of 0xC000 which amounts for the 16k.",
            AmountsFor::default(),
            "It has an offset of 0xC000 which accounts for the 16k.",
        );
    }

    #[test]
    fn corrects_this_amounts_for_to_accounts_for_large_part() {
        assert_suggestion_result(
            "I'm pretty sure that this amounts for a large part of the speed I gained when typing, in addition to touch-typing.",
            AmountsFor::default(),
            "I'm pretty sure that this accounts for a large part of the speed I gained when typing, in addition to touch-typing.",
        );
    }

    #[test]
    fn corrects_they_amount_for_to_amount_to_16kb() {
        assert_suggestion_result(
            "it is obvious that the messages are being held \"somewhere\" until they amount for 16kB and then the whole lot come at once.",
            AmountsFor::default(),
            "it is obvious that the messages are being held \"somewhere\" until they amount to 16kB and then the whole lot come at once.",
        );
    }

    #[test]
    fn corrects_which_amounts_for_to_amounts_to_10_minutes() {
        assert_suggestion_result(
            "set a small TTL for your hostname (like 600 which amounts for 10 minutes).",
            AmountsFor::default(),
            "set a small TTL for your hostname (like 600 which amounts to 10 minutes).",
        );
    }

    #[test]
    fn corrects_it_amounts_for_to_amounts_to_redefinition() {
        assert_suggestion_result(
            "included for convenience to get a Lorentz invariant result (it amounts for a redefinition of ap).",
            AmountsFor::default(),
            "included for convenience to get a Lorentz invariant result (it amounts to a redefinition of ap).",
        );
    }

    #[test]
    fn corrects_they_amount_for_to_amount_to_nothing() {
        assert_suggestion_result(
            "Matter and antimatter are spread throughout the Universe, and in total, they amount for nothing",
            AmountsFor::default(),
            "Matter and antimatter are spread throughout the Universe, and in total, they amount to nothing",
        );
    }

    #[test]
    fn would_amount_for_to_amount_to_api_requests() {
        assert_suggestion_result(
            "10% of 6,782,091 would amount for 678,209 API requests",
            AmountsFor::default(),
            "10% of 6,782,091 would amount to 678,209 API requests",
        );
    }

    #[test]
    fn will_amount_for_to_amount_to_relationships() {
        assert_suggestion_result(
            "Consider this statistic from Gartner, that artificial intelligence will amount for 85% of customer relationships by 2020.",
            AmountsFor::default(),
            "Consider this statistic from Gartner, that artificial intelligence will amount to 85% of customer relationships by 2020.",
        );
    }

    #[test]
    fn should_amount_for_to_amount_to_half_pack() {
        assert_suggestion_result(
            "It doesn't seem realistic that this single elite should amount for half the pack",
            AmountsFor::default(),
            "It doesn't seem realistic that this single elite should amount to half the pack",
        );
    }

    #[test]
    fn can_amount_for_to_amount_to_draw_calls() {
        assert_suggestion_result(
            "That can amount for a lot of draw calls and work for the engine to cull. ",
            AmountsFor::default(),
            "That can amount to a lot of draw calls and work for the engine to cull. ",
        );
    }
}
