// Copyright 2012-2013 The Rust Project Developers. See the COPYRIGHT
// file at the top-level directory of this distribution and at
// http://rust-lang.org/COPYRIGHT.
//
// 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.

// rustpkg - a package manager and build system for Rust

#[link(name = "rustpkg",
       vers = "0.8-pre",
       uuid = "25de5e6e-279e-4a20-845c-4cabae92daaf",
       url = "https://github.com/mozilla/rust/tree/master/src/librustpkg")];

#[license = "MIT/ASL2"];
#[crate_type = "lib"];

extern mod extra;
extern mod rustc;
extern mod syntax;

use std::result;
use std::io;
use std::os;
use std::run;
use std::str;

pub use std::path::Path;
use std::hashmap::HashMap;

use rustc::driver::{driver, session};
use rustc::metadata::filesearch;
use rustc::metadata::filesearch::rust_path;
use extra::{getopts};
use syntax::{ast, diagnostic};
use util::*;
use messages::*;
use path_util::{build_pkg_id_in_workspace, first_pkgid_src_in_workspace};
use path_util::{U_RWX, in_rust_path};
use path_util::{built_executable_in_workspace, built_library_in_workspace, default_workspace};
use path_util::{target_executable_in_workspace, target_library_in_workspace};
use source_control::is_git_dir;
use workspace::{each_pkg_parent_workspace, pkg_parent_workspaces, in_workspace, cwd_to_workspace};
use context::Ctx;
use package_id::PkgId;
use package_source::PkgSrc;

pub mod api;
mod conditions;
mod context;
mod crate;
mod installed_packages;
mod messages;
mod package_id;
mod package_source;
mod path_util;
mod search;
mod source_control;
mod target;
#[cfg(test)]
mod tests;
mod util;
mod version;
mod workspace;

pub mod usage;

/// A PkgScript represents user-supplied custom logic for
/// special build hooks. This only exists for packages with
/// an explicit package script.
struct PkgScript<'self> {
    /// Uniquely identifies this package
    id: &'self PkgId,
    // Used to have this field:    deps: ~[(~str, Option<~str>)]
    // but I think it shouldn't be stored here
    /// The contents of the package script: either a file path,
    /// or a string containing the text of the input
    input: driver::input,
    /// The session to use *only* for compiling the custom
    /// build script
    sess: session::Session,
    /// The config for compiling the custom build script
    cfg: ast::CrateConfig,
    /// The crate for the custom build script
    crate: @ast::Crate,
    /// Directory in which to store build output
    build_dir: Path
}

impl<'self> PkgScript<'self> {
    /// Given the path name for a package script
    /// and a package ID, parse the package script into
    /// a PkgScript that we can then execute
    fn parse<'a>(sysroot: @Path,
                 script: Path,
                 workspace: &Path,
                 id: &'a PkgId) -> PkgScript<'a> {
        // Get the executable name that was invoked
        let binary = os::args()[0].to_managed();
        // Build the rustc session data structures to pass
        // to the compiler
        debug!("pkgscript parse: %s", sysroot.to_str());
        let options = @session::options {
            binary: binary,
            maybe_sysroot: Some(sysroot),
            crate_type: session::bin_crate,
            .. (*session::basic_options()).clone()
        };
        let input = driver::file_input(script);
        let sess = driver::build_session(options, diagnostic::emit);
        let cfg = driver::build_configuration(sess);
        let crate = driver::phase_1_parse_input(sess, cfg.clone(), &input);
        let crate = driver::phase_2_configure_and_expand(sess, cfg.clone(), crate);
        let work_dir = build_pkg_id_in_workspace(id, workspace);

        debug!("Returning package script with id %s", id.to_str());

        PkgScript {
            id: id,
            input: input,
            sess: sess,
            cfg: cfg,
            crate: crate,
            build_dir: work_dir
        }
    }

    /// Run the contents of this package script, where <what>
    /// is the command to pass to it (e.g., "build", "clean", "install")
    /// Returns a pair of an exit code and list of configs (obtained by
    /// calling the package script's configs() function if it exists
    // FIXME (#4432): Use workcache to only compile the script when changed
    fn run_custom(&self, sysroot: @Path) -> (~[~str], ExitCode) {
        let sess = self.sess;

        debug!("Working directory = %s", self.build_dir.to_str());
        // Collect together any user-defined commands in the package script
        let crate = util::ready_crate(sess, self.crate);
        debug!("Building output filenames with script name %s",
               driver::source_name(&self.input));
        let exe = self.build_dir.push(~"pkg" + util::exe_suffix());
        util::compile_crate_from_input(&self.input,
                                       &self.build_dir,
                                       sess,
                                       crate);
        debug!("Running program: %s %s %s", exe.to_str(),
               sysroot.to_str(), "install");
        // FIXME #7401 should support commands besides `install`
        let status = run::process_status(exe.to_str(), [sysroot.to_str(), ~"install"]);
        if status != 0 {
            return (~[], status);
        }
        else {
            debug!("Running program (configs): %s %s %s",
                   exe.to_str(), sysroot.to_str(), "configs");
            let output = run::process_output(exe.to_str(), [sysroot.to_str(), ~"configs"]);
            // Run the configs() function to get the configs
            let cfgs = str::from_bytes_slice(output.output).word_iter()
                .map(|w| w.to_owned()).collect();
            (cfgs, output.status)
        }
    }

    fn hash(&self) -> ~str {
        self.id.hash()
    }

}

pub trait CtxMethods {
    fn run(&self, cmd: &str, args: ~[~str]);
    fn do_cmd(&self, _cmd: &str, _pkgname: &str);
    fn build(&self, workspace: &Path, pkgid: &PkgId);
    fn clean(&self, workspace: &Path, id: &PkgId);
    fn info(&self);
    fn install(&self, workspace: &Path, id: &PkgId);
    fn install_no_build(&self, workspace: &Path, id: &PkgId);
    fn prefer(&self, _id: &str, _vers: Option<~str>);
    fn test(&self);
    fn uninstall(&self, _id: &str, _vers: Option<~str>);
    fn unprefer(&self, _id: &str, _vers: Option<~str>);
}

impl CtxMethods for Ctx {

    fn run(&self, cmd: &str, args: ~[~str]) {
        match cmd {
            "build" => {
                if args.len() < 1 {
                    if !in_workspace(|| { usage::build() } ) {
                        return;
                    }
                    let (workspace, pkgid) = cwd_to_workspace();
                    self.build(&workspace, &pkgid);
                }
                else {
                    // The package id is presumed to be the first command-line
                    // argument
                    let pkgid = PkgId::new(args[0].clone());
                    do each_pkg_parent_workspace(&pkgid) |workspace| {
                        debug!("found pkg %s in workspace %s, trying to build",
                               pkgid.to_str(), workspace.to_str());
                        self.build(workspace, &pkgid);
                        true
                    };
                }
            }
            "clean" => {
                if args.len() < 1 {
                    if !in_workspace(|| { usage::clean() } ) {
                        return;
                    }
                    // tjc: Maybe clean should clean all the packages in the
                    // current workspace, though?
                    let (workspace, pkgid) = cwd_to_workspace();
                    self.clean(&workspace, &pkgid);

                }
                else {
                    // The package id is presumed to be the first command-line
                    // argument
                    let pkgid = PkgId::new(args[0].clone());
                    let cwd = os::getcwd();
                    self.clean(&cwd, &pkgid); // tjc: should use workspace, not cwd
                }
            }
            "do" => {
                if args.len() < 2 {
                    return usage::do_cmd();
                }

                self.do_cmd(args[0].clone(), args[1].clone());
            }
            "info" => {
                self.info();
            }
            "install" => {
                if args.len() < 1 {
                    if !in_workspace(|| { usage::install() }) {
                        return;
                    }
                    let (workspace, pkgid) = cwd_to_workspace();
                    self.install(&workspace, &pkgid);
                }
                else {
                    // The package id is presumed to be the first command-line
                    // argument
                    let pkgid = PkgId::new(args[0]);
                    let workspaces = pkg_parent_workspaces(&pkgid);
                    if workspaces.is_empty() {
                        let rp = rust_path();
                        assert!(!rp.is_empty());
                        let src = PkgSrc::new(&rp[0], &pkgid);
                        src.fetch_git();
                        self.install(&rp[0], &pkgid);
                    }
                    else {
                        do each_pkg_parent_workspace(&pkgid) |workspace| {
                            self.install(workspace, &pkgid);
                            true
                        };
                    }
                }
            }
            "list" => {
                io::println("Installed packages:");
                do installed_packages::list_installed_packages |pkg_id| {
                    println(pkg_id.path.to_str());
                    true
                };
            }
            "prefer" => {
                if args.len() < 1 {
                    return usage::uninstall();
                }

                self.prefer(args[0], None);
            }
            "test" => {
                self.test();
            }
            "uninstall" => {
                if args.len() < 1 {
                    return usage::uninstall();
                }

                let pkgid = PkgId::new(args[0]);
                if !installed_packages::package_is_installed(&pkgid) {
                    warn(fmt!("Package %s doesn't seem to be installed! Doing nothing.", args[0]));
                    return;
                }
                else {
                    let rp = rust_path();
                    assert!(!rp.is_empty());
                    do each_pkg_parent_workspace(&pkgid) |workspace| {
                        path_util::uninstall_package_from(workspace, &pkgid);
                        note(fmt!("Uninstalled package %s (was installed in %s)",
                                  pkgid.to_str(), workspace.to_str()));
                        true
                    };
                }
            }
            "unprefer" => {
                if args.len() < 1 {
                    return usage::unprefer();
                }

                self.unprefer(args[0], None);
            }
            _ => fail!(fmt!("I don't know the command `%s`", cmd))
        }
    }

    fn do_cmd(&self, _cmd: &str, _pkgname: &str)  {
        // stub
        fail!("`do` not yet implemented");
    }

    fn build(&self, workspace: &Path, pkgid: &PkgId) {
        debug!("build: workspace = %s (in Rust path? %? is git dir? %? \
                pkgid = %s", workspace.to_str(),
               in_rust_path(workspace), is_git_dir(&workspace.push_rel(&pkgid.path)),
               pkgid.to_str());
        let src_dir   = first_pkgid_src_in_workspace(pkgid, workspace);

        // If workspace isn't in the RUST_PATH, and it's a git repo,
        // then clone it into the first entry in RUST_PATH, and repeat
        debug!("%? %? %s", in_rust_path(workspace),
               is_git_dir(&workspace.push_rel(&pkgid.path)),
               workspace.to_str());
        if !in_rust_path(workspace) && is_git_dir(&workspace.push_rel(&pkgid.path)) {
            let out_dir = default_workspace().push("src").push_rel(&pkgid.path);
            source_control::git_clone(&workspace.push_rel(&pkgid.path),
                                      &out_dir, &pkgid.version);
            let default_ws = default_workspace();
            debug!("Calling build recursively with %? and %?", default_ws.to_str(),
                   pkgid.to_str());
            return self.build(&default_ws, pkgid);
        }

        // Create the package source
        let mut src = PkgSrc::new(workspace, pkgid);
        debug!("Package src = %?", src);

        // Is there custom build logic? If so, use it
        let pkg_src_dir = src_dir;
        let mut custom = false;
        debug!("Package source directory = %?", pkg_src_dir);
        let cfgs = match pkg_src_dir.chain_ref(|p| src.package_script_option(p)) {
            Some(package_script_path) => {
                let sysroot = self.sysroot_to_use().expect("custom build needs a sysroot");
                let pscript = PkgScript::parse(sysroot,
                                               package_script_path,
                                               workspace,
                                               pkgid);
                let (cfgs, hook_result) = pscript.run_custom(sysroot);
                debug!("Command return code = %?", hook_result);
                if hook_result != 0 {
                    fail!("Error running custom build command")
                }
                custom = true;
                // otherwise, the package script succeeded
                cfgs
            }
            None => {
                debug!("No package script, continuing");
                ~[]
            }
        };

        // If there was a package script, it should have finished
        // the build already. Otherwise...
        if !custom {
            // Find crates inside the workspace
            src.find_crates();
            // Build it!
            src.build(self, cfgs);
        }
    }

    fn clean(&self, workspace: &Path, id: &PkgId)  {
        // Could also support a custom build hook in the pkg
        // script for cleaning files rustpkg doesn't know about.
        // Do something reasonable for now

        let dir = build_pkg_id_in_workspace(id, workspace);
        note(fmt!("Cleaning package %s (removing directory %s)",
                        id.to_str(), dir.to_str()));
        if os::path_exists(&dir) {
            os::remove_dir_recursive(&dir);
            note(fmt!("Removed directory %s", dir.to_str()));
        }

        note(fmt!("Cleaned package %s", id.to_str()));
    }

    fn info(&self) {
        // stub
        fail!("info not yet implemented");
    }

    fn install(&self, workspace: &Path, id: &PkgId)  {
        // FIXME #7402: Use RUST_PATH to determine target dir
        // Also should use workcache to not build if not necessary.
        self.build(workspace, id);
        debug!("install: workspace = %s, id = %s", workspace.to_str(),
               id.to_str());
        self.install_no_build(workspace, id);

    }

    fn install_no_build(&self, workspace: &Path, id: &PkgId) {
        use conditions::copy_failed::cond;

        // Now copy stuff into the install dirs
        let maybe_executable = built_executable_in_workspace(id, workspace);
        let maybe_library = built_library_in_workspace(id, workspace);
        let target_exec = target_executable_in_workspace(id, workspace);
        let target_lib = maybe_library.map(|_p| target_library_in_workspace(id, workspace));

        debug!("target_exec = %s target_lib = %? \
                maybe_executable = %? maybe_library = %?",
               target_exec.to_str(), target_lib,
               maybe_executable, maybe_library);

        for exec in maybe_executable.iter() {
            debug!("Copying: %s -> %s", exec.to_str(), target_exec.to_str());
            if !(os::mkdir_recursive(&target_exec.dir_path(), U_RWX) &&
                 os::copy_file(exec, &target_exec)) {
                cond.raise(((*exec).clone(), target_exec.clone()));
            }
        }
        for lib in maybe_library.iter() {
            let target_lib = target_lib.clone().expect(fmt!("I built %s but apparently \
                                                didn't install it!", lib.to_str()));
            let target_lib = target_lib.pop().push(lib.filename().expect("weird target lib"));
            debug!("Copying: %s -> %s", lib.to_str(), target_lib.to_str());
            if !(os::mkdir_recursive(&target_lib.dir_path(), U_RWX) &&
                 os::copy_file(lib, &target_lib)) {
                cond.raise(((*lib).clone(), target_lib.clone()));
            }
        }
    }

    fn prefer(&self, _id: &str, _vers: Option<~str>)  {
        fail!("prefer not yet implemented");
    }

    fn test(&self)  {
        // stub
        fail!("test not yet implemented");
    }

    fn uninstall(&self, _id: &str, _vers: Option<~str>)  {
        fail!("uninstall not yet implemented");
    }

    fn unprefer(&self, _id: &str, _vers: Option<~str>)  {
        fail!("unprefer not yet implemented");
    }
}


pub fn main() {
    io::println("WARNING: The Rust package manager is experimental and may be unstable");

    let args = os::args();
    let opts = ~[getopts::optflag("h"), getopts::optflag("help"),
                 getopts::optflag("j"), getopts::optflag("json"),
                 getopts::optmulti("c"), getopts::optmulti("cfg")];
    let matches = &match getopts::getopts(args, opts) {
        result::Ok(m) => m,
        result::Err(f) => {
            error(fmt!("%s", getopts::fail_str(f)));

            return;
        }
    };
    let help = getopts::opt_present(matches, "h") ||
               getopts::opt_present(matches, "help");
    let json = getopts::opt_present(matches, "j") ||
               getopts::opt_present(matches, "json");
    let mut args = matches.free.clone();

    args.shift();

    if (args.len() < 1) {
        return usage::general();
    }

    let cmd = args.shift();

    if !util::is_cmd(cmd) {
        return usage::general();
    } else if help {
        return match cmd {
            ~"build" => usage::build(),
            ~"clean" => usage::clean(),
            ~"do" => usage::do_cmd(),
            ~"info" => usage::info(),
            ~"install" => usage::install(),
            ~"list"    => usage::list(),
            ~"prefer" => usage::prefer(),
            ~"test" => usage::test(),
            ~"uninstall" => usage::uninstall(),
            ~"unprefer" => usage::unprefer(),
            _ => usage::general()
        };
    }

    let sroot = Some(@filesearch::get_or_default_sysroot());
    debug!("Using sysroot: %?", sroot);
    Ctx {
        sysroot_opt: sroot, // Currently, only tests override this
        json: json,
        dep_cache: @mut HashMap::new()
    }.run(cmd, args);
}

/**
 * Get the working directory of the package script.
 * Assumes that the package script has been compiled
 * in is the working directory.
 */
pub fn work_dir() -> Path {
    os::self_exe_path().unwrap()
}

/**
 * Get the source directory of the package (i.e.
 * where the crates are located). Assumes
 * that the cwd is changed to it before
 * running this executable.
 */
pub fn src_dir() -> Path {
    os::getcwd()
}
