use super::lifetimes::{Lifetime, Lifetimes, MaybeStatic};
use super::{
    Borrow, LinkedLifetimes, MaybeOwn, Mutability, OutStructId, ReturnableStructPath, StructId,
    StructPath, TypeContext, TypeId,
};
use core::fmt::Debug;

/// Abstraction over where a type can appear in a function signature.
///
/// # "Output only" and "everywhere" types
///
/// While Rust is able to give up ownership of values, languages that Diplomat
/// supports (C++, Javascript, etc.) generally cannot. For example, we can
/// construct a `Box<MyOpaque>` in a Rust function and _return_ it to the other
/// language as a pointer. However, we cannot _accept_ `Box<MyOpaque>` as an input
/// because there's nothing stopping other languages from using that value again.
/// Therefore, we classify boxed opaques as "output only" types, since they can
/// only be returned from Rust but not taken as inputs.
///
/// Furthermore, Diplomat also supports "bag o' stuff" structs where all fields get
/// translated at the boundary. If one contains an "output only" type as a field,
/// then the whole struct must also be "output only". In particular, this means
/// that if a boxed opaque is nested in a bunch of "bag o' stuff" structs, than
/// all of those structs must also be "output only".
///
/// Currently, there are only two classes of structs: those that are "output only",
/// and those that are not. These are represented by the types [`OutputOnly`]
/// and [`Everywhere`] marker types respectively, which are the _only_ two types
/// that implement [`TyPosition`].
///
/// # How does abstraction help?
///
/// The HIR was designed around the idea of making invalid states unrepresentable.
/// Since "output only" types can contain values that "everywhere" types cannot,
/// it doesn't make sense for them to be represented in the same type, even if
/// they're mostly the same. One of these differences is that opaques (which are
/// always behind a pointer) can only be represented as a borrow in "everywhere"
/// types, but can additionally be represented as owned in "output only" types.
/// If we were to use the same type for both, then backends working with "everywhere"
/// types would constantly have unreachable statements for owned opaque cases.
///
/// That being said, "output only" and "everywhere" types are still mostly the
/// same, so this trait allows us to describe the differences. For example, the
/// HIR uses a singular [`Type`](super::Type) type for representing both
/// "output only" types and "everywhere" types, since it takes advantage of this
/// traits associated types to "fill in" the different parts:
/// ```ignore
/// pub enum Type<P: TyPosition = Everywhere> {
///     Primitive(PrimitiveType),
///     Opaque(OpaquePath<Optional, P::OpaqueOwnership>),
///     Struct(P::StructPath),
///     Enum(EnumPath),
///     Slice(Slice),
/// }
/// ```
///
/// When `P` takes on [`Everywhere`], this signature becomes:
/// ```ignore
/// pub enum Type {
///     Primitive(PrimitiveType),
///     Opaque(OpaquePath<Optional, Borrow>),
///     Struct(StructPath),
///     Enum(EnumPath),
///     Slice(Slice),
/// }
/// ```
///
/// This allows us to represent any kind of type that can appear "everywhere"
/// i.e. in inputs or outputs. Notice how the second generic in the `Opaque`
/// variant becomes [`Borrow`]. This describes the semantics of the pointer that
/// the opaque lives behind, and shows that for "everywhere" types, opaques
/// can _only_ be represented as living behind a borrow.
///
/// Contrast this to when `P` takes on [`OutputOnly`]:
/// ```ignore
/// pub enum Type {
///     Primitive(PrimitiveType),
///     Opaque(OpaquePath<Optional, MaybeOwn>),
///     Struct(OutStructPath),
///     Enum(EnumPath),
///     Slice(Slice),
/// }
/// ```
/// Here, the second generic of the `Opaque` variant becomes [`MaybeOwn`], meaning
/// that "output only" types can contain opaques that are either borrowed _or_ owned.
///
/// Therefore, this trait allows be extremely precise about making invalid states
/// unrepresentable, while also reducing duplicated code.
///
pub trait TyPosition: Debug + Copy {
    const IS_OUT_ONLY: bool;

    /// Type representing how we can point to opaques, which must always be behind a pointer.
    ///
    /// The types represented by [`OutputOnly`] are capable of either owning or
    /// borrowing opaques, and so the associated type for that impl is [`MaybeOwn`].
    ///
    /// On the other hand, types represented by [`Everywhere`] can only contain
    /// borrowes, so the associated type for that impl is [`Borrow`].
    type OpaqueOwnership: Debug + OpaqueOwner;

    type StructId: Debug;

    type StructPath: Debug + StructPathLike;
}

/// One of two types implementing [`TyPosition`], representing types that can be
/// used as both input and output to functions.
///
/// The complement of this type is [`OutputOnly`].
#[derive(Debug, Copy, Clone)]
#[non_exhaustive]
pub struct Everywhere;

/// One of two types implementing [`TyPosition`], representing types that can
/// only be used as return types in functions.
///
/// The complement of this type is [`Everywhere`].
#[derive(Debug, Copy, Clone)]
#[non_exhaustive]
pub struct OutputOnly;

impl TyPosition for Everywhere {
    const IS_OUT_ONLY: bool = false;
    type OpaqueOwnership = Borrow;
    type StructId = StructId;
    type StructPath = StructPath;
}

impl TyPosition for OutputOnly {
    const IS_OUT_ONLY: bool = true;
    type OpaqueOwnership = MaybeOwn;
    type StructId = OutStructId;
    type StructPath = ReturnableStructPath;
}

pub trait StructPathLike {
    fn lifetimes(&self) -> &Lifetimes;
    fn id(&self) -> TypeId;

    /// Get a map of lifetimes used on this path to lifetimes as named in the def site. See [`LinkedLifetimes`]
    /// for more information.
    fn link_lifetimes<'def, 'tcx>(
        &'def self,
        tcx: &'tcx TypeContext,
    ) -> LinkedLifetimes<'def, 'tcx>;
}

impl StructPathLike for StructPath {
    fn lifetimes(&self) -> &Lifetimes {
        &self.lifetimes
    }
    fn id(&self) -> TypeId {
        self.tcx_id.into()
    }

    fn link_lifetimes<'def, 'tcx>(
        &'def self,
        tcx: &'tcx TypeContext,
    ) -> LinkedLifetimes<'def, 'tcx> {
        let struc = self.resolve(tcx);
        let env = &struc.lifetimes;
        LinkedLifetimes::new(env, None, &self.lifetimes)
    }
}

impl StructPathLike for ReturnableStructPath {
    fn lifetimes(&self) -> &Lifetimes {
        self.lifetimes()
    }
    fn id(&self) -> TypeId {
        match self {
            ReturnableStructPath::Struct(p) => p.tcx_id.into(),
            ReturnableStructPath::OutStruct(p) => p.tcx_id.into(),
        }
    }

    fn link_lifetimes<'def, 'tcx>(
        &'def self,
        tcx: &'tcx TypeContext,
    ) -> LinkedLifetimes<'def, 'tcx> {
        match self {
            Self::Struct(p) => p.link_lifetimes(tcx),
            Self::OutStruct(p) => p.link_lifetimes(tcx),
        }
    }
}
/// Abstraction over how a type can hold a pointer to an opaque.
///
/// This trait is designed as a helper abstraction for the `OpaqueOwnership`
/// associated type in the [`TyPosition`] trait. As such, only has two implementing
/// types: [`MaybeOwn`] and [`Borrow`] for the [`OutputOnly`] and [`Everywhere`]
/// implementations of [`TyPosition`] respectively.
pub trait OpaqueOwner {
    /// Return the mutability of this owner
    fn mutability(&self) -> Option<Mutability>;

    fn is_owned(&self) -> bool;

    /// Return the lifetime of the borrow, if any.
    fn lifetime(&self) -> Option<MaybeStatic<Lifetime>>;
}

impl OpaqueOwner for MaybeOwn {
    fn mutability(&self) -> Option<Mutability> {
        match self {
            MaybeOwn::Own => None,
            MaybeOwn::Borrow(b) => b.mutability(),
        }
    }

    fn is_owned(&self) -> bool {
        match self {
            MaybeOwn::Own => true,
            MaybeOwn::Borrow(_) => false,
        }
    }

    fn lifetime(&self) -> Option<MaybeStatic<Lifetime>> {
        match self {
            MaybeOwn::Own => None,
            MaybeOwn::Borrow(b) => b.lifetime(),
        }
    }
}

impl OpaqueOwner for Borrow {
    fn mutability(&self) -> Option<Mutability> {
        Some(self.mutability)
    }

    fn is_owned(&self) -> bool {
        false
    }

    fn lifetime(&self) -> Option<MaybeStatic<Lifetime>> {
        Some(self.lifetime)
    }
}
