//! Reading and writing Cargo.toml and package.json manifests.

use std::collections::HashMap;
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;

use console::style;
use emoji;
use error::Error;
use progressbar::Step;
use serde_json;
use toml;
use PBAR;

#[derive(Debug, Deserialize)]
struct CargoManifest {
    package: CargoPackage,
    dependencies: Option<HashMap<String, CargoDependency>>,
    #[serde(rename = "dev-dependencies")]
    dev_dependencies: Option<HashMap<String, CargoDependency>>,
    lib: Option<CargoLib>,
}

fn normalize_dependency_name(dep: &str) -> String {
    dep.replace("-", "_")
}

fn normalize_dependencies(
    deps: HashMap<String, CargoDependency>,
) -> HashMap<String, CargoDependency> {
    let mut new_deps = HashMap::with_capacity(deps.len());
    for (key, val) in deps {
        new_deps.insert(normalize_dependency_name(&key), val);
    }
    new_deps
}

impl CargoManifest {
    fn normalize_dependencies(&mut self) {
        if let Some(deps) = self.dependencies.take() {
            self.dependencies = Some(normalize_dependencies(deps));
        }
        if let Some(dev_deps) = self.dev_dependencies.take() {
            self.dev_dependencies = Some(normalize_dependencies(dev_deps));
        }
    }
}

#[derive(Debug, Deserialize)]
struct CargoPackage {
    name: String,
    authors: Vec<String>,
    description: Option<String>,
    version: String,
    license: Option<String>,
    repository: Option<String>,
}

#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum CargoDependency {
    Simple(String),
    Detailed(DetailedCargoDependency),
}

#[derive(Debug, Deserialize)]
struct DetailedCargoDependency {
    version: Option<String>,
}

#[derive(Debug, Deserialize)]
struct CargoLib {
    #[serde(rename = "crate-type")]
    crate_type: Option<Vec<String>>,
}

#[derive(Serialize)]
struct NpmPackage {
    name: String,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    collaborators: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    description: Option<String>,
    version: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    license: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    repository: Option<Repository>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    files: Vec<String>,
    main: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    types: Option<String>,
}

#[derive(Serialize)]
struct Repository {
    #[serde(rename = "type")]
    ty: String,
    url: String,
}

fn read_cargo_toml(path: &Path) -> Result<CargoManifest, Error> {
    let manifest_path = path.join("Cargo.toml");
    if !manifest_path.is_file() {
        return Error::crate_config(&format!(
            "Crate directory is missing a `Cargo.toml` file; is `{}` the wrong directory?",
            path.display()
        )).map(|_| unreachable!());
    }
    let mut cargo_file = File::open(manifest_path)?;
    let mut cargo_contents = String::new();
    cargo_file.read_to_string(&mut cargo_contents)?;

    let mut manifest: CargoManifest = toml::from_str(&cargo_contents)?;
    manifest.normalize_dependencies();

    Ok(manifest)
}

impl CargoManifest {
    fn into_npm(mut self, scope: &Option<String>, disable_dts: bool, target: &str) -> NpmPackage {
        let filename = self.package.name.replace("-", "_");
        let wasm_file = format!("{}_bg.wasm", filename);
        let js_file = format!("{}.js", filename);

        let dts_file = if disable_dts == true {
            None
        } else {
            Some(format!("{}.d.ts", filename))
        };

        let js_bg_file = if target == "nodejs" {
            Some(format!("{}_bg.js", filename))
        } else {
            None
        };

        if let Some(s) = scope {
            self.package.name = format!("@{}/{}", s, self.package.name);
        }
        let mut files = vec![wasm_file];

        match dts_file {
            Some(ref dts_file) => {
                files.push(dts_file.to_string());
            }
            None => {}
        }

        match js_bg_file {
            Some(ref js_bg_file) => {
                files.push(js_bg_file.to_string());
            }
            None => {}
        }

        NpmPackage {
            name: self.package.name,
            collaborators: self.package.authors,
            description: self.package.description,
            version: self.package.version,
            license: self.package.license,
            repository: self.package.repository.map(|repo_url| Repository {
                ty: "git".to_string(),
                url: repo_url,
            }),
            files: files,
            main: js_file,
            types: dts_file,
        }
    }
}

/// Generate a package.json file inside in `./pkg`.
pub fn write_package_json(
    path: &Path,
    out_dir: &Path,
    scope: &Option<String>,
    disable_dts: bool,
    target: &str,
    step: &Step,
) -> Result<(), Error> {
    let msg = format!("{}Writing a package.json...", emoji::MEMO);

    let warn_fmt = |field| {
        format!(
            "Field '{}' is missing from Cargo.toml. It is not necessary, but recommended",
            field
        )
    };

    PBAR.step(step, &msg);
    let pkg_file_path = out_dir.join("package.json");
    let mut pkg_file = File::create(pkg_file_path)?;
    let crate_data = read_cargo_toml(path)?;
    let npm_data = crate_data.into_npm(scope, disable_dts, target);

    if npm_data.description.is_none() {
        PBAR.warn(&warn_fmt("description"));
    }
    if npm_data.repository.is_none() {
        PBAR.warn(&warn_fmt("repository"));
    }
    if npm_data.license.is_none() {
        PBAR.warn(&warn_fmt("license"));
    }

    let npm_json = serde_json::to_string_pretty(&npm_data)?;
    pkg_file.write_all(npm_json.as_bytes())?;
    Ok(())
}

/// Get the crate name for the crate at the given path.
pub fn get_crate_name(path: &Path) -> Result<String, Error> {
    Ok(read_cargo_toml(path)?.package.name)
}

/// Check that the crate the given path is properly configured.
pub fn check_crate_config(path: &Path, step: &Step) -> Result<(), Error> {
    let msg = format!("{}Checking crate configuration...", emoji::WRENCH);
    PBAR.step(&step, &msg);
    check_wasm_bindgen(path)?;
    check_wasm_bindgen_test(path)?;
    check_crate_type(path)?;
    Ok(())
}

fn check_wasm_bindgen(path: &Path) -> Result<(), Error> {
    get_wasm_bindgen_version(path)?;
    Ok(())
}

fn check_wasm_bindgen_test(path: &Path) -> Result<(), Error> {
    let expected_version = get_wasm_bindgen_version(path)?;

    // Only do the version check if `wasm-bindgen-test` is actually a
    // dependency. Not every crate needs to have tests!
    if let Ok(actual_version) = get_wasm_bindgen_test_version(path) {
        if expected_version != actual_version {
            return Error::crate_config(&format!(
                "The `wasm-bindgen-test` dependency version ({}) must match \
                 the `wasm-bindgen` dependency version ({}), but it does not.",
                actual_version, expected_version
            ));
        }
    }

    Ok(())
}

fn check_crate_type(path: &Path) -> Result<(), Error> {
    if read_cargo_toml(path)?.lib.map_or(false, |lib| {
        lib.crate_type
            .map_or(false, |types| types.iter().any(|s| s == "cdylib"))
    }) {
        return Ok(());
    }
    Error::crate_config(
      "crate-type must be cdylib to compile to wasm32-unknown-unknown. Add the following to your \
       Cargo.toml file:\n\n\
       [lib]\n\
       crate-type = [\"cdylib\"]"
    )
}

fn get_dependency_version(
    dependencies: Option<&HashMap<String, CargoDependency>>,
    dependency: &str,
    dependencies_section_name: &str,
    version_suggestion: &str,
) -> Result<String, Error> {
    if let Some(deps) = dependencies {
        let dependency = normalize_dependency_name(dependency);
        match deps.get(&dependency) {
            Some(CargoDependency::Simple(version))
            | Some(CargoDependency::Detailed(DetailedCargoDependency {
                version: Some(version),
            })) => Ok(version.clone()),
            Some(CargoDependency::Detailed(DetailedCargoDependency { version: None })) => {
                let msg = format!(
                    "\"{}\" dependency is missing its version number",
                    style(&dependency).bold().dim()
                );
                Err(Error::CrateConfig { message: msg })
            }
            None => {
                let message = format!(
                    "Ensure that you have \"{}\" as a dependency in your Cargo.toml file:\n\
                     [{}]\n\
                     {} = \"{}\"",
                    style(&dependency).bold().dim(),
                    dependencies_section_name,
                    dependency,
                    version_suggestion
                );
                Err(Error::CrateConfig { message })
            }
        }
    } else {
        let message = String::from("Could not find crate dependencies");
        Err(Error::CrateConfig { message })
    }
}

/// Get the version of `wasm-bindgen` specified as a dependency.
pub fn get_wasm_bindgen_version(path: &Path) -> Result<String, Error> {
    let toml = read_cargo_toml(path)?;
    get_dependency_version(
        toml.dependencies.as_ref(),
        "wasm-bindgen",
        "dependencies",
        "0.2",
    )
}

/// Get the version of `wasm-bindgen-test` specified as a dependency.
pub fn get_wasm_bindgen_test_version(path: &Path) -> Result<String, Error> {
    let toml = read_cargo_toml(path)?;
    get_dependency_version(
        toml.dev_dependencies.as_ref(),
        "wasm-bindgen-test",
        "dev-dependencies",
        "0.2",
    )
}