//! This module contains a helper for converting a field access expression into a
//! path expression. This is used when destructuring a tuple or struct.
//!
//! It determines whether to deref the new expression and/or wrap it in parentheses,
//! based on the parent of the existing expression.
use syntax::{
    AstNode, T,
    ast::{self, FieldExpr, MethodCallExpr, syntax_factory::SyntaxFactory},
};

use crate::AssistContext;

/// Decides whether the new path expression needs to be dereferenced and/or wrapped in parens.
/// Returns the relevant parent expression to replace and the [RefData].
pub(crate) fn determine_ref_and_parens(
    ctx: &AssistContext<'_, '_>,
    field_expr: &FieldExpr,
) -> (ast::Expr, RefData) {
    let s = field_expr.syntax();
    let mut ref_data = RefData { needs_deref: true, needs_parentheses: true };
    let mut target_node = field_expr.clone().into();

    let parent = match s.parent().map(ast::Expr::cast) {
        Some(Some(parent)) => parent,
        Some(None) => {
            ref_data.needs_parentheses = false;
            return (target_node, ref_data);
        }
        None => return (target_node, ref_data),
    };

    match parent {
        ast::Expr::ParenExpr(it) => {
            // already parens in place -> don't replace
            ref_data.needs_parentheses = false;
            // there might be a ref outside: `&(t.0)` -> can be removed
            if let Some(it) = it.syntax().parent().and_then(ast::RefExpr::cast) {
                ref_data.needs_deref = false;
                target_node = it.into();
            }
        }
        ast::Expr::RefExpr(it) => {
            // `&*` -> cancel each other out
            ref_data.needs_deref = false;
            ref_data.needs_parentheses = false;
            // might be surrounded by parens -> can be removed too
            match it.syntax().parent().and_then(ast::ParenExpr::cast) {
                Some(parent) => target_node = parent.into(),
                None => target_node = it.into(),
            };
        }
        // higher precedence than deref `*`
        // https://doc.rust-lang.org/reference/expressions.html#expression-precedence
        // -> requires parentheses
        ast::Expr::PathExpr(_it) => {}
        ast::Expr::MethodCallExpr(it) => {
            // `field_expr` is `self_param` (otherwise it would be in `ArgList`)

            // test if there's already auto-ref in place (`value` -> `&value`)
            // -> no method accepting `self`, but `&self` -> no need for deref
            //
            // other combinations (`&value` -> `value`, `&&value` -> `&value`, `&value` -> `&&value`) might or might not be able to auto-ref/deref,
            // but there might be trait implementations an added `&` might resolve to
            // -> ONLY handle auto-ref from `value` to `&value`
            fn is_auto_ref(ctx: &AssistContext<'_, '_>, call_expr: &MethodCallExpr) -> bool {
                fn impl_(ctx: &AssistContext<'_, '_>, call_expr: &MethodCallExpr) -> Option<bool> {
                    let rec = call_expr.receiver()?;
                    let rec_ty = ctx.sema.type_of_expr(&rec)?.original();
                    // input must be actual value
                    if rec_ty.is_reference() {
                        return Some(false);
                    }

                    // doesn't resolve trait impl
                    let f = ctx.sema.resolve_method_call(call_expr)?;
                    let self_param = f.self_param(ctx.db())?;
                    // self must be ref
                    match self_param.access(ctx.db()) {
                        hir::Access::Shared | hir::Access::Exclusive => Some(true),
                        hir::Access::Owned => Some(false),
                    }
                }
                impl_(ctx, call_expr).unwrap_or(false)
            }

            if is_auto_ref(ctx, &it) {
                ref_data.needs_deref = false;
                ref_data.needs_parentheses = false;
            }
        }
        ast::Expr::FieldExpr(_it) => {
            // `t.0.my_field`
            ref_data.needs_deref = false;
            ref_data.needs_parentheses = false;
        }
        ast::Expr::IndexExpr(_it) => {
            // `t.0[1]`
            ref_data.needs_deref = false;
            ref_data.needs_parentheses = false;
        }
        ast::Expr::TryExpr(_it) => {
            // `t.0?`
            // requires deref and parens: `(*_0)`
        }
        // lower precedence than deref `*` -> no parens
        _ => {
            ref_data.needs_parentheses = false;
        }
    };

    (target_node, ref_data)
}

/// Indicates whether to deref an expression or wrap it in parens
pub(crate) struct RefData {
    needs_deref: bool,
    needs_parentheses: bool,
}

impl RefData {
    /// Derefs `expr` and wraps it in parens if necessary
    pub(crate) fn wrap_expr(&self, mut expr: ast::Expr, make: &SyntaxFactory) -> ast::Expr {
        if self.needs_deref {
            expr = make.expr_prefix(T![*], expr).into();
        }

        if self.needs_parentheses {
            expr = make.expr_paren(expr).into();
        }

        expr
    }

    pub(crate) fn wrap_expr_with_factory(
        &self,
        mut expr: ast::Expr,
        syntax_factory: &SyntaxFactory,
    ) -> ast::Expr {
        if self.needs_deref {
            expr = syntax_factory.expr_prefix(T![*], expr).into();
        }

        if self.needs_parentheses {
            expr = syntax_factory.expr_paren(expr).into();
        }

        expr
    }
}
