// `error_chain!` can recurse deeply
#![recursion_limit = "1024"]

#[macro_use]
extern crate error_chain;
extern crate reqwest;
extern crate serde_json;

use std::io::{self, Write};
use std::{env, process};

use serde_json::{map, Value};

// We'll put our errors in a `errors` module
mod errors {
    // Create the Error, ErrorKind, ResultExt, and Result types
    #[allow(unused_doc_comments)]
    error_chain! {
        // Automatic conversions between this error chain and other
        // error types not defined by the `error_chain!`.
        foreign_links {
            IO(::std::io::Error);
            Reqwest(::reqwest::Error);
        }
    }
}

type Map = map::Map<String, Value>;

fn download<F>(uri: &str, mut action: F, debug: bool) -> errors::Result<()>
where
    F: FnMut(Map) -> errors::Result<()>,
{
    let json: Value = reqwest::blocking::get(uri)?.json()?;
    let json = if let Value::Object(m) = json {
        m
    } else {
        bail!("Malformed JSON: {:?}", json)
    };

    if debug {
        writeln!(io::stderr(), "#json == {}", json.len())?;
        writeln!(
            io::stderr(),
            "License list version {}",
            get(&json, "licenseListVersion")?
        )?;
    }
    action(json)
}

fn get<'a>(m: &'a Map, k: &str) -> errors::Result<&'a Value> {
    if let Some(v) = m.get(k) {
        Ok(v)
    } else {
        bail!("Malformed JSON: {:?} lacks {}", m, k)
    }
}

fn main1(args: &[String]) -> errors::Result<()> {
    let mut upstream_tag = "master";
    let mut debug = false;
    for e in args {
        match e.as_str() {
            "-d" => {
                debug = true;
            }
            s if s.starts_with('v') => {
                upstream_tag = &s;
            }
            _ => bail!("Unknown option {:?}", e),
        }
    }

    let mut stdout = io::stdout();
    let mut stderr = io::stderr();

    if upstream_tag == "master" {
        writeln!(
            stderr,
            "WARN: fetching data from the master branch of spdx/license-list-data; \
             consider specifying a tag (e.g. v3.0)"
        )?;
    } else {
        writeln!(stderr, "Using tag {:?}", upstream_tag)?;
    }

    writeln!(
        stdout,
        "\
/*
 * whitelist fetched from https://github.com/spdx/license-list-data
 *
 * AUTO-GENERATED BY ./fetch-license-list-from-spdx
 * DO NOT MODIFY
 *
 * cargo run -p fetch-license-list-from-spdx -- {} > src/spdx.rs
 */
",
        upstream_tag
    )?;

    let licenses_json_uri = format!(
        "https://raw.githubusercontent.com/spdx/license-list-data/{}/json/licenses.json",
        upstream_tag
    );
    download(
        &licenses_json_uri,
        |json| {
            let licenses = get(&json, "licenses")?;
            let licenses = if let &Value::Array(ref v) = licenses {
                v
            } else {
                bail!("Malformed JSON: {:?}", licenses)
            };
            writeln!(stderr, "#licenses == {}", licenses.len())?;

            let mut v = vec![];
            for lic in licenses.iter() {
                let lic = if let Value::Object(ref m) = *lic {
                    m
                } else {
                    bail!("Malformed JSON: {:?}", lic)
                };
                if debug {
                    writeln!(
                        stderr,
                        "{:?},{:?}",
                        get(&lic, "licenseId"),
                        get(&lic, "name")
                    )?;
                }

                let lic_id = get(&lic, "licenseId")?;
                if let &Value::String(ref s) = lic_id {
                    v.push(s);
                } else {
                    bail!("Malformed JSON: {:?}", lic_id);
                }
            }
            v.sort();

            let lic_list_ver = get(&json, "licenseListVersion")?;
            if let &Value::String(ref s) = lic_list_ver {
                writeln!(stdout, "pub const VERSION: &'static str = {:?};", s)?;
            } else {
                bail!("Malformed JSON: {:?}", lic_list_ver)
            }
            writeln!(stdout)?;
            writeln!(stdout, "pub const LICENSES: &'static [&'static str] = &[")?;
            for lic in v.iter() {
                writeln!(stdout, "    \"{}\",", lic)?;
            }
            writeln!(stdout, "];")?;

            Ok(())
        },
        debug,
    )?;

    writeln!(stdout)?;

    let exceptions_json_uri = format!(
        "https://raw.githubusercontent.com/spdx/license-list-data/{}/json/exceptions.json",
        upstream_tag
    );
    download(
        &exceptions_json_uri,
        |json| {
            let exceptions = get(&json, "exceptions")?;
            let exceptions = if let &Value::Array(ref v) = exceptions {
                v
            } else {
                bail!("Malformed JSON: {:?}", exceptions)
            };
            writeln!(stderr, "#exceptions == {}", exceptions.len())?;

            let mut v = vec![];
            for exc in exceptions.iter() {
                let exc = if let Value::Object(ref m) = *exc {
                    m
                } else {
                    bail!("Malformed JSON: {:?}", exc)
                };
                if debug {
                    writeln!(
                        stderr,
                        "{:?},{:?}",
                        get(&exc, "licenseExceptionId"),
                        get(&exc, "name")
                    )?;
                }

                let lic_exc_id = get(&exc, "licenseExceptionId")?;
                if let &Value::String(ref s) = lic_exc_id {
                    v.push(s);
                } else {
                    bail!("Malformed JSON: {:?}", lic_exc_id)
                };
            }

            writeln!(stdout, "pub const EXCEPTIONS: &'static [&'static str] = &[")?;
            v.sort();
            for exc in v.iter() {
                writeln!(stdout, "    \"{}\",", exc)?;
            }
            writeln!(stdout, "];")?;

            Ok(())
        },
        debug,
    )?;

    Ok(())
}

fn main() {
    let args = env::args().skip(1).collect::<Vec<_>>();
    if let Err(ref e) = main1(args.as_ref()) {
        use error_chain::ChainedError; // trait which holds `display_chain`

        writeln!(io::stderr(), "{}", e.display_chain()).expect("writeln to stderr");
        process::exit(1);
    }
}
