//! Helper code for building a linked list of user-type projections on the
//! stack while visiting a THIR pattern.
//!
//! This avoids having to repeatedly clone a partly-built [`UserTypeProjections`]
//! at every step of the traversal, which is what the previous code was doing.

use std::{assert_matches, iter};

use rustc_abi::{FieldIdx, VariantIdx};
use rustc_data_structures::smallvec::SmallVec;
use rustc_middle::mir::{ProjectionElem, UserTypeProjection, UserTypeProjections};
use rustc_middle::ty::{AdtDef, UserTypeAnnotationIndex};
use rustc_span::Symbol;

/// A single `thir::Pat` node should almost never have more than 0-2 user types.
/// We can store up to 4 inline in the same size as an ordinary `Vec`.
pub(crate) type UserTypeIndices = SmallVec<[UserTypeAnnotationIndex; 4]>;

/// One of a list of "operations" that can be used to lazily build projections
/// of user-specified types.
#[derive(Debug)]
pub(crate) enum ProjectedUserTypesOp {
    PushUserTypes { base_types: UserTypeIndices },

    Index,
    Subslice { from: u64, to: u64 },
    Deref,
    Leaf { field: FieldIdx },
    Variant { name: Symbol, variant: VariantIdx, field: FieldIdx },
}

#[derive(Debug)]
pub(crate) enum ProjectedUserTypesNode<'a> {
    None,
    Chain { parent: &'a Self, op: ProjectedUserTypesOp },
}

impl<'a> ProjectedUserTypesNode<'a> {
    pub(crate) fn push_user_types(&'a self, base_types: UserTypeIndices) -> Self {
        assert!(!base_types.is_empty());
        // Pushing one or more base user types always causes the chain to become non-empty.
        Self::Chain { parent: self, op: ProjectedUserTypesOp::PushUserTypes { base_types } }
    }

    /// Push another projection op onto the chain, but only if it is already non-empty.
    fn maybe_push(&'a self, op_fn: impl FnOnce() -> ProjectedUserTypesOp) -> Self {
        match self {
            Self::None => Self::None,
            Self::Chain { .. } => Self::Chain { parent: self, op: op_fn() },
        }
    }

    pub(crate) fn index(&'a self) -> Self {
        self.maybe_push(|| ProjectedUserTypesOp::Index)
    }

    pub(crate) fn subslice(&'a self, from: u64, to: u64) -> Self {
        self.maybe_push(|| ProjectedUserTypesOp::Subslice { from, to })
    }

    pub(crate) fn deref(&'a self) -> Self {
        self.maybe_push(|| ProjectedUserTypesOp::Deref)
    }

    pub(crate) fn leaf(&'a self, field: FieldIdx) -> Self {
        self.maybe_push(|| ProjectedUserTypesOp::Leaf { field })
    }

    pub(crate) fn variant(
        &'a self,
        adt_def: AdtDef<'_>,
        variant: VariantIdx,
        field: FieldIdx,
    ) -> Self {
        self.maybe_push(|| {
            let name = adt_def.variant(variant).name;
            ProjectedUserTypesOp::Variant { name, variant, field }
        })
    }

    /// Traverses the chain of nodes to yield each op in the chain.
    /// Because this walks from child node to parent node, the ops are
    /// naturally yielded in "reverse" order.
    fn iter_ops_reversed(&'a self) -> impl Iterator<Item = &'a ProjectedUserTypesOp> {
        let mut next = self;
        iter::from_fn(move || match next {
            Self::None => None,
            Self::Chain { parent, op } => {
                next = parent;
                Some(op)
            }
        })
    }

    /// Assembles this chain of user-type projections into a proper data structure.
    pub(crate) fn build_user_type_projections(&self) -> Option<Box<UserTypeProjections>> {
        // If we know there's nothing to do, just return None immediately.
        if matches!(self, Self::None) {
            return None;
        }

        let ops_reversed = self.iter_ops_reversed().collect::<Vec<_>>();
        // The "first" op should always be `PushUserType`.
        // Other projections are only added if there is at least one user type.
        assert_matches!(ops_reversed.last(), Some(ProjectedUserTypesOp::PushUserTypes { .. }));

        let mut projections = vec![];
        for op in ops_reversed.into_iter().rev() {
            match *op {
                ProjectedUserTypesOp::PushUserTypes { ref base_types } => {
                    assert!(!base_types.is_empty());
                    for &base in base_types {
                        projections.push(UserTypeProjection { base, projs: vec![] })
                    }
                }

                ProjectedUserTypesOp::Index => {
                    for p in &mut projections {
                        p.projs.push(ProjectionElem::Index(()))
                    }
                }
                ProjectedUserTypesOp::Subslice { from, to } => {
                    for p in &mut projections {
                        p.projs.push(ProjectionElem::Subslice { from, to, from_end: true })
                    }
                }
                ProjectedUserTypesOp::Deref => {
                    for p in &mut projections {
                        p.projs.push(ProjectionElem::Deref)
                    }
                }
                ProjectedUserTypesOp::Leaf { field } => {
                    for p in &mut projections {
                        p.projs.push(ProjectionElem::Field(field, ()))
                    }
                }
                ProjectedUserTypesOp::Variant { name, variant, field } => {
                    for p in &mut projections {
                        p.projs.push(ProjectionElem::Downcast(Some(name), variant));
                        p.projs.push(ProjectionElem::Field(field, ()));
                    }
                }
            }
        }

        Some(Box::new(UserTypeProjections { contents: projections }))
    }
}
