
extern crate serde;
extern crate serde_json;
extern crate serde_yaml;
extern crate clap;

use serde_derive::Deserialize;
use clap::{Arg, App};

use std::fs::File;
use std::io::prelude::*;
use std::string::String;

#[derive(Debug, Deserialize)]
struct Target {
    name: String,
    #[serde(default)]
    cflags: Vec<String>,
    #[serde(default)]
    ldflags: Vec<String>,
    #[serde(default)]
    libs: Vec<String>,
    sources: Vec<String>,
}

/// Structure containing the build information.
///
/// compiler will default to "clang"
/// cflags and ldflags will default to empty
///
/// There must be at least one target.
#[derive(Debug, Deserialize)]
struct Buildinfo {
    #[serde(default)]
    makefile: bool,
    #[serde(default)]
    compiler: String,
    #[serde(default)]
    cflags: Vec<String>,
    #[serde(default)]
    ldflags: Vec<String>,
    #[serde(default)]
    libs: Vec<String>,
    targets: Vec<Target>,
}

impl Buildinfo {

    pub fn to_ninja_string(&self) -> String {

        let mut ninja_string = "\nninja_required_version = 1.6\nroot = .\nbuilddir = .target\n".to_string();

        if self.compiler.is_empty() {
            ninja_string += "\ncompiler = clang\n\nlinker = clang\n";
        } else {
            ninja_string += format!("\ncompiler = {}\n", self.compiler).as_str();
            ninja_string += format!("\nlinker = {}\n", self.compiler).as_str();
        }

        if !self.cflags.is_empty() {
            ninja_string += format!("\ncflags = {}\n", self.cflags.join(" ")).as_str();
        }

        if !self.ldflags.is_empty() {
            ninja_string += format!("\nldflags = {}\n", self.ldflags.join(" ")).as_str();
        }

        for t in self.targets.iter() {

            if !t.cflags.is_empty() {
                ninja_string += format!("\n{}_cflags = {}\n", t.name, t.cflags.join(" ")).as_str();
            }

            if !t.ldflags.is_empty() {
                ninja_string += format!("\n{}_ldflags = {}\n", t.name, t.ldflags.join(" ")).as_str();
            }

        }

        // The build lines can only contain dependency stuff, so we have to make a compile and link
        // command for each target in order to have target specific flags.

        for t in self.targets.iter() {

            ninja_string += format!("\nrule compile_{}\n", t.name).as_str();
            ninja_string += format!("  command = $compiler -MMD -MT $out -MF $out.d $cflags ${}_cflags -c $in -o $out\n", t.name).as_str();
            ninja_string += "  description = COMPILE $out\n";
            ninja_string += "  depfile = $out.d\n";
            ninja_string += "  deps = gcc\n";

            ninja_string += format!("\nrule link_{}\n", t.name).as_str();
            ninja_string += format!("  command = $compiler $ldflags ${}_ldflags -o $out $in $libs\n", t.name).as_str();
            ninja_string += "  description = LINK $out\n";

        }

        for t in self.targets.iter() {

            for s in t.sources.iter() {
                ninja_string += format!("\nbuild $builddir/{0}_{1}.o: compile_{0} $root/{1}\n", t.name, s).as_str();
            }

        }

        for t in self.targets.iter() {
            let all_objects = t.sources.join(format!(".o $builddir/{}_", t.name).as_str());
            ninja_string += format!("\nbuild {0}: link_{0} $builddir/{0}_{1}.o", t.name, all_objects).as_str();

            if !self.libs.is_empty() || !t.libs.is_empty() {

                ninja_string += " |\n";

                ninja_string += "    libs = ";

                if !self.libs.is_empty() {
                    ninja_string += format!("-l{} ", self.libs.join(" -l")).as_str().trim();
                }

                if !t.libs.is_empty() {
                    ninja_string += format!(" -l{}", t.libs.join(" -l")).as_str().trim();
                }

            } else {
                ninja_string += "\n";
            }

            ninja_string += "\n";
        }

        let all_targets: String =
            self.targets.iter().map(|t| {
                t.name.clone()
            }).collect::<Vec<String>>().join(" ");

        ninja_string += format!("\nbuild all: phony {}\n", all_targets).as_str();

        ninja_string

    } // to_ninja_string()


    pub fn write_build_file(&self) -> std::result::Result<(),String> {

        let ninja_string = self.to_ninja_string();

        { // scope for file
            let mut file = File::create("build.ninja").unwrap();
            file.write_all(ninja_string.as_bytes()).unwrap();
        }

        if self.makefile {
            let mut mkfile = File::create("Makefile").unwrap();
            mkfile.write_all(b"\n.PHONY: all\n\n").unwrap();
            mkfile.write_all(b"\nall:\n").unwrap();
            mkfile.write_all(b"	@ninja").unwrap();

            self.targets.iter().for_each(|t| mkfile.write_all(format!("\n.PHONY: {0}\n\n\n{0}:\n	@ninja {0}\n", t.name).as_bytes()).unwrap());

            mkfile.write_all(b"\n.PHONY: clean\n\nclean:\n	@ninja -t clean\n").unwrap();
        }

        Ok(())

    } // write_build_file()

}

fn load_from_file(source_name: &str) -> std::result::Result<(),String> {

    let result: Result<Buildinfo,_> = serde_any::from_file(source_name);

    if let Ok(binfo) = result {
        binfo.write_build_file()
    } else {
        Err(format!("Failed to read {} -- check the syntax", source_name))
    }

} // load_from_file()

fn main() {

    let matches = App::new("Ninjify")
        .version("1.0")
        .author("John Nanney <john.nanney@gmail.com>")
        .about("Creates a build.ninja file from RON, JSON, TOML, or YAML build file")
        .arg(Arg::with_name("FILE")
            .short("f")
            .long("file")
            .help("Sets the input file to use")
            .required(true)
            .takes_value(true))
        .get_matches();

    let buildfile = matches.value_of("FILE").unwrap();

    match load_from_file(buildfile) {
        Ok(_) => println!("Generated build.ninja"),
        Err(string) => println!("{}", string),
    };

} // main()

#[cfg(test)]

mod test {

    use super::*;

    fn should_be_ninja_string() -> String {
    "
ninja_required_version = 1.6
root = .
builddir = .target

compiler = gcc

linker = gcc

cflags = -Wall -Werror -pthread

nifty_cflags = -fPIE -O3

nifty_ldflags = -O3

nifty_debug_cflags = -fPIE -O0 -g

nifty_debug_ldflags = -O0 -g

rule compile_nifty
  command = $compiler -MMD -MT $out -MF $out.d $cflags $nifty_cflags -c $in -o $out
  description = COMPILE $out
  depfile = $out.d
  deps = gcc

rule link_nifty
  command = $compiler $ldflags $nifty_ldflags -o $out $in $libs
  description = LINK $out

rule compile_nifty_debug
  command = $compiler -MMD -MT $out -MF $out.d $cflags $nifty_debug_cflags -c $in -o $out
  description = COMPILE $out
  depfile = $out.d
  deps = gcc

rule link_nifty_debug
  command = $compiler $ldflags $nifty_debug_ldflags -o $out $in $libs
  description = LINK $out

build $builddir/nifty_main.c.o: compile_nifty $root/main.c

build $builddir/nifty_greeter.c.o: compile_nifty $root/greeter.c

build $builddir/nifty_debug_main.c.o: compile_nifty_debug $root/main.c

build $builddir/nifty_debug_greeter.c.o: compile_nifty_debug $root/greeter.c

build nifty: link_nifty $builddir/nifty_main.c.o $builddir/nifty_greeter.c.o |
    libs = -lpthread -lrt

build nifty_debug: link_nifty_debug $builddir/nifty_debug_main.c.o $builddir/nifty_debug_greeter.c.o |
    libs = -lpthread -lrt

build all: phony nifty nifty_debug\n".to_string()
    }

    #[test]
    pub fn test_conversion_from_json() {
        let binfo: Buildinfo = serde_any::from_file("MyFunProject.json").unwrap();
        assert_eq!(binfo.to_ninja_string(), should_be_ninja_string());
    }

    #[test]
    pub fn test_conversion_from_ron() {
        let binfo: Buildinfo = serde_any::from_file("MyFunProject.ron").unwrap();
        assert_eq!(binfo.to_ninja_string(), should_be_ninja_string());
    }

    #[test]
    pub fn test_conversion_from_yaml() {
        let binfo: Buildinfo = serde_any::from_file("MyFunProject.yaml").unwrap();
        assert_eq!(binfo.to_ninja_string(), should_be_ninja_string());
    }

    #[test]
    pub fn test_conversion_from_toml() {
        let binfo: Buildinfo = serde_any::from_file("MyFunProject.toml").unwrap();
        assert_eq!(binfo.to_ninja_string(), should_be_ninja_string());
    }

} // mod test
