/*
==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--

R1

Copyright (C) 2019-2026  Anonymous

There are several releases over multiple years,
they are listed as ranges, such as: "2019-2026".

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.

::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--
*/

//! # R1

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

use {
    core::borrow::Borrow,
    std::{
        borrow::Cow,
        io::{Error, ErrorKind},
        path::PathBuf,
        process::{self, Command},
    },
    self::notification::Notification,
    dia_args::{
        Args,
        paths::{self, PathKind, TakeOption},
    },
    r1::Result,
    zeros::keccak::Hash,
};

mod notification;

#[cfg(unix)]
mod sway_desktop;

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 CMD_LICENSES: &str = "licenses";
const CMD_LICENSES_DOCS: Cow<str> = Cow::Borrowed("Prints licenses 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.try_into_sub_cmd()?.1)?;
            print_help()
        },
        Some(CMD_VERSION) => {
            ensure_args_are_empty(args.try_into_sub_cmd()?.1)?;
            print_version()
        },
        Some(CMD_LICENSES) => print_licenses(args.try_into_sub_cmd()?.1),
        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: Borrow<Args> {
    let args = args.borrow();
    if args.is_empty() {
        Ok(())
    } else {
        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 licenses
fn print_licenses(args: Args) -> Result<()> {
    ensure_args_are_empty(args)?;

    dia_args::lock_write_out(format!(
        "{copying}\n\n================================================================\n\n{copying_lesser}\n",
        copying=include_str!("../../../COPYING"),
        copying_lesser=include_str!("../../../COPYING.LESSER"),
    ));

    Ok(())
}

/// # Prints help
fn print_help() -> Result<()> {
    use dia_args::docs::{Cmd, Docs, NO_VALUES, Option, 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_options![
        Option::new(ARG_TARGET, false, NO_VALUES, None, Cow::Owned(format!(include_str!("args/arg_target"), program_name=r1::CODE_NAME))),
        Option::new(ARG_APP_NAME, false, NO_VALUES, None, ARG_APP_NAME_DOCS),
        Option::new(ARG_APP_ICON, false, NO_VALUES, None, ARG_APP_ICON_DOCS),
        Option::new(ARG_DEBUG, false, &[], 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),
        Cmd::new(CMD_LICENSES, CMD_LICENSES_DOCS, None),
    ]);

    let mut docs = Docs::new(make_version_string(), docs);
    docs.options = options;
    docs.commands = commands;
    docs.project = Some(Project::new(
        None,
        "GNU Lesser General Public License, either version 3, or (at your option) any later version",
        None,
    ));
    docs.print()
}

/// # 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 namaste::make(&namaste_id) {
        Ok(namaste) => namaste,
        Err(_) => {
            #[cfg(unix)]
            sway_desktop::try_to_focus_last_proc(&namaste_id)?;
            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);
    for a in app_args {
        cmd.arg(a);
    }

    match cmd.spawn() {
        Ok(mut proc) => {
            #[cfg(unix)]
            sway_desktop::spawn_focus_thread_if_needed(namaste_id, proc.id());
            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, 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 => {
            let mut sub_args = args.take_sub_args();
            match sub_args.len() {
                usize::MIN => return Err(Error::new(ErrorKind::InvalidInput, "Missing command to run")),
                _ => {
                    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, sub_args)
                },
            }
        },
    };

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

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