use clippy_utils::consts::{ConstEvalCtxt, Constant};
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::sugg::Sugg;
use clippy_utils::{parent_item_name, sym};
use rustc_errors::Applicability;
use rustc_hir::{BinOpKind, Expr, ExprKind, UnOp};
use rustc_lint::LateContext;
use rustc_middle::ty;

use super::{FLOAT_CMP, FLOAT_CMP_CONST};

pub(crate) fn check<'tcx>(
    cx: &LateContext<'tcx>,
    expr: &'tcx Expr<'_>,
    op: BinOpKind,
    left: &'tcx Expr<'_>,
    right: &'tcx Expr<'_>,
) {
    if (op == BinOpKind::Eq || op == BinOpKind::Ne) && is_float(cx, left) {
        let ecx = ConstEvalCtxt::new(cx);
        let left_is_local = match ecx.eval_with_source(left) {
            Some((c, s)) if !is_allowed(&c) => s.is_local(),
            Some(_) => return,
            None => true,
        };
        let right_is_local = match ecx.eval_with_source(right) {
            Some((c, s)) if !is_allowed(&c) => s.is_local(),
            Some(_) => return,
            None => true,
        };

        // Allow comparing the results of signum()
        if is_signum(cx, left) && is_signum(cx, right) {
            return;
        }

        if let Some(name) = parent_item_name(cx, expr) {
            let name = name.as_str();
            if name == "eq" || name == "ne" || name == "is_nan" || name.starts_with("eq_") || name.ends_with("_eq") {
                return;
            }
        }
        let is_comparing_arrays = is_array(cx, left) || is_array(cx, right);
        let (lint, msg) = get_lint_and_message(left_is_local && right_is_local, is_comparing_arrays);
        span_lint_and_then(cx, lint, expr.span, msg, |diag| {
            let lhs = Sugg::hir(cx, left, "..");
            let rhs = Sugg::hir(cx, right, "..");

            if !is_comparing_arrays {
                diag.span_suggestion(
                    expr.span,
                    "consider comparing them within some margin of error",
                    format!(
                        "({}).abs() {} error_margin",
                        lhs - rhs,
                        if op == BinOpKind::Eq { '<' } else { '>' }
                    ),
                    Applicability::HasPlaceholders, // snippet
                );
            }
        });
    }
}

fn get_lint_and_message(is_local: bool, is_comparing_arrays: bool) -> (&'static rustc_lint::Lint, &'static str) {
    if is_local {
        (
            FLOAT_CMP,
            if is_comparing_arrays {
                "strict comparison of `f32` or `f64` arrays"
            } else {
                "strict comparison of `f32` or `f64`"
            },
        )
    } else {
        (
            FLOAT_CMP_CONST,
            if is_comparing_arrays {
                "strict comparison of `f32` or `f64` constant arrays"
            } else {
                "strict comparison of `f32` or `f64` constant"
            },
        )
    }
}

fn is_allowed(val: &Constant<'_>) -> bool {
    match val {
        // FIXME(f16_f128): add when equality check is available on all platforms
        &Constant::F32(f) => f == 0.0 || f.is_infinite(),
        &Constant::F64(f) => f == 0.0 || f.is_infinite(),
        Constant::Vec(vec) => vec.iter().all(|f| match f {
            Constant::F32(f) => *f == 0.0 || (*f).is_infinite(),
            Constant::F64(f) => *f == 0.0 || (*f).is_infinite(),
            _ => false,
        }),
        _ => false,
    }
}

// Return true if `expr` is the result of `signum()` invoked on a float value.
fn is_signum(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
    // The negation of a signum is still a signum
    if let ExprKind::Unary(UnOp::Neg, child_expr) = expr.kind {
        return is_signum(cx, child_expr);
    }

    if let ExprKind::MethodCall(method_name, self_arg, [], _) = expr.kind
        && method_name.ident.name == sym::signum
    // Check that the receiver of the signum() is a float (expressions[0] is the receiver of
    // the method call)
    {
        return is_float(cx, self_arg);
    }
    false
}

fn is_float(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
    let value = &cx.typeck_results().expr_ty(expr).peel_refs().kind();

    if let ty::Array(arr_ty, _) = value {
        return matches!(arr_ty.kind(), ty::Float(_));
    }

    matches!(value, ty::Float(_))
}

fn is_array(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
    matches!(&cx.typeck_results().expr_ty(expr).peel_refs().kind(), ty::Array(_, _))
}
