use super::*;
use std::convert::Infallible;

use full_moon::{
    ast::{self, Ast},
    visitors::Visitor,
};

pub struct CompareNanLint;

impl Lint for CompareNanLint {
    type Config = ();
    type Error = Infallible;

    const SEVERITY: Severity = Severity::Error;
    const LINT_TYPE: LintType = LintType::Correctness;

    fn new(_: Self::Config) -> Result<Self, Self::Error> {
        Ok(CompareNanLint)
    }

    fn pass(&self, ast: &Ast, _: &Context, _: &AstContext) -> Vec<Diagnostic> {
        let mut visitor = CompareNanVisitor {
            comparisons: Vec::new(),
        };

        visitor.visit_ast(ast);

        visitor
            .comparisons
            .iter()
            .map(|comparisons| {
                Diagnostic::new_complete(
                    "compare_nan",
                    "comparing things to nan directly is not allowed".to_owned(),
                    Label::new(comparisons.range),
                    vec![format!(
                        "try: `{variable} {operator} {variable}` instead",
                        variable = comparisons.variable,
                        operator = comparisons.operator,
                    )],
                    Vec::new(),
                )
            })
            .collect()
    }
}

struct CompareNanVisitor {
    comparisons: Vec<Comparison>,
}

struct Comparison {
    variable: String,
    operator: String,
    range: (usize, usize),
}

fn value_is_zero(value: &ast::Expression) -> bool {
    if let ast::Expression::Number(token) = value {
        token.token().to_string() == "0"
    } else {
        false
    }
}

fn expression_is_nan(node: &ast::Expression) -> bool {
    if_chain::if_chain! {
        if let ast::Expression::BinaryOperator { lhs, binop: ast::BinOp::Slash(_), rhs } = node;
        if value_is_zero(lhs) && value_is_zero(rhs);
        then {
            return true;
        }
    }
    false
}

impl Visitor for CompareNanVisitor {
    fn visit_expression(&mut self, node: &ast::Expression) {
        if_chain::if_chain! {
            if let ast::Expression::BinaryOperator { lhs, binop, rhs } = node;
            if let ast::Expression::Var(_) = lhs.as_ref();
            then {
                match binop {
                    ast::BinOp::TildeEqual(_) => {
                        if expression_is_nan(rhs) {
                            let range = node.range().unwrap();
                            self.comparisons.push(
                                Comparison {
                                    variable: lhs.to_string().trim().to_owned(),
                                    operator: "==".to_owned(),
                                    range: (range.0.bytes(), range.1.bytes()),
                                }
                            );
                        }
                    },
                    ast::BinOp::TwoEqual(_) => {
                        if expression_is_nan(rhs) {
                            let range = node.range().unwrap();
                            self.comparisons.push(
                                Comparison {
                                    variable: lhs.to_string().trim().to_owned(),
                                    operator: "~=".to_owned(),
                                    range: (range.0.bytes(), range.1.bytes()),
                                }
                            );
                        }
                    },
                    _ => {},
                }
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{super::test_util::test_lint, *};

    #[test]
    fn test_compare_nan_variables() {
        test_lint(
            CompareNanLint::new(()).unwrap(),
            "compare_nan",
            "compare_nan_variables",
        );
    }

    #[test]
    fn test_compare_nan_if() {
        test_lint(
            CompareNanLint::new(()).unwrap(),
            "compare_nan",
            "compare_nan_if",
        );
    }
}
