use clippy_config::Conf;
use clippy_utils::consts::{ConstEvalCtxt, Constant};
use clippy_utils::diagnostics::{span_lint, span_lint_and_then};
use clippy_utils::ty::{deref_chain, get_adt_inherent_method};
use clippy_utils::{higher, is_from_proc_macro, is_in_test, sym};
use rustc_ast::ast::RangeLimits;
use rustc_hir::{Expr, ExprKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::ty::{self, Ty};
use rustc_session::impl_lint_pass;

declare_clippy_lint! {
    /// ### What it does
    /// Checks for out of bounds array indexing with a constant
    /// index.
    ///
    /// ### Why is this bad?
    /// This will always panic at runtime.
    ///
    /// ### Example
    /// ```rust,no_run
    /// let x = [1, 2, 3, 4];
    ///
    /// x[9];
    /// &x[2..9];
    /// ```
    ///
    /// Use instead:
    /// ```no_run
    /// # let x = [1, 2, 3, 4];
    /// // Index within bounds
    ///
    /// x[0];
    /// x[3];
    /// ```
    #[clippy::version = "pre 1.29.0"]
    pub OUT_OF_BOUNDS_INDEXING,
    correctness,
    "out of bounds constant indexing"
}

declare_clippy_lint! {
    /// ### What it does
    /// Checks for usage of indexing or slicing that may panic at runtime.
    ///
    /// This lint does not report on indexing or slicing operations
    /// that always panic, clippy's `out_of_bound_indexing` already
    /// handles those cases.
    ///
    /// ### Why restrict this?
    /// To avoid implicit panics from indexing and slicing.
    ///
    /// There are “checked” alternatives which do not panic, and can be used with `unwrap()` to make
    /// an explicit panic when it is desired.
    ///
    /// ### Limitations
    /// This lint does not check for the usage of indexing or slicing on strings. These are covered
    /// by the more specific `string_slice` lint.
    ///
    /// ### Example
    /// ```rust,no_run
    /// // Vector
    /// let x = vec![0, 1, 2, 3];
    ///
    /// x[2];
    /// x[100];
    /// &x[2..100];
    ///
    /// // Array
    /// let y = [0, 1, 2, 3];
    ///
    /// let i = 10; // Could be a runtime value
    /// let j = 20;
    /// &y[i..j];
    /// ```
    ///
    /// Use instead:
    /// ```no_run
    /// # let x = vec![0, 1, 2, 3];
    /// x.get(2);
    /// x.get(100);
    /// x.get(2..100);
    ///
    /// # let y = [0, 1, 2, 3];
    /// let i = 10;
    /// let j = 20;
    /// y.get(i..j);
    /// ```
    #[clippy::version = "pre 1.29.0"]
    pub INDEXING_SLICING,
    restriction,
    "indexing/slicing usage"
}

impl_lint_pass!(IndexingSlicing => [INDEXING_SLICING, OUT_OF_BOUNDS_INDEXING]);

pub struct IndexingSlicing {
    allow_indexing_slicing_in_tests: bool,
    suppress_restriction_lint_in_const: bool,
}

impl IndexingSlicing {
    pub fn new(conf: &'static Conf) -> Self {
        Self {
            allow_indexing_slicing_in_tests: conf.allow_indexing_slicing_in_tests,
            suppress_restriction_lint_in_const: conf.suppress_restriction_lint_in_const,
        }
    }
}

impl<'tcx> LateLintPass<'tcx> for IndexingSlicing {
    fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
        if let ExprKind::Index(array, index, _) = &expr.kind
            && (!self.suppress_restriction_lint_in_const || !cx.tcx.hir_is_inside_const_context(expr.hir_id))
            && let expr_ty = cx.typeck_results().expr_ty(array)
            && let mut deref = deref_chain(cx, expr_ty)
            && deref.any(|l| {
                l.peel_refs().is_slice()
                    || l.peel_refs().is_array()
                    || ty_has_applicable_get_function(cx, l.peel_refs(), expr_ty, expr)
            })
            && !is_from_proc_macro(cx, expr)
        {
            let note = "the suggestion might not be applicable in constant blocks";
            let ty = cx.typeck_results().expr_ty(array).peel_refs();
            let allowed_in_tests = self.allow_indexing_slicing_in_tests && is_in_test(cx.tcx, expr.hir_id);
            if let Some(range) = higher::Range::hir(cx, index) {
                // Ranged indexes, i.e., &x[n..m], &x[n..], &x[..n] and &x[..]
                if let ty::Array(_, s) = ty.kind() {
                    let size: u128 = if let Some(size) = s.try_to_target_usize(cx.tcx) {
                        size.into()
                    } else {
                        return;
                    };

                    let const_range = to_const_range(cx, range, size);

                    if let (Some(start), _) = const_range
                        && start > size
                    {
                        span_lint(
                            cx,
                            OUT_OF_BOUNDS_INDEXING,
                            range.start.map_or(expr.span, |start| start.span),
                            "range is out of bounds",
                        );
                        return;
                    }

                    if let (_, Some(end)) = const_range
                        && end > size
                    {
                        span_lint(
                            cx,
                            OUT_OF_BOUNDS_INDEXING,
                            range.end.map_or(expr.span, |end| end.span),
                            "range is out of bounds",
                        );
                        return;
                    }

                    if let (Some(_), Some(_)) = const_range {
                        // early return because both start and end are constants
                        // and we have proven above that they are in bounds
                        return;
                    }
                }

                let help_msg = match (range.start, range.end) {
                    (None, Some(_)) => "consider using `.get(..n)`or `.get_mut(..n)` instead",
                    (Some(_), None) => "consider using `.get(n..)` or .get_mut(n..)` instead",
                    (Some(_), Some(_)) => "consider using `.get(n..m)` or `.get_mut(n..m)` instead",
                    (None, None) => return, // [..] is ok.
                };

                if allowed_in_tests {
                    return;
                }

                span_lint_and_then(cx, INDEXING_SLICING, expr.span, "slicing may panic", |diag| {
                    diag.help(help_msg);

                    if cx.tcx.hir_is_inside_const_context(expr.hir_id) {
                        diag.note(note);
                    }
                });
            } else {
                // Catchall non-range index, i.e., [n] or [n << m]
                if let ty::Array(..) = ty.kind() {
                    // Index is a const block.
                    if let ExprKind::ConstBlock(..) = index.kind {
                        return;
                    }
                    // Index is a constant uint.
                    if let Some(constant) = ConstEvalCtxt::new(cx).eval(index) {
                        // only `usize` index is legal in rust array index
                        // leave other type to rustc
                        if let Constant::Int(off) = constant
                            && off <= usize::MAX as u128
                            && let ty::Uint(utype) = cx.typeck_results().expr_ty(index).kind()
                            && *utype == ty::UintTy::Usize
                            && let ty::Array(_, s) = ty.kind()
                            && let Some(size) = s.try_to_target_usize(cx.tcx)
                        {
                            // get constant offset and check whether it is in bounds
                            let off = usize::try_from(off).unwrap();
                            let size = usize::try_from(size).unwrap();

                            if off >= size {
                                span_lint(cx, OUT_OF_BOUNDS_INDEXING, expr.span, "index is out of bounds");
                            }
                        }
                        // Let rustc's `const_err` lint handle constant `usize` indexing on arrays.
                        return;
                    }
                }

                if allowed_in_tests {
                    return;
                }

                span_lint_and_then(cx, INDEXING_SLICING, expr.span, "indexing may panic", |diag| {
                    diag.help("consider using `.get(n)` or `.get_mut(n)` instead");

                    if cx.tcx.hir_is_inside_const_context(expr.hir_id) {
                        diag.note(note);
                    }
                });
            }
        }
    }
}

/// Returns a tuple of options with the start and end (exclusive) values of
/// the range. If the start or end is not constant, None is returned.
fn to_const_range(cx: &LateContext<'_>, range: higher::Range<'_>, array_size: u128) -> (Option<u128>, Option<u128>) {
    let ecx = ConstEvalCtxt::new(cx);
    let s = range.start.map(|expr| ecx.eval(expr));
    let start = match s {
        Some(Some(Constant::Int(x))) => Some(x),
        Some(_) => None,
        None => Some(0),
    };

    let e = range.end.map(|expr| ecx.eval(expr));
    let end = match e {
        Some(Some(Constant::Int(x))) => {
            if range.limits == RangeLimits::Closed {
                Some(x + 1)
            } else {
                Some(x)
            }
        },
        Some(_) => None,
        None => Some(array_size),
    };

    (start, end)
}

/// Checks if the output Ty of the `get` method on this Ty (if any) matches the Ty returned by the
/// indexing operation (if any).
fn ty_has_applicable_get_function<'tcx>(
    cx: &LateContext<'tcx>,
    ty: Ty<'tcx>,
    array_ty: Ty<'tcx>,
    index_expr: &Expr<'_>,
) -> bool {
    if let ty::Adt(_, _) = array_ty.kind()
        && let Some(get_output_ty) = get_adt_inherent_method(cx, ty, sym::get).map(|m| {
            cx.tcx
                .fn_sig(m.def_id)
                .skip_binder()
                .output()
                .skip_binder()
        })
        && let ty::Adt(def, args) = get_output_ty.kind()
        && cx.tcx.is_diagnostic_item(sym::Option, def.0.did)
        && let Some(option_generic_param) = args.first()
        && let generic_ty = option_generic_param.expect_ty().peel_refs()
        // FIXME: ideally this would handle type params and projections properly, for now just assume it's the same type
        && (cx.typeck_results().expr_ty(index_expr).peel_refs() == generic_ty.peel_refs()
            || matches!(generic_ty.peel_refs().kind(), ty::Param(_) | ty::Alias(_, _)))
    {
        true
    } else {
        false
    }
}
