// Copyright 2017 The RLS Project Developers.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

//! Defines an `AnalysisLoader` trait, which allows to specify directories
//! from which save-analysis JSON files can be read. Also supplies a
//! default implementation `CargoAnalysisLoader` for Cargo-emitted save-analysis
//! files.

use std::env;
use std::ffi::OsStr;
use std::fmt;
use std::path::{Path, PathBuf};
use std::process::Command;

use AnalysisHost;

#[derive(Debug)]
pub struct CargoAnalysisLoader {
    pub path_prefix: Option<PathBuf>,
    pub target: Target,
}

#[derive(Debug, new)]
pub struct SearchDirectory {
    pub path: PathBuf,
    // The directory searched must have spans re-written to be based on a new
    // path prefix. This happens for example when the std lib crates are compiled
    // on the Rust CI, but live in the user's sysroot directory, this adjustment
    // (which happens in `lower_span`) means we have the new source location.
    pub prefix_rewrite: Option<PathBuf>,
}

impl CargoAnalysisLoader {
    pub fn new(target: Target) -> CargoAnalysisLoader {
        CargoAnalysisLoader { path_prefix: None, target }
    }
}

/// Allows to specify from where and which analysis files will be considered
/// when reloading data to lower.
pub trait AnalysisLoader: Sized {
    fn needs_hard_reload(&self, path_prefix: &Path) -> bool;
    fn fresh_host(&self) -> AnalysisHost<Self>;
    fn set_path_prefix(&mut self, path_prefix: &Path);
    fn abs_path_prefix(&self) -> Option<PathBuf>;
    /// Returns every directory in which analysis files are to be considered.
    fn search_directories(&self) -> Vec<SearchDirectory>;
}

impl AnalysisLoader for CargoAnalysisLoader {
    fn needs_hard_reload(&self, path_prefix: &Path) -> bool {
        self.path_prefix.as_ref().map_or(true, |p| p != path_prefix)
    }

    fn fresh_host(&self) -> AnalysisHost<Self> {
        AnalysisHost::new_with_loader(CargoAnalysisLoader {
            path_prefix: self.path_prefix.clone(),
            ..CargoAnalysisLoader::new(self.target)
        })
    }

    fn set_path_prefix(&mut self, path_prefix: &Path) {
        self.path_prefix = Some(path_prefix.to_owned());
    }

    fn abs_path_prefix(&self) -> Option<PathBuf> {
        self.path_prefix.as_ref().map(|s| Path::new(s).canonicalize().unwrap().to_owned())
    }

    fn search_directories(&self) -> Vec<SearchDirectory> {
        let path_prefix = self.path_prefix.as_ref().unwrap();
        let target = self.target.to_string();

        let deps_path =
            path_prefix.join("target").join("rls").join(&target).join("deps").join("save-analysis");
        // FIXME sys_root_path allows to break out of 'sandbox' - is that Ok?
        // FIXME libs_path and src_path both assume the default `libdir = "lib"`.
        let sys_root_path = sys_root_path();
        let target_triple = extract_target_triple(sys_root_path.as_path());
        let libs_path =
            sys_root_path.join("lib").join("rustlib").join(&target_triple).join("analysis");

        let src_path = sys_root_path.join("lib").join("rustlib").join("src").join("rust");

        vec![SearchDirectory::new(libs_path, Some(src_path)), SearchDirectory::new(deps_path, None)]
    }
}

fn extract_target_triple(sys_root_path: &Path) -> String {
    // First try to get the triple from the rustc version output,
    // otherwise fall back on the rustup-style toolchain path.
    // FIXME: Both methods assume that the target is the host triple,
    // which isn't the case for cross-compilation (rust-lang/rls#309).
    extract_rustc_host_triple().unwrap_or_else(|| extract_rustup_target_triple(sys_root_path))
}

fn extract_rustc_host_triple() -> Option<String> {
    let rustc = env::var("RUSTC").unwrap_or_else(|_| String::from("rustc"));
    let verbose_version = Command::new(rustc)
        .arg("--verbose")
        .arg("--version")
        .output()
        .ok()
        .and_then(|out| String::from_utf8(out.stdout).ok())?;

    // Extracts the triple from a line like `host: x86_64-unknown-linux-gnu`
    verbose_version
        .lines()
        .find(|line| line.starts_with("host: "))
        .and_then(|host| host.split_whitespace().nth(1))
        .map(String::from)
}

// FIXME: This can fail when using a custom toolchain in rustup (often linked to
// `/$rust_repo/build/$target/stage2`)
fn extract_rustup_target_triple(sys_root_path: &Path) -> String {
    // Extracts nightly-x86_64-pc-windows-msvc from
    // $HOME/.rustup/toolchains/nightly-x86_64-pc-windows-msvc
    let toolchain =
        sys_root_path.iter().last().and_then(OsStr::to_str).expect("extracting toolchain failed");
    // Extracts x86_64-pc-windows-msvc from nightly-x86_64-pc-windows-pc
    toolchain.splitn(2, '-').last().map(String::from).expect("extracting triple failed")
}

fn sys_root_path() -> PathBuf {
    env::var("SYSROOT")
        .ok()
        .map(PathBuf::from)
        .or_else(|| {
            Command::new(env::var("RUSTC").unwrap_or_else(|_| String::from("rustc")))
                .arg("--print")
                .arg("sysroot")
                .output()
                .ok()
                .and_then(|out| String::from_utf8(out.stdout).ok())
                .map(|s| PathBuf::from(s.trim()))
        })
        .expect("need to specify SYSROOT or RUSTC env vars, or rustc must be in PATH")
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Target {
    Release,
    Debug,
}

impl fmt::Display for Target {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            Target::Release => write!(f, "release"),
            Target::Debug => write!(f, "debug"),
        }
    }
}

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

    #[test]
    fn windows_path() {
        let path = Path::new(r#"C:\Users\user\.rustup\toolchains\nightly-x86_64-pc-windows-msvc"#);
        assert_eq!(extract_rustup_target_triple(path), String::from("x86_64-pc-windows-msvc"));
    }

    #[test]
    fn unix_path() {
        let path = Path::new("/home/user/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu");
        assert_eq!(extract_rustup_target_triple(path), String::from("x86_64-unknown-linux-gnu"));
    }

    #[test]
    fn target_triple() {
        let sys_root_path = sys_root_path();
        let target_triple = extract_target_triple(&sys_root_path);
        let target_path = sys_root_path.join("lib").join("rustlib").join(&target_triple);
        assert!(target_path.is_dir(), "{:?} is not a directory!", target_path);
    }
}
