//! Some helper functions for `AutoDeref`
use super::method::MethodCallee;
use super::{FnCtxt, PlaceOp};

use rustc_infer::infer::InferOk;
use rustc_middle::ty::adjustment::{Adjust, Adjustment, OverloadedDeref};
use rustc_middle::ty::{self, Ty};
use rustc_span::Span;
use rustc_trait_selection::autoderef::{Autoderef, AutoderefKind};

use std::iter;

impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
    pub fn autoderef(&'a self, span: Span, base_ty: Ty<'tcx>) -> Autoderef<'a, 'tcx> {
        Autoderef::new(self, self.param_env, self.body_id, span, base_ty, span)
    }

    /// Like `autoderef`, but provides a custom `Span` to use for calls to
    /// an overloaded `Deref` operator
    pub fn autoderef_overloaded_span(
        &'a self,
        span: Span,
        base_ty: Ty<'tcx>,
        overloaded_span: Span,
    ) -> Autoderef<'a, 'tcx> {
        Autoderef::new(self, self.param_env, self.body_id, span, base_ty, overloaded_span)
    }

    pub fn try_overloaded_deref(
        &self,
        span: Span,
        base_ty: Ty<'tcx>,
    ) -> Option<InferOk<'tcx, MethodCallee<'tcx>>> {
        self.try_overloaded_place_op(span, base_ty, &[], PlaceOp::Deref)
    }

    /// Returns the adjustment steps.
    pub fn adjust_steps(&self, autoderef: &Autoderef<'a, 'tcx>) -> Vec<Adjustment<'tcx>> {
        self.register_infer_ok_obligations(self.adjust_steps_as_infer_ok(autoderef))
    }

    pub fn adjust_steps_as_infer_ok(
        &self,
        autoderef: &Autoderef<'a, 'tcx>,
    ) -> InferOk<'tcx, Vec<Adjustment<'tcx>>> {
        let mut obligations = vec![];
        let steps = autoderef.steps();
        let targets =
            steps.iter().skip(1).map(|&(ty, _)| ty).chain(iter::once(autoderef.final_ty(false)));
        let steps: Vec<_> = steps
            .iter()
            .map(|&(source, kind)| {
                if let AutoderefKind::Overloaded = kind {
                    self.try_overloaded_deref(autoderef.span(), source).and_then(
                        |InferOk { value: method, obligations: o }| {
                            obligations.extend(o);
                            if let ty::Ref(region, _, mutbl) = *method.sig.output().kind() {
                                Some(OverloadedDeref {
                                    region,
                                    mutbl,
                                    span: autoderef.overloaded_span(),
                                })
                            } else {
                                None
                            }
                        },
                    )
                } else {
                    None
                }
            })
            .zip(targets)
            .map(|(autoderef, target)| Adjustment { kind: Adjust::Deref(autoderef), target })
            .collect();

        InferOk { obligations, value: steps }
    }
}
