use crate::generator::module_matcher::ModuleMatcher;
use crate::generator::RenderMethod;
use crate::renderer::{quote_all_data, to_module_path, to_rust_identifier, RenderContext, RenderFilterMode};
use daml_lf::element::{DamlArchive, DamlModule, DamlPackage};
use itertools::Itertools;
use std::fs::File;
use std::io::{Error, Write};
use std::path::Path;
use std::path::PathBuf;
use std::{fs, io};

const DISABLE_WARNINGS: &str = "#![allow(clippy::all, warnings)]";
const USE_DAML_PRELUDE: &str = "use ::daml::prelude::*;";

pub fn generate_archive_separate(
    archive: &DamlArchive<'_>,
    output_path: &Path,
    module_matcher: &ModuleMatcher,
    render_method: &RenderMethod,
) -> Result<(), Error> {
    let ctx = RenderContext::with_archive(archive, RenderFilterMode::default());
    for package in archive.packages() {
        generate_package_source(&ctx, package, output_path, module_matcher, render_method)?;
    }
    Ok(())
}

fn generate_package_source(
    ctx: &RenderContext<'_>,
    package: &DamlPackage<'_>,
    output_path: &Path,
    module_matcher: &ModuleMatcher,
    render_method: &RenderMethod,
) -> Result<(), Error> {
    let root_modules: Vec<_> =
        package.root_module().child_modules().filter(|&m| is_interesting_module(m, module_matcher)).collect();
    let module_decl = root_modules.iter().map(|m| make_pub_mod_declaration(m.local_name())).join("\n");
    let package_body = format!("{}\n{}", DISABLE_WARNINGS, module_decl);
    let mut package_file = create_file(&PathBuf::from(output_path), &make_package_filename(package.name()))?;
    package_file.write_all(package_body.as_bytes())?;
    let package_dir_path = PathBuf::from(output_path).join(&to_rust_identifier(package.name()));
    for module in root_modules {
        generate_module_source(ctx, module, &package_dir_path, module_matcher, render_method)?;
    }
    Ok(())
}

fn generate_module_source(
    ctx: &RenderContext<'_>,
    module: &DamlModule<'_>,
    package_dir_path: &Path,
    module_matcher: &ModuleMatcher,
    render_method: &RenderMethod,
) -> Result<(), Error> {
    let sub_modules: Vec<_> = module.child_modules().filter(|&m| is_interesting_module(m, module_matcher)).collect();
    let sub_module_decl: String = sub_modules.iter().map(|&m| make_pub_mod_declaration(m.local_name())).join("\n");
    let module_types_text = quote_module_data_types(ctx, module, render_method);
    let module_body = format!("{}\n{}{}", USE_DAML_PRELUDE, sub_module_decl, module_types_text);
    let module_path: Vec<_> = module.path().collect();
    let module_dir_path = module_path[..module_path.len() - 1].iter().map(to_rust_identifier).join("/");
    let package_module_dir_path = PathBuf::from(package_dir_path).join(module_dir_path);
    let mut module_file = create_file(&package_module_dir_path, &make_module_filename(module.local_name()))?;
    module_file.write_all(module_body.as_bytes())?;
    for child_module in sub_modules {
        generate_module_source(ctx, child_module, package_dir_path, module_matcher, render_method)?;
    }
    Ok(())
}

/// A module is interesting (and should therefore be rendered) if its path is matched by the [`ModuleMatcher`] and if it
/// contains any data types.  Additionally, a module is considered interesting if any of decedent modules are
/// interesting.
fn is_interesting_module(module: &DamlModule<'_>, module_matcher: &ModuleMatcher) -> bool {
    (module.data_types().next().is_some() && module_matcher.matches(&to_module_path(module.path())))
        || module.child_modules().any(|m| is_interesting_module(m, module_matcher))
}

fn quote_module_data_types(ctx: &RenderContext<'_>, module: &DamlModule<'_>, render_method: &RenderMethod) -> String {
    quote_all_data(ctx, module.data_types().collect::<Vec<_>>().as_slice(), render_method).to_string()
}

fn create_file(base_dir: &Path, filename: &str) -> io::Result<File> {
    fs::create_dir_all(&base_dir)?;
    File::create(base_dir.join(filename))
}

fn make_package_filename(name: &str) -> String {
    format!("{}.rs", to_rust_identifier(name))
}

fn make_module_filename(name: &str) -> String {
    format!("{}.rs", to_rust_identifier(name))
}

fn make_pub_mod_declaration(module_name: &str) -> String {
    format!("pub mod {};", to_rust_identifier(module_name))
}
