use std::collections::HashMap;
use std::collections::HashSet;

use crate::passes::dependency_analysis::DependencyGraph;
use crate::types::cooked::Definition;
use heck::ToPascalCase;
use quote::quote;
use quote::ToTokens;
use quote::TokenStreamExt;

#[derive(Clone, Copy, Debug)]
pub struct Type<'a> {
    pub name: &'a str,

    pub needs_lifetime: bool,
}

impl ToTokens for Type<'_> {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        tokens.append_all(self.to_token_stream())
    }

    fn to_token_stream(&self) -> proc_macro2::TokenStream {
        let name = quote::format_ident!("{}", self.name.to_pascal_case());
        let lifetime = self.needs_lifetime.then(|| quote!(<'a>));

        quote!(#name #lifetime)
    }
}

pub fn resolve_types<'a>(
    definitions: &'a HashSet<Definition<'a>>,
    graph: &DependencyGraph<'a>,
    overrides: &HashMap<&'a str, Type<'a>>,
) -> HashMap<&'a str, Type<'a>> {
    let mut types: HashMap<&str, Type> = HashMap::new();

    for id in graph.bottom_up() {
        let ty = if let Some(ty) = overrides.get(id) {
            *ty
        } else {
            let definition = definitions.get(id).unwrap();

            let name = definition.name();
            let needs_lifetime = match definition {
                Definition::Amount(_)
                | Definition::CodeSet(_)
                | Definition::Date(_)
                | Definition::DateTime(_)
                | Definition::Indicator(_)
                | Definition::Month(_)
                | Definition::Quantity(_)
                | Definition::Rate(_)
                | Definition::Time(_)
                | Definition::Year(_)
                | Definition::YearMonth(_) => false,

                Definition::IdentifierSet(_) | Definition::Text(_) => true,

                other => other.dependencies().any(|dep| types[dep].needs_lifetime),
            };

            Type {
                name,
                needs_lifetime,
            }
        };

        let old = types.insert(id, ty);
        assert!(old.is_none());
    }

    types
}