// Copyright 2018 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Parse the output of a cargo build.rs script and generate a list of flags and
//! environment variable for the build.
use std::io::{BufRead, BufReader, Read};
use std::process::{Command, Output};

#[derive(Debug, PartialEq, Eq)]
pub struct CompileAndLinkFlags {
    pub compile_flags: String,
    pub link_flags: String,
}

/// Enum containing all the considered return value from the script
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BuildScriptOutput {
    /// cargo:rustc-link-lib
    LinkLib(String),
    /// cargo:rustc-link-search
    LinkSearch(String),
    /// cargo:rustc-cfg
    Cfg(String),
    /// cargo:rustc-flags
    Flags(String),
    /// cargo:rustc-env
    Env(String),
    /// cargo:VAR=VALUE
    DepEnv(String),
}

impl BuildScriptOutput {
    /// Converts a line into a [BuildScriptOutput] enum.
    ///
    /// Examples
    /// ```rust
    /// assert_eq!(BuildScriptOutput::new("cargo:rustc-link-lib=lib"), Some(BuildScriptOutput::LinkLib("lib".to_owned())));
    /// ```
    fn new(line: &str) -> Option<BuildScriptOutput> {
        let split = line.splitn(2, '=').collect::<Vec<_>>();
        if split.len() <= 1 {
            // Not a cargo directive.
            return None;
        }
        let param = split[1].trim().to_owned();
        let key_split = split[0].splitn(2, ':').collect::<Vec<_>>();
        if key_split.len() <= 1 || key_split[0] != "cargo" {
            // Not a cargo directive.
            return None;
        }
        match key_split[1] {
            "rustc-link-lib" => Some(BuildScriptOutput::LinkLib(param)),
            "rustc-link-search" => Some(BuildScriptOutput::LinkSearch(param)),
            "rustc-cfg" => Some(BuildScriptOutput::Cfg(param)),
            "rustc-flags" => Some(BuildScriptOutput::Flags(param)),
            "rustc-env" => Some(BuildScriptOutput::Env(param)),
            "rerun-if-changed" | "rerun-if-env-changed" =>
            // Ignored because Bazel will re-run if those change all the time.
            {
                None
            }
            "warning" => {
                eprint!("Build Script Warning: {}", split[1]);
                None
            }
            "rustc-cdylib-link-arg" => {
                // cargo:rustc-cdylib-link-arg=FLAG — Passes custom flags to a linker for cdylib crates.
                eprint!(
                    "Warning: build script returned unsupported directive `{}`",
                    split[0]
                );
                None
            }
            _ => {
                // cargo:KEY=VALUE — Metadata, used by links scripts.
                Some(BuildScriptOutput::DepEnv(format!(
                    "{}={}",
                    key_split[1].to_uppercase(),
                    param
                )))
            }
        }
    }

    /// Converts a [BufReader] into a vector of [BuildScriptOutput] enums.
    fn from_reader<T: Read>(mut reader: BufReader<T>) -> Vec<BuildScriptOutput> {
        let mut result = Vec::<BuildScriptOutput>::new();
        let mut line = String::new();
        while reader.read_line(&mut line).expect("Cannot read line") != 0 {
            if let Some(bso) = BuildScriptOutput::new(&line) {
                result.push(bso);
            }
            line.clear();
        }
        result
    }

    /// Take a [Command], execute it and converts its input into a vector of [BuildScriptOutput]
    pub fn from_command(cmd: &mut Command) -> Result<(Vec<BuildScriptOutput>, Output), Output> {
        let child_output = cmd
            .output()
            .expect("Unable to start binary");
        if child_output.status.success() {
            let reader = BufReader::new(child_output.stdout.as_slice());
            let output = Self::from_reader(reader);
            Ok((output, child_output))
        } else {
            Err(child_output)
        }
    }

    /// Convert a vector of [BuildScriptOutput] into a list of environment variables.
    pub fn to_env(v: &Vec<BuildScriptOutput>, exec_root: &str) -> String {
        v.iter()
            .filter_map(|x| {
                if let BuildScriptOutput::Env(env) = x {
                    Some(Self::redact_exec_root(env, exec_root))
                } else {
                    None
                }
            })
            .collect::<Vec<_>>()
            .join("\n")
    }

    /// Convert a vector of [BuildScriptOutput] into a list of dependencies environment variables.
    pub fn to_dep_env(v: &Vec<BuildScriptOutput>, crate_links: &str, exec_root: &str) -> String {
        let prefix = format!("DEP_{}_", crate_links.replace("-", "_").to_uppercase());
        v.iter()
            .filter_map(|x| {
                if let BuildScriptOutput::DepEnv(env) = x {
                    Some(format!(
                        "{}{}",
                        prefix,
                        Self::redact_exec_root(env, exec_root)
                    ))
                } else {
                    None
                }
            })
            .collect::<Vec<_>>()
            .join("\n")
    }

    /// Convert a vector of [BuildScriptOutput] into a flagfile.
    pub fn to_flags(v: &Vec<BuildScriptOutput>, exec_root: &str) -> CompileAndLinkFlags {
        let mut compile_flags = Vec::new();
        let mut link_flags = Vec::new();

        for flag in v {
            match flag {
                BuildScriptOutput::Cfg(e) => compile_flags.push(format!("--cfg={}", e)),
                BuildScriptOutput::Flags(e) => compile_flags.push(e.to_owned()),
                BuildScriptOutput::LinkLib(e) => link_flags.push(format!("-l{}", e)),
                BuildScriptOutput::LinkSearch(e) => link_flags.push(format!("-L{}", e)),
                _ => {}
            }
        }
        CompileAndLinkFlags {
            compile_flags: compile_flags.join("\n"),
            link_flags: Self::redact_exec_root(&link_flags.join("\n"), exec_root),
        }
    }

    fn redact_exec_root(value: &str, exec_root: &str) -> String {
        value.replace(exec_root, "${pwd}")
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Cursor;

    #[test]
    fn test_from_read_buffer_to_env_and_flags() {
        let buff = Cursor::new(
            "
cargo:rustc-link-lib=sdfsdf
cargo:rustc-env=FOO=BAR
cargo:rustc-link-search=/some/absolute/path/bleh
cargo:rustc-env=BAR=FOO
cargo:rustc-flags=-Lblah
cargo:rerun-if-changed=ignored
cargo:rustc-cfg=feature=awesome
cargo:version=123
cargo:version_number=1010107f
cargo:include_path=/some/absolute/path/include
cargo:rustc-env=SOME_PATH=/some/absolute/path/beep
",
        );
        let reader = BufReader::new(buff);
        let result = BuildScriptOutput::from_reader(reader);
        assert_eq!(result.len(), 10);
        assert_eq!(result[0], BuildScriptOutput::LinkLib("sdfsdf".to_owned()));
        assert_eq!(result[1], BuildScriptOutput::Env("FOO=BAR".to_owned()));
        assert_eq!(
            result[2],
            BuildScriptOutput::LinkSearch("/some/absolute/path/bleh".to_owned())
        );
        assert_eq!(result[3], BuildScriptOutput::Env("BAR=FOO".to_owned()));
        assert_eq!(result[4], BuildScriptOutput::Flags("-Lblah".to_owned()));
        assert_eq!(
            result[5],
            BuildScriptOutput::Cfg("feature=awesome".to_owned())
        );
        assert_eq!(
            result[6],
            BuildScriptOutput::DepEnv("VERSION=123".to_owned())
        );
        assert_eq!(
            result[7],
            BuildScriptOutput::DepEnv("VERSION_NUMBER=1010107f".to_owned())
        );
        assert_eq!(
            result[9],
            BuildScriptOutput::Env("SOME_PATH=/some/absolute/path/beep".to_owned())
        );

        assert_eq!(
            BuildScriptOutput::to_dep_env(&result, "ssh2", "/some/absolute/path"),
            "DEP_SSH2_VERSION=123\nDEP_SSH2_VERSION_NUMBER=1010107f\nDEP_SSH2_INCLUDE_PATH=${pwd}/include".to_owned()
        );
        assert_eq!(
            BuildScriptOutput::to_env(&result, "/some/absolute/path"),
            "FOO=BAR\nBAR=FOO\nSOME_PATH=${pwd}/beep".to_owned()
        );
        assert_eq!(
            BuildScriptOutput::to_flags(&result, "/some/absolute/path"),
            CompileAndLinkFlags {
                // -Lblah was output as a rustc-flags, so even though it probably _should_ be a link
                // flag, we don't treat it like one.
                compile_flags: "-Lblah\n--cfg=feature=awesome".to_owned(),
                link_flags: "-lsdfsdf\n-L${pwd}/bleh".to_owned(),
            }
        );
    }
}
