//! Determining the sizedness of types (as base classes and otherwise).

use super::{
    generate_dependencies, ConstrainResult, HasVtable, MonotoneFramework,
};
use crate::ir::context::{BindgenContext, TypeId};
use crate::ir::item::IsOpaque;
use crate::ir::traversal::EdgeKind;
use crate::ir::ty::TypeKind;
use crate::{Entry, HashMap};
use std::{cmp, ops};

/// The result of the `Sizedness` analysis for an individual item.
///
/// This is a chain lattice of the form:
///
/// ```ignore
///                   NonZeroSized
///                        |
///                DependsOnTypeParam
///                        |
///                     ZeroSized
/// ```
///
/// We initially assume that all types are `ZeroSized` and then update our
/// understanding as we learn more about each type.
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum SizednessResult {
    /// The type is zero-sized.
    ///
    /// This means that if it is a C++ type, and is not being used as a base
    /// member, then we must add an `_address` byte to enforce the
    /// unique-address-per-distinct-object-instance rule.
    ZeroSized,

    /// Whether this type is zero-sized or not depends on whether a type
    /// parameter is zero-sized or not.
    ///
    /// For example, given these definitions:
    ///
    /// ```c++
    /// template<class T>
    /// class Flongo : public T {};
    ///
    /// class Empty {};
    ///
    /// class NonEmpty { int x; };
    /// ```
    ///
    /// Then `Flongo<Empty>` is zero-sized, and needs an `_address` byte
    /// inserted, while `Flongo<NonEmpty>` is *not* zero-sized, and should *not*
    /// have an `_address` byte inserted.
    ///
    /// We don't properly handle this situation correctly right now:
    /// <https://github.com/rust-lang/rust-bindgen/issues/586>
    DependsOnTypeParam,

    /// Has some size that is known to be greater than zero. That doesn't mean
    /// it has a static size, but it is not zero sized for sure. In other words,
    /// it might contain an incomplete array or some other dynamically sized
    /// type.
    NonZeroSized,
}

impl Default for SizednessResult {
    fn default() -> Self {
        SizednessResult::ZeroSized
    }
}

impl SizednessResult {
    /// Take the least upper bound of `self` and `rhs`.
    pub(crate) fn join(self, rhs: Self) -> Self {
        cmp::max(self, rhs)
    }
}

impl ops::BitOr for SizednessResult {
    type Output = Self;

    fn bitor(self, rhs: SizednessResult) -> Self::Output {
        self.join(rhs)
    }
}

impl ops::BitOrAssign for SizednessResult {
    fn bitor_assign(&mut self, rhs: SizednessResult) {
        *self = self.join(rhs)
    }
}

/// An analysis that computes the sizedness of all types.
///
/// * For types with known sizes -- for example pointers, scalars, etc... --
/// they are assigned `NonZeroSized`.
///
/// * For compound structure types with one or more fields, they are assigned
/// `NonZeroSized`.
///
/// * For compound structure types without any fields, the results of the bases
/// are `join`ed.
///
/// * For type parameters, `DependsOnTypeParam` is assigned.
#[derive(Debug)]
pub(crate) struct SizednessAnalysis<'ctx> {
    ctx: &'ctx BindgenContext,
    dependencies: HashMap<TypeId, Vec<TypeId>>,
    // Incremental results of the analysis. Missing entries are implicitly
    // considered `ZeroSized`.
    sized: HashMap<TypeId, SizednessResult>,
}

impl<'ctx> SizednessAnalysis<'ctx> {
    fn consider_edge(kind: EdgeKind) -> bool {
        // These are the only edges that can affect whether a type is
        // zero-sized or not.
        matches!(
            kind,
            EdgeKind::TemplateArgument |
                EdgeKind::TemplateParameterDefinition |
                EdgeKind::TemplateDeclaration |
                EdgeKind::TypeReference |
                EdgeKind::BaseMember |
                EdgeKind::Field
        )
    }

    /// Insert an incremental result, and return whether this updated our
    /// knowledge of types and we should continue the analysis.
    fn insert(
        &mut self,
        id: TypeId,
        result: SizednessResult,
    ) -> ConstrainResult {
        trace!("inserting {:?} for {:?}", result, id);

        if let SizednessResult::ZeroSized = result {
            return ConstrainResult::Same;
        }

        match self.sized.entry(id) {
            Entry::Occupied(mut entry) => {
                if *entry.get() < result {
                    entry.insert(result);
                    ConstrainResult::Changed
                } else {
                    ConstrainResult::Same
                }
            }
            Entry::Vacant(entry) => {
                entry.insert(result);
                ConstrainResult::Changed
            }
        }
    }

    fn forward(&mut self, from: TypeId, to: TypeId) -> ConstrainResult {
        match self.sized.get(&from).cloned() {
            None => ConstrainResult::Same,
            Some(r) => self.insert(to, r),
        }
    }
}

impl<'ctx> MonotoneFramework for SizednessAnalysis<'ctx> {
    type Node = TypeId;
    type Extra = &'ctx BindgenContext;
    type Output = HashMap<TypeId, SizednessResult>;

    fn new(ctx: &'ctx BindgenContext) -> SizednessAnalysis<'ctx> {
        let dependencies = generate_dependencies(ctx, Self::consider_edge)
            .into_iter()
            .filter_map(|(id, sub_ids)| {
                id.as_type_id(ctx).map(|id| {
                    (
                        id,
                        sub_ids
                            .into_iter()
                            .filter_map(|s| s.as_type_id(ctx))
                            .collect::<Vec<_>>(),
                    )
                })
            })
            .collect();

        let sized = HashMap::default();

        SizednessAnalysis {
            ctx,
            dependencies,
            sized,
        }
    }

    fn initial_worklist(&self) -> Vec<TypeId> {
        self.ctx
            .allowlisted_items()
            .iter()
            .cloned()
            .filter_map(|id| id.as_type_id(self.ctx))
            .collect()
    }

    fn constrain(&mut self, id: TypeId) -> ConstrainResult {
        trace!("constrain {:?}", id);

        if let Some(SizednessResult::NonZeroSized) =
            self.sized.get(&id).cloned()
        {
            trace!("    already know it is not zero-sized");
            return ConstrainResult::Same;
        }

        if id.has_vtable_ptr(self.ctx) {
            trace!("    has an explicit vtable pointer, therefore is not zero-sized");
            return self.insert(id, SizednessResult::NonZeroSized);
        }

        let ty = self.ctx.resolve_type(id);

        if id.is_opaque(self.ctx, &()) {
            trace!("    type is opaque; checking layout...");
            let result =
                ty.layout(self.ctx).map_or(SizednessResult::ZeroSized, |l| {
                    if l.size == 0 {
                        trace!("    ...layout has size == 0");
                        SizednessResult::ZeroSized
                    } else {
                        trace!("    ...layout has size > 0");
                        SizednessResult::NonZeroSized
                    }
                });
            return self.insert(id, result);
        }

        match *ty.kind() {
            TypeKind::Void => {
                trace!("    void is zero-sized");
                self.insert(id, SizednessResult::ZeroSized)
            }

            TypeKind::TypeParam => {
                trace!(
                    "    type params sizedness depends on what they're \
                     instantiated as"
                );
                self.insert(id, SizednessResult::DependsOnTypeParam)
            }

            TypeKind::Int(..) |
            TypeKind::Float(..) |
            TypeKind::Complex(..) |
            TypeKind::Function(..) |
            TypeKind::Enum(..) |
            TypeKind::Reference(..) |
            TypeKind::NullPtr |
            TypeKind::ObjCId |
            TypeKind::ObjCSel |
            TypeKind::Pointer(..) => {
                trace!("    {:?} is known not to be zero-sized", ty.kind());
                self.insert(id, SizednessResult::NonZeroSized)
            }

            TypeKind::ObjCInterface(..) => {
                trace!("    obj-c interfaces always have at least the `isa` pointer");
                self.insert(id, SizednessResult::NonZeroSized)
            }

            TypeKind::TemplateAlias(t, _) |
            TypeKind::Alias(t) |
            TypeKind::BlockPointer(t) |
            TypeKind::ResolvedTypeRef(t) => {
                trace!("    aliases and type refs forward to their inner type");
                self.forward(t, id)
            }

            TypeKind::TemplateInstantiation(ref inst) => {
                trace!(
                    "    template instantiations are zero-sized if their \
                     definition is zero-sized"
                );
                self.forward(inst.template_definition(), id)
            }

            TypeKind::Array(_, 0) => {
                trace!("    arrays of zero elements are zero-sized");
                self.insert(id, SizednessResult::ZeroSized)
            }
            TypeKind::Array(..) => {
                trace!("    arrays of > 0 elements are not zero-sized");
                self.insert(id, SizednessResult::NonZeroSized)
            }
            TypeKind::Vector(..) => {
                trace!("    vectors are not zero-sized");
                self.insert(id, SizednessResult::NonZeroSized)
            }

            TypeKind::Comp(ref info) => {
                trace!("    comp considers its own fields and bases");

                if !info.fields().is_empty() {
                    return self.insert(id, SizednessResult::NonZeroSized);
                }

                let result = info
                    .base_members()
                    .iter()
                    .filter_map(|base| self.sized.get(&base.ty))
                    .fold(SizednessResult::ZeroSized, |a, b| a.join(*b));

                self.insert(id, result)
            }

            TypeKind::Opaque => {
                unreachable!("covered by the .is_opaque() check above")
            }

            TypeKind::UnresolvedTypeRef(..) => {
                unreachable!("Should have been resolved after parsing!");
            }
        }
    }

    fn each_depending_on<F>(&self, id: TypeId, mut f: F)
    where
        F: FnMut(TypeId),
    {
        if let Some(edges) = self.dependencies.get(&id) {
            for ty in edges {
                trace!("enqueue {:?} into worklist", ty);
                f(*ty);
            }
        }
    }
}

impl<'ctx> From<SizednessAnalysis<'ctx>> for HashMap<TypeId, SizednessResult> {
    fn from(analysis: SizednessAnalysis<'ctx>) -> Self {
        // We let the lack of an entry mean "ZeroSized" to save space.
        extra_assert!(analysis
            .sized
            .values()
            .all(|v| { *v != SizednessResult::ZeroSized }));

        analysis.sized
    }
}

/// A convenience trait for querying whether some type or ID is sized.
///
/// This is not for _computing_ whether the thing is sized, it is for looking up
/// the results of the `Sizedness` analysis's computations for a specific thing.
pub(crate) trait Sizedness {
    /// Get the sizedness of this type.
    fn sizedness(&self, ctx: &BindgenContext) -> SizednessResult;

    /// Is the sizedness for this type `SizednessResult::ZeroSized`?
    fn is_zero_sized(&self, ctx: &BindgenContext) -> bool {
        self.sizedness(ctx) == SizednessResult::ZeroSized
    }
}
