use clippy_utils::consts::{ConstEvalCtxt, Constant};
use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then};
use clippy_utils::msrvs::{self, Msrv};
use clippy_utils::res::{MaybeDef, MaybeResPath};
use clippy_utils::source::snippet_with_context;
use clippy_utils::usage::local_used_after_expr;
use clippy_utils::visitors::{Descend, for_each_expr};
use clippy_utils::{paths, sym};
use core::ops::ControlFlow;
use rustc_errors::Applicability;
use rustc_hir::{
    BindingMode, Expr, ExprKind, HirId, LangItem, LetStmt, MatchSource, Node, Pat, PatKind, Stmt, StmtKind,
};
use rustc_lint::LateContext;
use rustc_middle::ty;
use rustc_span::{Span, Symbol, SyntaxContext};

use super::{MANUAL_SPLIT_ONCE, NEEDLESS_SPLITN};

pub(super) fn check(
    cx: &LateContext<'_>,
    method_name: Symbol,
    expr: &Expr<'_>,
    self_arg: &Expr<'_>,
    pat_arg: &Expr<'_>,
    count: u128,
    msrv: Msrv,
) {
    if count < 2 || !cx.typeck_results().expr_ty_adjusted(self_arg).peel_refs().is_str() {
        return;
    }

    let needless = |usage_kind| match usage_kind {
        IterUsageKind::Nth(n) => count > n + 1,
        IterUsageKind::NextTuple => count > 2,
    };
    let manual = count == 2 && msrv.meets(cx, msrvs::STR_SPLIT_ONCE);

    match parse_iter_usage(cx, expr.span.ctxt(), cx.tcx.hir_parent_iter(expr.hir_id)) {
        Some(usage) if needless(usage.kind) => lint_needless(cx, method_name, expr, self_arg, pat_arg),
        Some(usage) if manual => check_manual_split_once(cx, method_name, expr, self_arg, pat_arg, &usage),
        None if manual => {
            check_manual_split_once_indirect(cx, method_name, expr, self_arg, pat_arg);
        },
        _ => {},
    }
}

fn lint_needless(cx: &LateContext<'_>, method_name: Symbol, expr: &Expr<'_>, self_arg: &Expr<'_>, pat_arg: &Expr<'_>) {
    let mut app = Applicability::MachineApplicable;
    let r = if method_name == sym::splitn { "" } else { "r" };

    span_lint_and_sugg(
        cx,
        NEEDLESS_SPLITN,
        expr.span,
        format!("unnecessary use of `{r}splitn`"),
        "try",
        format!(
            "{}.{r}split({})",
            snippet_with_context(cx, self_arg.span, expr.span.ctxt(), "..", &mut app).0,
            snippet_with_context(cx, pat_arg.span, expr.span.ctxt(), "..", &mut app).0,
        ),
        app,
    );
}

fn check_manual_split_once(
    cx: &LateContext<'_>,
    method_name: Symbol,
    expr: &Expr<'_>,
    self_arg: &Expr<'_>,
    pat_arg: &Expr<'_>,
    usage: &IterUsage,
) {
    let ctxt = expr.span.ctxt();
    let (msg, reverse) = if method_name == sym::splitn {
        ("manual implementation of `split_once`", false)
    } else {
        ("manual implementation of `rsplit_once`", true)
    };

    let mut app = Applicability::MachineApplicable;
    let self_snip = snippet_with_context(cx, self_arg.span, ctxt, "..", &mut app).0;
    let pat_snip = snippet_with_context(cx, pat_arg.span, ctxt, "..", &mut app).0;

    let sugg = match usage.kind {
        IterUsageKind::NextTuple => {
            if reverse {
                format!("{self_snip}.rsplit_once({pat_snip}).map(|(x, y)| (y, x))")
            } else {
                format!("{self_snip}.split_once({pat_snip})")
            }
        },
        IterUsageKind::Nth(1) => {
            let (r, field) = if reverse { ("r", 0) } else { ("", 1) };

            match usage.unwrap_kind {
                Some(UnwrapKind::Unwrap) => {
                    format!("{self_snip}.{r}split_once({pat_snip}).unwrap().{field}")
                },
                Some(UnwrapKind::QuestionMark) => {
                    format!("{self_snip}.{r}split_once({pat_snip})?.{field}")
                },
                None => {
                    format!("{self_snip}.{r}split_once({pat_snip}).map(|x| x.{field})")
                },
            }
        },
        IterUsageKind::Nth(_) => return,
    };

    span_lint_and_sugg(cx, MANUAL_SPLIT_ONCE, usage.span, msg, "try", sugg, app);
}

/// checks for
///
/// ```no_run
/// let mut iter = "a.b.c".splitn(2, '.');
/// let a = iter.next();
/// let b = iter.next();
/// ```
fn check_manual_split_once_indirect(
    cx: &LateContext<'_>,
    method_name: Symbol,
    expr: &Expr<'_>,
    self_arg: &Expr<'_>,
    pat_arg: &Expr<'_>,
) -> Option<()> {
    let ctxt = expr.span.ctxt();
    let mut parents = cx.tcx.hir_parent_iter(expr.hir_id);
    if let (_, Node::LetStmt(local)) = parents.next()?
        && let PatKind::Binding(BindingMode::MUT, iter_binding_id, _, None) = local.pat.kind
        && let (iter_stmt_id, Node::Stmt(_)) = parents.next()?
        && let (_, Node::Block(enclosing_block)) = parents.next()?
        && let mut stmts = enclosing_block
            .stmts
            .iter()
            .skip_while(|stmt| stmt.hir_id != iter_stmt_id)
            .skip(1)
        && let first = indirect_usage(cx, stmts.next()?, iter_binding_id, ctxt)?
        && let second = indirect_usage(cx, stmts.next()?, iter_binding_id, ctxt)?
        && first.unwrap_kind == second.unwrap_kind
        && first.name != second.name
        && !local_used_after_expr(cx, iter_binding_id, second.init_expr)
    {
        let (r, lhs, rhs) = if method_name == sym::splitn {
            ("", first.name, second.name)
        } else {
            ("r", second.name, first.name)
        };
        let msg = format!("manual implementation of `{r}split_once`");

        let mut app = Applicability::MachineApplicable;
        let self_snip = snippet_with_context(cx, self_arg.span, ctxt, "..", &mut app).0;
        let pat_snip = snippet_with_context(cx, pat_arg.span, ctxt, "..", &mut app).0;

        span_lint_and_then(cx, MANUAL_SPLIT_ONCE, local.span, msg, |diag| {
            diag.span_label(first.span, "first usage here");
            diag.span_label(second.span, "second usage here");

            let unwrap = match first.unwrap_kind {
                UnwrapKind::Unwrap => ".unwrap()",
                UnwrapKind::QuestionMark => "?",
            };

            // Add a multipart suggestion
            diag.multipart_suggestion(
                format!("replace with `{r}split_once`"),
                vec![
                    (
                        local.span,
                        format!("let ({lhs}, {rhs}) = {self_snip}.{r}split_once({pat_snip}){unwrap};"),
                    ),
                    (first.span, String::new()),  // Remove the first usage
                    (second.span, String::new()), // Remove the second usage
                ],
                app,
            );
        });
    }

    Some(())
}

#[derive(Debug)]
struct IndirectUsage<'a> {
    name: Symbol,
    span: Span,
    init_expr: &'a Expr<'a>,
    unwrap_kind: UnwrapKind,
}

/// returns `Some(IndirectUsage)` for e.g.
///
/// ```ignore
/// let name = binding.next()?;
/// let name = binding.next().unwrap();
/// ```
fn indirect_usage<'tcx>(
    cx: &LateContext<'tcx>,
    stmt: &Stmt<'tcx>,
    binding: HirId,
    ctxt: SyntaxContext,
) -> Option<IndirectUsage<'tcx>> {
    if let StmtKind::Let(&LetStmt {
        pat: Pat {
            kind: PatKind::Binding(BindingMode::NONE, _, ident, None),
            ..
        },
        init: Some(init_expr),
        hir_id: local_hir_id,
        ..
    }) = stmt.kind
    {
        let mut path_to_binding = None;
        let _: Option<!> = for_each_expr(cx, init_expr, |e| {
            if e.res_local_id() == Some(binding) {
                path_to_binding = Some(e);
            }
            ControlFlow::Continue(Descend::from(path_to_binding.is_none()))
        });

        let mut parents = cx.tcx.hir_parent_iter(path_to_binding?.hir_id);
        let iter_usage = parse_iter_usage(cx, ctxt, &mut parents)?;

        let (parent_id, _) = parents.find(|(_, node)| {
            !matches!(
                node,
                Node::Expr(Expr {
                    kind: ExprKind::Match(.., MatchSource::TryDesugar(_)),
                    ..
                })
            )
        })?;

        if let IterUsage {
            kind: IterUsageKind::Nth(0),
            unwrap_kind: Some(unwrap_kind),
            ..
        } = iter_usage
            && parent_id == local_hir_id
        {
            return Some(IndirectUsage {
                name: ident.name,
                span: stmt.span,
                init_expr,
                unwrap_kind,
            });
        }
    }

    None
}

#[derive(Debug, Clone, Copy)]
enum IterUsageKind {
    Nth(u128),
    NextTuple,
}

#[derive(Debug, PartialEq, Eq)]
enum UnwrapKind {
    Unwrap,
    QuestionMark,
}

#[derive(Debug)]
struct IterUsage {
    kind: IterUsageKind,
    unwrap_kind: Option<UnwrapKind>,
    span: Span,
}

fn parse_iter_usage<'tcx>(
    cx: &LateContext<'tcx>,
    ctxt: SyntaxContext,
    mut iter: impl Iterator<Item = (HirId, Node<'tcx>)>,
) -> Option<IterUsage> {
    let (kind, span) = match iter.next() {
        Some((_, Node::Expr(e))) if e.span.ctxt() == ctxt => {
            let ExprKind::MethodCall(name, _, args, _) = e.kind else {
                return None;
            };
            let did = cx.typeck_results().type_dependent_def_id(e.hir_id)?;
            let iter_id = cx.tcx.get_diagnostic_item(sym::Iterator)?;

            match (name.ident.name, args) {
                (sym::next, []) if cx.tcx.trait_of_assoc(did) == Some(iter_id) => (IterUsageKind::Nth(0), e.span),
                (sym::next_tuple, []) => {
                    return if paths::ITERTOOLS_NEXT_TUPLE.matches(cx, did)
                        && let ty::Adt(adt_def, subs) = cx.typeck_results().expr_ty(e).kind()
                        && cx.tcx.is_diagnostic_item(sym::Option, adt_def.did())
                        && let ty::Tuple(subs) = subs.type_at(0).kind()
                        && subs.len() == 2
                    {
                        Some(IterUsage {
                            kind: IterUsageKind::NextTuple,
                            unwrap_kind: None,
                            span: e.span,
                        })
                    } else {
                        None
                    };
                },
                (sym::nth | sym::skip, [idx_expr]) if cx.tcx.trait_of_assoc(did) == Some(iter_id) => {
                    if let Some(Constant::Int(idx)) = ConstEvalCtxt::new(cx).eval_local(idx_expr, ctxt) {
                        let span = if name.ident.as_str() == "nth" {
                            e.span
                        } else if let Some((_, Node::Expr(next_expr))) = iter.next()
                            && let ExprKind::MethodCall(next_name, _, [], _) = next_expr.kind
                            && next_name.ident.name == sym::next
                            && next_expr.span.ctxt() == ctxt
                            && let Some(next_id) = cx.typeck_results().type_dependent_def_id(next_expr.hir_id)
                            && cx.tcx.trait_of_assoc(next_id) == Some(iter_id)
                        {
                            next_expr.span
                        } else {
                            return None;
                        };
                        (IterUsageKind::Nth(idx), span)
                    } else {
                        return None;
                    }
                },
                _ => return None,
            }
        },
        _ => return None,
    };

    let (unwrap_kind, span) = if let Some((_, Node::Expr(e))) = iter.next() {
        match e.kind {
            ExprKind::Call(
                &Expr {
                    kind: ExprKind::Path(qpath),
                    ..
                },
                [_],
            ) if cx.tcx.qpath_is_lang_item(qpath, LangItem::TryTraitBranch) => {
                let parent_span = e.span.parent_callsite().unwrap();
                if parent_span.ctxt() == ctxt {
                    (Some(UnwrapKind::QuestionMark), parent_span)
                } else {
                    (None, span)
                }
            },
            _ if e.span.ctxt() != ctxt => (None, span),
            ExprKind::MethodCall(name, _, [], _)
                if name.ident.name == sym::unwrap
                    && cx
                        .typeck_results()
                        .type_dependent_def_id(e.hir_id)
                        .opt_parent(cx)
                        .opt_impl_ty(cx)
                        .is_diag_item(cx, sym::Option) =>
            {
                (Some(UnwrapKind::Unwrap), e.span)
            },
            _ => (None, span),
        }
    } else {
        (None, span)
    };

    Some(IterUsage {
        kind,
        unwrap_kind,
        span,
    })
}
