use crate::syntax::set::OrderedSet as Set;
use crate::syntax::{Api, Enum, ExternFn, Pair, RustName, Struct, Type};
use proc_macro2::Ident;
use std::collections::BTreeMap as Map;
use std::fmt::{self, Display};

#[derive(Copy, Clone)]
pub enum TrivialReason<'a> {
    StructField(&'a Struct),
    FunctionArgument(&'a ExternFn),
    FunctionReturn(&'a ExternFn),
    BoxTarget,
    VecElement,
    UnpinnedMut(&'a ExternFn),
}

pub fn required_trivial_reasons<'a>(
    apis: &'a [Api],
    all: &Set<&'a Type>,
    structs: &Map<&'a Ident, &'a Struct>,
    enums: &Map<&'a Ident, &'a Enum>,
    cxx: &Set<&'a Ident>,
) -> Map<&'a Ident, Vec<TrivialReason<'a>>> {
    let mut required_trivial = Map::new();

    let mut insist_extern_types_are_trivial = |ident: &'a RustName, reason| {
        if cxx.contains(&ident.rust)
            && !structs.contains_key(&ident.rust)
            && !enums.contains_key(&ident.rust)
        {
            required_trivial
                .entry(&ident.rust)
                .or_insert_with(Vec::new)
                .push(reason);
        }
    };

    for api in apis {
        match api {
            Api::Struct(strct) => {
                for field in &strct.fields {
                    if let Type::Ident(ident) = &field.ty {
                        let reason = TrivialReason::StructField(strct);
                        insist_extern_types_are_trivial(ident, reason);
                    }
                }
            }
            Api::CxxFunction(efn) | Api::RustFunction(efn) => {
                if let Some(receiver) = &efn.receiver {
                    if receiver.mutable && !receiver.pinned {
                        let reason = TrivialReason::UnpinnedMut(efn);
                        insist_extern_types_are_trivial(&receiver.ty, reason);
                    }
                }
                for arg in &efn.args {
                    match &arg.ty {
                        Type::Ident(ident) => {
                            let reason = TrivialReason::FunctionArgument(efn);
                            insist_extern_types_are_trivial(ident, reason);
                        }
                        Type::Ref(ty) => {
                            if ty.mutable && !ty.pinned {
                                if let Type::Ident(ident) = &ty.inner {
                                    let reason = TrivialReason::UnpinnedMut(efn);
                                    insist_extern_types_are_trivial(ident, reason);
                                }
                            }
                        }
                        _ => {}
                    }
                }
                if let Some(ret) = &efn.ret {
                    match ret {
                        Type::Ident(ident) => {
                            let reason = TrivialReason::FunctionReturn(efn);
                            insist_extern_types_are_trivial(ident, reason);
                        }
                        Type::Ref(ty) => {
                            if ty.mutable && !ty.pinned {
                                if let Type::Ident(ident) = &ty.inner {
                                    let reason = TrivialReason::UnpinnedMut(efn);
                                    insist_extern_types_are_trivial(ident, reason);
                                }
                            }
                        }
                        _ => {}
                    }
                }
            }
            _ => {}
        }
    }

    for ty in all {
        match ty {
            Type::RustBox(ty) => {
                if let Type::Ident(ident) = &ty.inner {
                    let reason = TrivialReason::BoxTarget;
                    insist_extern_types_are_trivial(ident, reason);
                }
            }
            Type::RustVec(ty) => {
                if let Type::Ident(ident) = &ty.inner {
                    let reason = TrivialReason::VecElement;
                    insist_extern_types_are_trivial(ident, reason);
                }
            }
            _ => {}
        }
    }

    required_trivial
}

// Context:
// "type {type} should be trivially move constructible and trivially destructible in C++ to be used as {what} in Rust"
// "needs a cxx::ExternType impl in order to be used as {what}"
pub fn as_what<'a>(name: &'a Pair, reasons: &'a [TrivialReason]) -> impl Display + 'a {
    struct Description<'a> {
        name: &'a Pair,
        reasons: &'a [TrivialReason<'a>],
    }

    impl<'a> Display for Description<'a> {
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
            let mut field_of = Set::new();
            let mut argument_of = Set::new();
            let mut return_of = Set::new();
            let mut box_target = false;
            let mut vec_element = false;
            let mut unpinned_mut = Set::new();

            for reason in self.reasons {
                match reason {
                    TrivialReason::StructField(strct) => {
                        field_of.insert(&strct.name.rust);
                    }
                    TrivialReason::FunctionArgument(efn) => {
                        argument_of.insert(&efn.name.rust);
                    }
                    TrivialReason::FunctionReturn(efn) => {
                        return_of.insert(&efn.name.rust);
                    }
                    TrivialReason::BoxTarget => box_target = true,
                    TrivialReason::VecElement => vec_element = true,
                    TrivialReason::UnpinnedMut(efn) => {
                        unpinned_mut.insert(&efn.name.rust);
                    }
                }
            }

            let mut clauses = Vec::new();
            if !field_of.is_empty() {
                clauses.push(Clause::Set {
                    article: "a",
                    desc: "field of",
                    set: &field_of,
                });
            }
            if !argument_of.is_empty() {
                clauses.push(Clause::Set {
                    article: "an",
                    desc: "argument of",
                    set: &argument_of,
                });
            }
            if !return_of.is_empty() {
                clauses.push(Clause::Set {
                    article: "a",
                    desc: "return value of",
                    set: &return_of,
                });
            }
            if box_target {
                clauses.push(Clause::Ty1 {
                    article: "type",
                    desc: "Box",
                    param: self.name,
                });
            }
            if vec_element {
                clauses.push(Clause::Ty1 {
                    article: "a",
                    desc: "vector element in Vec",
                    param: self.name,
                });
            }
            if !unpinned_mut.is_empty() {
                clauses.push(Clause::Set {
                    article: "a",
                    desc: "non-pinned mutable reference in signature of",
                    set: &unpinned_mut,
                });
            }

            for (i, clause) in clauses.iter().enumerate() {
                if i == 0 {
                    write!(f, "{} ", clause.article())?;
                } else if i + 1 < clauses.len() {
                    write!(f, ", ")?;
                } else {
                    write!(f, " or ")?;
                }
                clause.fmt(f)?;
            }

            Ok(())
        }
    }

    enum Clause<'a> {
        Set {
            article: &'a str,
            desc: &'a str,
            set: &'a Set<&'a Ident>,
        },
        Ty1 {
            article: &'a str,
            desc: &'a str,
            param: &'a Pair,
        },
    }

    impl<'a> Clause<'a> {
        fn article(&self) -> &'a str {
            match self {
                Clause::Set { article, .. } | Clause::Ty1 { article, .. } => article,
            }
        }

        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
            match self {
                Clause::Set {
                    article: _,
                    desc,
                    set,
                } => {
                    write!(f, "{} ", desc)?;
                    for (i, ident) in set.iter().take(3).enumerate() {
                        if i > 0 {
                            write!(f, ", ")?;
                        }
                        write!(f, "`{}`", ident)?;
                    }
                    Ok(())
                }
                Clause::Ty1 {
                    article: _,
                    desc,
                    param,
                } => write!(f, "{}<{}>", desc, param.rust),
            }
        }
    }

    Description { name, reasons }
}
