#!/usr/bin/env run-cargo-script
//!
//! ```cargo
//! [dependencies]
//! atomicwrites = "0.3.1"
//! yaml-rust = "0.4.5"
//! ```

extern crate atomicwrites;
extern crate yaml_rust;

use atomicwrites::{AllowOverwrite, AtomicFile};
use std::io::Write;
use std::path::Path;
use yaml_rust::yaml::Hash;
use yaml_rust::{Yaml, YamlEmitter, YamlLoader};

type Pattern = Box<dyn Fn(&str, &str) -> String + 'static>;

fn parse_pattern(pattern: &String) -> Option<(Pattern, Vec<&'static str>)> {
    // TODO: generalize this with an external table or something
    if pattern.contains("%UBUNTU_LTS_VERSION%") {
        Some((
            Box::new(move |p: &str, s: &str| {
                p.replace("%UBUNTU_LTS_VERSION%", s)
            }),
            vec!("18", "20", "22"),
        ))
    } else if pattern.contains("%FEDORA_STABLE_VERSION%") {
        Some((
            Box::new(move |p: &str, s: &str| {
                p.replace("%FEDORA_STABLE_VERSION%", s)
            }),
            // TODO: enable 35 by setting FIND_LIBRARY_USE_LIB64_PATHS
            // for zstd
            vec!("36"),
        ))
    } else {
        None
    }
}

fn substitute_value(pattern: &Pattern, value: &Yaml, substitution: &str) -> Yaml {
    match value {
        Yaml::String(str) => Yaml::String(pattern(&str, substitution)),
        Yaml::Array(arr) => Yaml::Array(
            arr.iter()
                .map(|v| substitute_value(pattern, v, substitution))
                .collect(),
        ),
        Yaml::Hash(hm) => {
            let mut new = Hash::new();
            for (key, value) in hm.iter() {
                new.insert(
                    substitute_value(pattern, &key, substitution),
                    substitute_value(pattern, &value, substitution),
                );
            }
            Yaml::Hash(new)
        }
        otherwise => otherwise.clone(),
    }
}

fn expand_value(value: &Yaml) -> Yaml {
    match value {
        Yaml::Array(arr) => Yaml::Array(arr.iter().map(expand_value).collect()),
        Yaml::Hash(hm) => {
            let mut new = Hash::new();
            for (key, value) in hm.iter() {
                //println!("key = {:?}", key);
                if let Yaml::String(key_str) = key {
                    if let Some((pat, substitutions)) = parse_pattern(&key_str) {
                        for substitution in substitutions {
                            new.insert(
                                Yaml::String(pat(&key_str, substitution)),
                                substitute_value(&pat, &value, substitution),
                            );
                        }
                    } else {
                        new.insert(key.clone(), expand_value(value));
                    }
                } else {
                    // What does a non-string key mean anyways?
                    new.insert(key.clone(), expand_value(value));
                }
            }
            Yaml::Hash(new)
        }
        otherwise => otherwise.clone(),
    }
}

fn main() {
    let input_path = Path::new("release.yml.in");
    let output_path = Path::new("release.yml");

    let contents = match std::fs::read_to_string(input_path) {
        Ok(contents) => contents,
        Err(e) => {
            eprintln!("Unable to read {}: {}", input_path.display(), e);
            std::process::exit(1);
        }
    };
    let doc = match YamlLoader::load_from_str(&contents) {
        Ok(doc) => doc,
        Err(e) => {
            eprintln!("Unable to parse {}: {}", input_path.display(), e);
            std::process::exit(1);
        }
    };

    let new_doc: Vec<Yaml> = doc.iter().map(expand_value).collect();

    let mut out_str = String::new();
    out_str.extend("# @generated by generate-release-yml.rs\n".chars());

    let mut emitter = YamlEmitter::new(&mut out_str);
    for doc in new_doc {
        match emitter.dump(&doc) {
            Ok(()) => (),
            Err(e) => {
                eprintln!("Unable to generate {}: {}", output_path.display(), e);
                std::process::exit(1);
            }
        }
    }

    let af = AtomicFile::new(&output_path, AllowOverwrite);
    match af.write(|f| f.write_all(&out_str.as_bytes())) {
        Ok(()) => (),
        Err(e) => {
            eprintln!("Unable to write {}: {}", output_path.display(), e);
            std::process::exit(1);
        }
    }
}
