// License: see LICENSE file at root directory of `master` branch

//! # R1

#![warn(missing_docs)]
#![cfg(unix)]

mod notification;

use {
    std::{
        borrow::Cow,
        io::{Error, ErrorKind},
        path::PathBuf,
        process::{self, Command},
    },

    dia_args::{
        Args,
        paths::{self, PathKind, TakeOption},
    },
    namaste::Namaste,
    r1::Result,
    zeros::Hash,

    self::notification::Notification,
};

const CMD_HELP: &str = "help";
const CMD_HELP_DOCS: Cow<str> = Cow::Borrowed("Prints help and exits.");

const CMD_VERSION: &str = "version";
const CMD_VERSION_DOCS: Cow<str> = Cow::Borrowed("Prints version and exits.");

const ARG_TARGET: &[&str] = &["--target"];

const ARG_APP_NAME: &[&str] = &["--app-name"];
const ARG_APP_NAME_DOCS: Cow<str> = Cow::Borrowed(concat!(
    "App name. It can be used as notification title. If not provided, full app path will be used.",
));

const ARG_APP_ICON: &[&str] = &["--app-icon"];
const ARG_APP_ICON_DOCS: Cow<str> = Cow::Borrowed(concat!("Full path to app icon."));

const ARG_DEBUG: &[&str] = &["--debug"];
const ARG_DEBUG_DOCS: Cow<str> = Cow::Borrowed("Debug mode.");
const ARG_DEBUG_DEFAULT: bool = false;

/// # Main
fn main() {
    if let Err(err) = run() {
        eprintln!("{}", err);
        process::exit(1);
    }
}

/// # Runs the program
fn run() -> Result<()> {
    let args = dia_args::parse()?;
    match args.cmd() {
        Some(CMD_HELP) => {
            ensure_args_are_empty(args.into_sub_cmd().1)?;
            print_help()
        },
        Some(CMD_VERSION) => {
            ensure_args_are_empty(args.into_sub_cmd().1)?;
            print_version()
        },
        Some(other) => Err(Error::new(ErrorKind::InvalidInput, format!("Unknown command: {:?}", other))),
        None => run_main_job(args),
    }
}

/// # Ensures arguments are empty
fn ensure_args_are_empty<A>(args: A) -> Result<()> where A: AsRef<Args> {
    let args = args.as_ref();
    match args.is_empty() {
        true => Ok(()),
        false => Err(Error::new(ErrorKind::InvalidInput, format!("Unknown arguments: {:?}", args))),
    }
}

/// # Makes version string
fn make_version_string<'a>() -> Cow<'a, str> {
    format!("{} {} {:?}", r1::NAME, r1::VERSION, r1::RELEASE_DATE).into()
}

/// # Prints version
fn print_version() -> Result<()> {
    println!("{}", make_version_string());
    Ok(())
}

/// # Prints help
fn print_help() -> Result<()> {
    use dia_args::docs::{Cmd, Docs, Opt, Project};

    let docs = Cow::Owned(format!(
        include_str!("args/program-docs"),
        program_name=r1::CODE_NAME, dia_args_file_format=dia_args::DIA_ARGS_FILE_FORMAT, arg_target=ARG_TARGET,
    ));

    let options = Some(dia_args::make_opts![
        Opt::new(ARG_TARGET, false, None, None, Cow::Owned(format!(include_str!("args/arg_target"), program_name=r1::CODE_NAME))),
        Opt::new(ARG_APP_NAME, false, None, None, ARG_APP_NAME_DOCS),
        Opt::new(ARG_APP_ICON, false, None, None, ARG_APP_ICON_DOCS),
        Opt::new(ARG_DEBUG, false, None, Some(&ARG_DEBUG_DEFAULT), ARG_DEBUG_DOCS),
    ]);

    let commands = Some(dia_args::make_cmds![
        Cmd::new(CMD_HELP, CMD_HELP_DOCS, None),
        Cmd::new(CMD_VERSION, CMD_VERSION_DOCS, None),
    ]);

    let mut docs = Docs::new(make_version_string(), docs);
    docs.options = options;
    docs.commands = commands;
    docs.project = {
        let license = include_str!("../../../LICENSE");
        Some(Project::new("https://bitbucket.org/haibison/r1", license.lines().next().unwrap(), Some(Cow::Borrowed(license))))
    };
    docs.print();

    Ok(())
}

/// # Runs main job
fn run_main_job(mut args: Args) -> Result<()> {
    let app_name = args.take::<String>(ARG_APP_NAME)?;
    let app_icon = args.take::<String>(ARG_APP_ICON)?;
    let (app, app_args, namaste_id) = take_app_and_make_namaste_id(&mut args)?;
    let debug = args.take(ARG_DEBUG)?.unwrap_or(ARG_DEBUG_DEFAULT);

    ensure_args_are_empty(args)?;

    let notification = ro::new(Notification {
        app_name: app_name.or_else(|| Some(app.display().to_string())),
        app_icon,
    });

    let namaste = match unsafe {
        Namaste::make(&namaste_id)
    } {
        Ok(namaste) => namaste,
        Err(_) => {
            let mut msg = String::from("Another instance is running");
            if debug {
                msg += &format!(" (at: {})", zeros::bytes_to_hex(&namaste_id[..]));
            }
            return Err(notification.show_failure(msg));
        },
    };

    let mut cmd = Command::new(&app);
    app_args.map_or((), |app_args| app_args.into_iter().for_each(|a| drop(cmd.arg(a))));

    match cmd.spawn() {
        Ok(mut proc) => {
            notification.show_success("Started");
            match proc.wait() {
                Ok(status) => match status.success() {
                    true => Ok(drop(namaste)),
                    false => Err(notification.show_failure(format!("{:?} returned: {:?}", app, status))),
                },
                Err(err) => Err(notification.show_failure(format!("Failed running {:?}: {:?}", app, err))),
            }
        },
        Err(err) => Err(notification.show_failure(format!("Failed starting {:?}: {:?}", app, err))),
    }
}

/// # Takes app, app arguments and makes Namaste ID
fn take_app_and_make_namaste_id(args: &mut Args) -> Result<(PathBuf, Option<Vec<String>>, Vec<u8>)> {
    use std::os::unix::ffi::OsStringExt;

    let mut keccak = Hash::Sha3_512.new_keccak();
    keccak.update(r1::ID);

    let (app, app_args) = match paths::take_path(args, ARG_TARGET, PathKind::File, TakeOption::MustExist)? {
        Some(app) => (app.canonicalize()?, args.take_sub_args()),
        None => match args.take_sub_args() {
            Some(mut sub_args) => {
                let app = paths::handle_path(PathBuf::from(sub_args.remove(0)), PathKind::File, TakeOption::MustExist)?.canonicalize()?;
                sub_args.iter().for_each(|s| keccak.update(s));
                (app, Some(sub_args))
            },
            _ => return Err(Error::new(ErrorKind::InvalidInput, "Missing command to run")),
        },
    };

    keccak.update(OsStringExt::into_vec(app.clone().into_os_string()));

    Ok((app, app_args, keccak.finish()))
}
