use crate::{ctor_generators::*, mock_generator::*, service_generators::*, type_generators::*};
use convert_case::{Case, Casing};
use genco::prelude::*;
use rust::Tokens;
use sails_idl_parser::{ast::visitor::Visitor, ast::*};
use std::collections::HashMap;

pub(crate) struct RootGenerator<'a> {
    tokens: Tokens,
    mocks_tokens: Tokens,
    service_impl_tokens: Tokens,
    service_trait_tokens: Tokens,
    anonymous_service_name: &'a str,
    mocks_feature_name: Option<&'a str>,
    sails_path: &'a str,
    external_types: HashMap<&'a str, &'a str>,
    no_derive_traits: bool,
}

impl<'a> RootGenerator<'a> {
    pub(crate) fn new(
        anonymous_service_name: &'a str,
        mocks_feature_name: Option<&'a str>,
        sails_path: &'a str,
        external_types: HashMap<&'a str, &'a str>,
        no_derive_traits: bool,
    ) -> Self {
        Self {
            anonymous_service_name,
            tokens: Tokens::new(),
            mocks_tokens: Tokens::new(),
            service_impl_tokens: Tokens::new(),
            service_trait_tokens: Tokens::new(),
            mocks_feature_name,
            sails_path,
            external_types,
            no_derive_traits,
        }
    }

    pub(crate) fn finalize(self, with_no_std: bool) -> String {
        let mocks_tokens = if let Some(mocks_feature_name) = self.mocks_feature_name {
            quote! {
                $['\n']
                #[cfg(feature = $(quoted(mocks_feature_name)))]
                #[cfg(not(target_arch = "wasm32"))]
                extern crate std;
                $['\n']
                #[cfg(feature = $(quoted(mocks_feature_name)))]
                #[cfg(not(target_arch = "wasm32"))]
                pub mod mockall {
                    use super::*;
                    use $(self.sails_path)::mockall::*;
                    $(self.mocks_tokens)
                }
            }
        } else {
            Tokens::new()
        };

        let mut tokens = quote! {
            #[allow(unused_imports)]
            use $(self.sails_path)::{client::*, collections::*, prelude::*};
        };

        for (&name, &path) in &self.external_types {
            quote_in! { tokens =>
                #[allow(unused_imports)]
                use $path as $name;
            };
        }

        let program_name = &self.anonymous_service_name.to_case(Case::Pascal);
        quote_in! { tokens =>
            pub struct $(program_name)Program;

            impl $(self.sails_path)::client::Program for  $(program_name)Program {}

            pub trait $program_name {
                type Env: $(self.sails_path)::client::GearEnv;
                $(self.service_trait_tokens)
            }

            impl<E: $(self.sails_path)::client::GearEnv> $program_name for $(self.sails_path)::client::Actor<$(program_name)Program, E> {
                type Env = E;
                $(self.service_impl_tokens)
            }

            $(self.tokens)

            $mocks_tokens
        };

        let mut result = tokens.to_file_string().unwrap();

        if with_no_std {
            result.insert_str(
                0,
                "// Code generated by sails-client-gen. DO NOT EDIT.\n#![no_std]\n\n",
            );
        } else {
            result.insert_str(0, "// Code generated by sails-client-gen. DO NOT EDIT.\n");
        }

        result
    }
}

impl<'ast> Visitor<'ast> for RootGenerator<'_> {
    fn visit_ctor(&mut self, ctor: &'ast Ctor) {
        let mut ctor_gen = CtorGenerator::new(self.anonymous_service_name, self.sails_path);
        ctor_gen.visit_ctor(ctor);
        self.tokens.extend(ctor_gen.finalize());
    }

    fn visit_service(&mut self, service: &'ast Service) {
        let service_name = if service.name().is_empty() {
            self.anonymous_service_name
        } else {
            service.name()
        };

        let mut ctor_gen = ServiceCtorGenerator::new(service_name, self.sails_path);
        ctor_gen.visit_service(service);
        let (trait_tokens, impl_tokens) = ctor_gen.finalize();
        self.service_trait_tokens.extend(trait_tokens);
        self.service_impl_tokens.extend(impl_tokens);

        let mut client_gen = ServiceGenerator::new(service_name, self.sails_path);
        client_gen.visit_service(service);
        self.tokens.extend(client_gen.finalize());

        let mut mock_gen = MockGenerator::new(service_name, self.sails_path);
        mock_gen.visit_service(service);
        self.mocks_tokens.extend(mock_gen.finalize());
    }

    fn visit_type(&mut self, t: &'ast Type) {
        if self.external_types.contains_key(t.name()) {
            return;
        }
        let mut type_gen =
            TopLevelTypeGenerator::new(t.name(), self.sails_path, self.no_derive_traits);
        type_gen.visit_type(t);
        self.tokens.extend(type_gen.finalize());
    }
}
