//! Provides wrappers over `RustIrDatabase` which record used definitions and write
//! `.chalk` files containing those definitions.
use std::{
    borrow::Borrow,
    fmt::{self, Debug, Display},
    io::Write,
    marker::PhantomData,
    sync::Arc,
    sync::Mutex,
};

use crate::rust_ir::*;
use crate::{
    display::{self, WriterState},
    RustIrDatabase,
};
use chalk_ir::{interner::Interner, *};

use indexmap::IndexSet;

mod id_collector;

/// Wraps another `RustIrDatabase` (`DB`) and records which definitions are
/// used.
///
/// A full .chalk file containing all used definitions can be recovered through
/// `LoggingRustIrDatabase`'s `Display` implementation.
///
/// Uses a separate type, `P`, for the database stored inside to account for
/// `Arc` or wrapping other storage mediums.
#[derive(Debug)]
pub struct LoggingRustIrDatabase<I, DB, P = DB>
where
    DB: RustIrDatabase<I>,
    P: Borrow<DB>,
    I: Interner,
{
    ws: WriterState<I, DB, P>,
    def_ids: Mutex<IndexSet<RecordedItemId<I>>>,
    _phantom: PhantomData<DB>,
}

impl<I, DB, P> LoggingRustIrDatabase<I, DB, P>
where
    DB: RustIrDatabase<I>,
    P: Borrow<DB>,
    I: Interner,
{
    pub fn new(db: P) -> Self {
        LoggingRustIrDatabase {
            ws: WriterState::new(db),
            def_ids: Default::default(),
            _phantom: PhantomData,
        }
    }
}

impl<I, DB, P> Display for LoggingRustIrDatabase<I, DB, P>
where
    DB: RustIrDatabase<I>,
    P: Borrow<DB>,
    I: Interner,
{
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let def_ids = self.def_ids.lock().unwrap();
        let stub_ids = id_collector::collect_unrecorded_ids(self.ws.db(), &def_ids);
        display::write_stub_items(f, &self.ws, stub_ids)?;
        display::write_items(f, &self.ws, def_ids.iter().copied())
    }
}

impl<I, DB, P> LoggingRustIrDatabase<I, DB, P>
where
    DB: RustIrDatabase<I>,
    P: Borrow<DB>,
    I: Interner,
{
    fn record(&self, id: impl Into<RecordedItemId<I>>) {
        self.def_ids.lock().unwrap().insert(id.into());
    }

    fn record_all<T, U>(&self, ids: T)
    where
        T: IntoIterator<Item = U>,
        U: Into<RecordedItemId<I>>,
    {
        self.def_ids
            .lock()
            .unwrap()
            .extend(ids.into_iter().map(Into::into));
    }
}

impl<I, DB, P> UnificationDatabase<I> for LoggingRustIrDatabase<I, DB, P>
where
    DB: RustIrDatabase<I>,
    P: Borrow<DB> + Debug,
    I: Interner,
{
    fn fn_def_variance(&self, fn_def_id: chalk_ir::FnDefId<I>) -> Variances<I> {
        self.ws
            .db()
            .unification_database()
            .fn_def_variance(fn_def_id)
    }

    fn adt_variance(&self, adt_id: chalk_ir::AdtId<I>) -> Variances<I> {
        self.ws.db().unification_database().adt_variance(adt_id)
    }
}

impl<I, DB, P> RustIrDatabase<I> for LoggingRustIrDatabase<I, DB, P>
where
    DB: RustIrDatabase<I>,
    P: Borrow<DB> + Debug,
    I: Interner,
{
    fn custom_clauses(&self) -> Vec<chalk_ir::ProgramClause<I>> {
        self.ws.db().custom_clauses()
    }

    fn associated_ty_data(
        &self,
        ty: chalk_ir::AssocTypeId<I>,
    ) -> Arc<crate::rust_ir::AssociatedTyDatum<I>> {
        let ty_datum = self.ws.db().associated_ty_data(ty);
        self.record(ty_datum.trait_id);
        ty_datum
    }

    fn trait_datum(&self, trait_id: TraitId<I>) -> Arc<TraitDatum<I>> {
        self.record(trait_id);
        self.ws.db().trait_datum(trait_id)
    }

    fn adt_datum(&self, adt_id: AdtId<I>) -> Arc<AdtDatum<I>> {
        self.record(adt_id);
        self.ws.db().adt_datum(adt_id)
    }

    fn coroutine_datum(&self, coroutine_id: CoroutineId<I>) -> Arc<CoroutineDatum<I>> {
        self.record(coroutine_id);
        self.ws.db().borrow().coroutine_datum(coroutine_id)
    }

    fn coroutine_witness_datum(
        &self,
        coroutine_id: CoroutineId<I>,
    ) -> Arc<CoroutineWitnessDatum<I>> {
        self.record(coroutine_id);
        self.ws.db().borrow().coroutine_witness_datum(coroutine_id)
    }

    fn adt_repr(&self, id: AdtId<I>) -> Arc<AdtRepr<I>> {
        self.record(id);
        self.ws.db().adt_repr(id)
    }

    fn adt_size_align(&self, id: chalk_ir::AdtId<I>) -> Arc<crate::rust_ir::AdtSizeAlign> {
        self.record(id);
        self.ws.db().adt_size_align(id)
    }

    fn impl_datum(&self, impl_id: ImplId<I>) -> Arc<ImplDatum<I>> {
        self.record(impl_id);
        self.ws.db().impl_datum(impl_id)
    }

    fn hidden_opaque_type(&self, id: OpaqueTyId<I>) -> Ty<I> {
        self.record(id);
        self.ws.db().hidden_opaque_type(id)
    }

    fn associated_ty_value(
        &self,
        id: crate::rust_ir::AssociatedTyValueId<I>,
    ) -> Arc<crate::rust_ir::AssociatedTyValue<I>> {
        let value = self.ws.db().associated_ty_value(id);
        self.record(value.impl_id);
        value
    }

    fn opaque_ty_data(&self, id: OpaqueTyId<I>) -> Arc<OpaqueTyDatum<I>> {
        self.record(id);
        self.ws.db().opaque_ty_data(id)
    }

    fn impls_for_trait(
        &self,
        trait_id: TraitId<I>,
        parameters: &[chalk_ir::GenericArg<I>],
        binders: &CanonicalVarKinds<I>,
    ) -> Vec<ImplId<I>> {
        self.record(trait_id);
        let impl_ids = self.ws.db().impls_for_trait(trait_id, parameters, binders);
        self.record_all(impl_ids.iter().copied());
        impl_ids
    }

    fn local_impls_to_coherence_check(&self, trait_id: TraitId<I>) -> Vec<ImplId<I>> {
        self.record(trait_id);
        self.ws.db().local_impls_to_coherence_check(trait_id)
    }

    fn impl_provided_for(&self, auto_trait_id: TraitId<I>, ty: &TyKind<I>) -> bool {
        self.record(auto_trait_id);
        if let TyKind::Adt(adt_id, _) = ty {
            self.record(*adt_id);
        }
        self.ws.db().impl_provided_for(auto_trait_id, ty)
    }

    fn well_known_trait_id(
        &self,
        well_known_trait: crate::rust_ir::WellKnownTrait,
    ) -> Option<TraitId<I>> {
        let trait_id = self.ws.db().well_known_trait_id(well_known_trait);
        if let Some(id) = trait_id {
            self.record(id);
        }
        trait_id
    }

    fn program_clauses_for_env(
        &self,
        environment: &chalk_ir::Environment<I>,
    ) -> chalk_ir::ProgramClauses<I> {
        self.ws.db().program_clauses_for_env(environment)
    }

    fn interner(&self) -> I {
        self.ws.db().interner()
    }

    fn trait_name(&self, trait_id: TraitId<I>) -> String {
        self.ws.db().trait_name(trait_id)
    }

    fn adt_name(&self, adt_id: AdtId<I>) -> String {
        self.ws.db().adt_name(adt_id)
    }

    fn assoc_type_name(&self, assoc_ty_id: AssocTypeId<I>) -> String {
        self.ws.db().assoc_type_name(assoc_ty_id)
    }

    fn opaque_type_name(&self, opaque_ty_id: OpaqueTyId<I>) -> String {
        self.ws.db().opaque_type_name(opaque_ty_id)
    }

    fn is_object_safe(&self, trait_id: TraitId<I>) -> bool {
        self.record(trait_id);
        self.ws.db().is_object_safe(trait_id)
    }

    fn fn_def_datum(&self, fn_def_id: chalk_ir::FnDefId<I>) -> Arc<FnDefDatum<I>> {
        self.record(fn_def_id);
        self.ws.db().fn_def_datum(fn_def_id)
    }

    fn fn_def_name(&self, fn_def_id: FnDefId<I>) -> String {
        self.ws.db().fn_def_name(fn_def_id)
    }

    fn closure_kind(&self, closure_id: ClosureId<I>, substs: &Substitution<I>) -> ClosureKind {
        // TODO: record closure IDs
        self.ws.db().closure_kind(closure_id, substs)
    }

    fn closure_inputs_and_output(
        &self,
        closure_id: ClosureId<I>,
        substs: &Substitution<I>,
    ) -> Binders<FnDefInputsAndOutputDatum<I>> {
        // TODO: record closure IDs
        self.ws.db().closure_inputs_and_output(closure_id, substs)
    }

    fn closure_upvars(&self, closure_id: ClosureId<I>, substs: &Substitution<I>) -> Binders<Ty<I>> {
        // TODO: record closure IDs
        self.ws.db().closure_upvars(closure_id, substs)
    }

    fn closure_fn_substitution(
        &self,
        closure_id: ClosureId<I>,
        substs: &Substitution<I>,
    ) -> Substitution<I> {
        // TODO: record closure IDs
        self.ws.db().closure_fn_substitution(closure_id, substs)
    }

    fn discriminant_type(&self, ty: Ty<I>) -> Ty<I> {
        self.ws.db().discriminant_type(ty)
    }

    fn unification_database(&self) -> &dyn UnificationDatabase<I> {
        self
    }
}

/// Wraps a [`RustIrDatabase`], and, when dropped, writes out all used
/// definition to the given file.
///
/// Uses [`LoggingRustIrDatabase`] internally.
///
/// Uses a separate type, `P`, for the database stored inside to account for
/// `Arc` or wrapping other storage mediums.
pub struct WriteOnDropRustIrDatabase<I, W, DB, P = DB>
where
    I: Interner,
    W: Write,
    DB: RustIrDatabase<I>,
    P: Borrow<DB>,
{
    db: LoggingRustIrDatabase<I, DB, P>,
    write: W,
}

impl<I, W, DB, P> fmt::Debug for WriteOnDropRustIrDatabase<I, W, DB, P>
where
    I: Interner,
    W: Write,
    DB: RustIrDatabase<I>,
    P: Borrow<DB> + fmt::Debug,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("WriteOnDropRustIrDatabase")
            .field("db", &self.db)
            .field("write", &"<opaque>")
            .finish()
    }
}

impl<I, W, DB, P> WriteOnDropRustIrDatabase<I, W, DB, P>
where
    I: Interner,
    W: Write,
    DB: RustIrDatabase<I>,
    P: Borrow<DB>,
{
    pub fn new(db: P, write: W) -> Self {
        WriteOnDropRustIrDatabase {
            db: LoggingRustIrDatabase::new(db),
            write,
        }
    }

    pub fn from_logging_db(db: LoggingRustIrDatabase<I, DB, P>, write: W) -> Self {
        WriteOnDropRustIrDatabase { db, write }
    }
}

impl<I, W, DB, P> Drop for WriteOnDropRustIrDatabase<I, W, DB, P>
where
    I: Interner,
    W: Write,
    DB: RustIrDatabase<I>,
    P: Borrow<DB>,
{
    fn drop(&mut self) {
        write!(self.write, "{}", self.db)
            .and_then(|_| self.write.flush())
            .expect("expected to be able to write rust ir database");
    }
}

impl<I, W, DB, P> UnificationDatabase<I> for WriteOnDropRustIrDatabase<I, W, DB, P>
where
    I: Interner,
    W: Write,
    DB: RustIrDatabase<I>,
    P: Borrow<DB> + Debug,
{
    fn fn_def_variance(&self, fn_def_id: chalk_ir::FnDefId<I>) -> Variances<I> {
        self.db
            .borrow()
            .unification_database()
            .fn_def_variance(fn_def_id)
    }

    fn adt_variance(&self, adt_id: chalk_ir::AdtId<I>) -> Variances<I> {
        self.db.borrow().unification_database().adt_variance(adt_id)
    }
}

impl<I, W, DB, P> RustIrDatabase<I> for WriteOnDropRustIrDatabase<I, W, DB, P>
where
    I: Interner,
    W: Write,
    DB: RustIrDatabase<I>,
    P: Borrow<DB> + Debug,
{
    fn custom_clauses(&self) -> Vec<chalk_ir::ProgramClause<I>> {
        self.db.custom_clauses()
    }

    fn associated_ty_data(
        &self,
        ty: chalk_ir::AssocTypeId<I>,
    ) -> Arc<crate::rust_ir::AssociatedTyDatum<I>> {
        self.db.associated_ty_data(ty)
    }

    fn trait_datum(&self, trait_id: TraitId<I>) -> Arc<TraitDatum<I>> {
        self.db.trait_datum(trait_id)
    }

    fn adt_datum(&self, adt_id: AdtId<I>) -> Arc<AdtDatum<I>> {
        self.db.adt_datum(adt_id)
    }

    fn coroutine_datum(&self, coroutine_id: CoroutineId<I>) -> Arc<CoroutineDatum<I>> {
        self.db.borrow().coroutine_datum(coroutine_id)
    }

    /// Returns the coroutine witness datum for the coroutine with the given id.
    fn coroutine_witness_datum(
        &self,
        coroutine_id: CoroutineId<I>,
    ) -> Arc<CoroutineWitnessDatum<I>> {
        self.db.borrow().coroutine_witness_datum(coroutine_id)
    }

    fn adt_repr(&self, id: AdtId<I>) -> Arc<AdtRepr<I>> {
        self.db.adt_repr(id)
    }

    fn adt_size_align(&self, id: chalk_ir::AdtId<I>) -> Arc<crate::rust_ir::AdtSizeAlign> {
        self.db.adt_size_align(id)
    }

    fn impl_datum(&self, impl_id: ImplId<I>) -> Arc<ImplDatum<I>> {
        self.db.impl_datum(impl_id)
    }

    fn associated_ty_value(
        &self,
        id: crate::rust_ir::AssociatedTyValueId<I>,
    ) -> Arc<crate::rust_ir::AssociatedTyValue<I>> {
        self.db.associated_ty_value(id)
    }

    fn opaque_ty_data(&self, id: OpaqueTyId<I>) -> Arc<OpaqueTyDatum<I>> {
        self.db.opaque_ty_data(id)
    }

    fn hidden_opaque_type(&self, id: OpaqueTyId<I>) -> Ty<I> {
        self.db.hidden_opaque_type(id)
    }

    fn impls_for_trait(
        &self,
        trait_id: TraitId<I>,
        parameters: &[chalk_ir::GenericArg<I>],
        binders: &CanonicalVarKinds<I>,
    ) -> Vec<ImplId<I>> {
        self.db.impls_for_trait(trait_id, parameters, binders)
    }

    fn local_impls_to_coherence_check(&self, trait_id: TraitId<I>) -> Vec<ImplId<I>> {
        self.db.local_impls_to_coherence_check(trait_id)
    }

    fn impl_provided_for(&self, auto_trait_id: TraitId<I>, ty: &TyKind<I>) -> bool {
        self.db.impl_provided_for(auto_trait_id, ty)
    }

    fn well_known_trait_id(
        &self,
        well_known_trait: crate::rust_ir::WellKnownTrait,
    ) -> Option<TraitId<I>> {
        self.db.well_known_trait_id(well_known_trait)
    }

    fn program_clauses_for_env(
        &self,
        environment: &chalk_ir::Environment<I>,
    ) -> chalk_ir::ProgramClauses<I> {
        self.db.program_clauses_for_env(environment)
    }

    fn interner(&self) -> I {
        self.db.interner()
    }

    fn is_object_safe(&self, trait_id: TraitId<I>) -> bool {
        self.db.is_object_safe(trait_id)
    }

    fn unification_database(&self) -> &dyn UnificationDatabase<I> {
        self
    }

    fn trait_name(&self, trait_id: TraitId<I>) -> String {
        self.db.trait_name(trait_id)
    }

    fn adt_name(&self, adt_id: AdtId<I>) -> String {
        self.db.adt_name(adt_id)
    }

    fn assoc_type_name(&self, assoc_ty_id: AssocTypeId<I>) -> String {
        self.db.assoc_type_name(assoc_ty_id)
    }

    fn opaque_type_name(&self, opaque_ty_id: OpaqueTyId<I>) -> String {
        self.db.opaque_type_name(opaque_ty_id)
    }

    fn fn_def_datum(&self, fn_def_id: chalk_ir::FnDefId<I>) -> Arc<FnDefDatum<I>> {
        self.db.fn_def_datum(fn_def_id)
    }

    fn fn_def_name(&self, fn_def_id: FnDefId<I>) -> String {
        self.db.fn_def_name(fn_def_id)
    }

    fn closure_kind(&self, closure_id: ClosureId<I>, substs: &Substitution<I>) -> ClosureKind {
        // TODO: record closure IDs
        self.db.closure_kind(closure_id, substs)
    }

    fn closure_inputs_and_output(
        &self,
        closure_id: ClosureId<I>,
        substs: &Substitution<I>,
    ) -> Binders<FnDefInputsAndOutputDatum<I>> {
        self.db.closure_inputs_and_output(closure_id, substs)
    }

    fn closure_upvars(&self, closure_id: ClosureId<I>, substs: &Substitution<I>) -> Binders<Ty<I>> {
        self.db.closure_upvars(closure_id, substs)
    }

    fn closure_fn_substitution(
        &self,
        closure_id: ClosureId<I>,
        substs: &Substitution<I>,
    ) -> Substitution<I> {
        self.db.closure_fn_substitution(closure_id, substs)
    }

    fn discriminant_type(&self, ty: Ty<I>) -> Ty<I> {
        self.db.discriminant_type(ty)
    }
}

#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
pub enum RecordedItemId<I: Interner> {
    Adt(AdtId<I>),
    Trait(TraitId<I>),
    Impl(ImplId<I>),
    OpaqueTy(OpaqueTyId<I>),
    FnDef(FnDefId<I>),
    Coroutine(CoroutineId<I>),
}

impl<I: Interner> From<AdtId<I>> for RecordedItemId<I> {
    fn from(v: AdtId<I>) -> Self {
        RecordedItemId::Adt(v)
    }
}

impl<I: Interner> From<TraitId<I>> for RecordedItemId<I> {
    fn from(v: TraitId<I>) -> Self {
        RecordedItemId::Trait(v)
    }
}

impl<I: Interner> From<ImplId<I>> for RecordedItemId<I> {
    fn from(v: ImplId<I>) -> Self {
        RecordedItemId::Impl(v)
    }
}

impl<I: Interner> From<OpaqueTyId<I>> for RecordedItemId<I> {
    fn from(v: OpaqueTyId<I>) -> Self {
        RecordedItemId::OpaqueTy(v)
    }
}

impl<I: Interner> From<FnDefId<I>> for RecordedItemId<I> {
    fn from(v: FnDefId<I>) -> Self {
        RecordedItemId::FnDef(v)
    }
}

impl<I: Interner> From<CoroutineId<I>> for RecordedItemId<I> {
    fn from(v: CoroutineId<I>) -> Self {
        RecordedItemId::Coroutine(v)
    }
}
