use std::{
    collections::HashMap,
    path::PathBuf,
    sync::{Arc, Mutex},
};

use shared_child::SharedChild;

type ChildStore = Arc<Mutex<HashMap<u32, Arc<SharedChild>>>>;

pub fn restart(env: &Env) {
    use std::process::{Command, exit};

    if let Ok(path) = current_binary(env) {
        Command::new(path).args(&env.args).spawn().expect("application failed to start");
    }
    exit(0);
}

pub fn current_binary(_env: &Env) -> std::io::Result<PathBuf> {
    // if we are running from an AppImage, we ONLY want the set AppImage path
    #[cfg(target_os = "linux")]
    if let Some(app_image_path) = &_env.appimage {
        return Ok(PathBuf::from(app_image_path));
    }

    current_exe()
}

pub fn current_exe() -> std::io::Result<PathBuf> {
    starting_binary::STARTING_BINARY.cloned()
}

/// Information about environment variables.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Env {
    /// The APPIMAGE environment variable.
    #[cfg(target_os = "linux")]
    pub appimage: Option<std::ffi::OsString>,
    /// The APPDIR environment variable.
    #[cfg(target_os = "linux")]
    pub appdir: Option<std::ffi::OsString>,
    /// The command line arguments of the current process.
    pub args: Vec<String>,
}

#[allow(clippy::derivable_impls)]
impl Default for Env {
    fn default() -> Self {
        let args = std::env::args().skip(1).collect();
        #[cfg(target_os = "linux")]
        {
            let env = Self {
                #[cfg(target_os = "linux")]
                appimage: std::env::var_os("APPIMAGE"),
                #[cfg(target_os = "linux")]
                appdir: std::env::var_os("APPDIR"),
                args,
            };
            if env.appimage.is_some() || env.appdir.is_some() {
                // validate that we're actually running on an AppImage
                // an AppImage is mounted to `/$TEMPDIR/.mount_${appPrefix}${hash}`
                // see https://github.com/AppImage/AppImageKit/blob/1681fd84dbe09c7d9b22e13cdb16ea601aa0ec47/src/runtime.c#L501
                // note that it is safe to use `std::env::current_exe` here since we just loaded an AppImage.
                let is_temp = std::env::current_exe()
                    .map(|p| p.display().to_string().starts_with(&format!("{}/.mount_", std::env::temp_dir().display())))
                    .unwrap_or(true);

                if !is_temp {
                    panic!("`APPDIR` or `APPIMAGE` environment variable found but this application was not detected as an AppImage; this might be a security issue.");
                }
            }
            env
        }
        #[cfg(not(target_os = "linux"))]
        {
            Self { args }
        }
    }
}

mod starting_binary {
    use ctor::ctor;
    use std::path::PathBuf;
    use std::{
        io::{Error, ErrorKind, Result},
        path::Path,
    };

    #[ctor]
    #[used]
    pub(super) static STARTING_BINARY: StartingBinary = { StartingBinary::new() };

    /// Represents a binary path that was cached when the program was loaded.
    pub(super) struct StartingBinary(std::io::Result<PathBuf>);

    impl StartingBinary {
        /// Find the starting executable as safely as possible.
        fn new() -> Self {
            // see notes on current_exe() for security implications
            let dangerous_path = match std::env::current_exe() {
                Ok(dangerous_path) => dangerous_path,
                error @ Err(_) => return Self(error),
            };

            // note: this only checks symlinks on problematic platforms, see implementation below
            if let Some(symlink) = Self::has_symlink(&dangerous_path) {
                return Self(Err(Error::new(
                    ErrorKind::InvalidData,
                    format!("StartingBinary found current_exe() that contains a symlink on a non-allowed platform: {}", symlink.display()),
                )));
            }

            // we canonicalize the path to resolve any symlinks to the real exe path
            Self(dangerous_path.canonicalize())
        }

        /// A clone of the [`PathBuf`] found to be the starting path.
        ///
        /// Because [`Error`] is not clone-able, it is recreated instead.
        pub(super) fn cloned(&self) -> Result<PathBuf> {
            self.0.as_ref().map(Clone::clone).map_err(|e| Error::new(e.kind(), e.to_string()))
        }

        /// We only care about checking this on macOS currently, as it has the least symlink protections.
        #[cfg(any(not(target_os = "macos"), feature = "process-relaunch-dangerous-allow-symlink-macos"))]
        fn has_symlink(_: &Path) -> Option<&Path> {
            None
        }

        /// We only care about checking this on macOS currently, as it has the least symlink protections.
        #[cfg(all(target_os = "macos", not(feature = "process-relaunch-dangerous-allow-symlink-macos")))]
        fn has_symlink(path: &Path) -> Option<&Path> {
            path.ancestors()
                .find(|ancestor| matches!(ancestor.symlink_metadata().as_ref().map(std::fs::Metadata::file_type).as_ref().map(std::fs::FileType::is_symlink), Ok(true)))
        }
    }
}

mod process {
    // Copyright 2019-2022 Tauri Programme within The Commons Conservancy
    // SPDX-License-Identifier: Apache-2.0
    // SPDX-License-Identifier: MIT

    use std::{
        collections::HashMap,
        io::{BufReader, Write},
        path::PathBuf,
        process::{Command as StdCommand, Stdio},
        sync::{
            Arc, Mutex, RwLock,
            mpsc::{Receiver, Sender, channel},
        },
        thread::spawn,
    };

    #[cfg(unix)]
    use std::os::unix::process::ExitStatusExt;
    #[cfg(windows)]
    use std::os::windows::process::CommandExt;

    #[cfg(windows)]
    const CREATE_NO_WINDOW: u32 = 0x0800_0000;

    // use tokio::task::{block_on as block_on_task, channel, Receiver, Sender};
    pub use encoding_rs::Encoding;
    use os_pipe::{PipeReader, PipeWriter, pipe};
    use serde::Serialize;
    use shared_child::SharedChild;
    use tokio::task::spawn_blocking as block_on_task;

    use super::current_exe;

    type ChildStore = Arc<Mutex<HashMap<u32, Arc<SharedChild>>>>;

    fn commands() -> &'static ChildStore {
        use once_cell::sync::Lazy;
        static STORE: Lazy<ChildStore> = Lazy::new(Default::default);
        &STORE
    }

    /// Kills all child processes created with [`Command`].
    /// By default it's called before the [`crate::App`] exits.
    pub fn kill_children() {
        let commands = commands().lock().unwrap();
        let children = commands.values();
        for child in children {
            let _ = child.kill();
        }
    }

    /// Payload for the [`CommandEvent::Terminated`] command event.
    #[derive(Debug, Clone, Serialize)]
    pub struct TerminatedPayload {
        /// Exit code of the process.
        pub code: Option<i32>,
        /// If the process was terminated by a signal, represents that signal.
        pub signal: Option<i32>,
    }

    /// A event sent to the command callback.
    #[derive(Debug, Clone, Serialize)]
    #[serde(tag = "event", content = "payload")]
    #[non_exhaustive]
    pub enum CommandEvent {
        /// Stderr bytes until a newline (\n) or carriage return (\r) is found.
        Stderr(String),
        /// Stdout bytes until a newline (\n) or carriage return (\r) is found.
        Stdout(String),
        /// An error happened waiting for the command to finish or converting the stdout/stderr bytes to an UTF-8 string.
        Error(String),
        /// Command process terminated.
        Terminated(TerminatedPayload),
    }

    /// The type to spawn commands.
    #[derive(Debug)]
    pub struct Command {
        program: String,
        args: Vec<String>,
        env_clear: bool,
        env: HashMap<String, String>,
        current_dir: Option<PathBuf>,
        encoding: Option<&'static Encoding>,
    }

    /// Spawned child process.
    #[derive(Debug)]
    pub struct CommandChild {
        inner: Arc<SharedChild>,
        stdin_writer: PipeWriter,
    }

    impl CommandChild {
        /// Writes to process stdin.
        pub fn write(&mut self, buf: &[u8]) -> crate::IResult<()> {
            self.stdin_writer.write_all(buf)?;
            Ok(())
        }

        /// Sends a kill signal to the child.
        pub fn kill(self) -> crate::IResult<()> {
            self.inner.kill()?;
            Ok(())
        }

        /// Returns the process pid.
        pub fn pid(&self) -> u32 {
            self.inner.id()
        }
    }

    /// Describes the result of a process after it has terminated.
    #[derive(Debug)]
    pub struct ExitStatus {
        code: Option<i32>,
    }

    impl ExitStatus {
        /// Returns the exit code of the process, if any.
        pub fn code(&self) -> Option<i32> {
            self.code
        }

        /// Returns true if exit status is zero. Signal termination is not considered a success, and success is defined as a zero exit status.
        pub fn success(&self) -> bool {
            self.code == Some(0)
        }
    }

    /// The output of a finished process.
    #[derive(Debug)]
    pub struct Output {
        /// The status (exit code) of the process.
        pub status: ExitStatus,
        /// The data that the process wrote to stdout.
        pub stdout: String,
        /// The data that the process wrote to stderr.
        pub stderr: String,
    }

    fn relative_command_path(command: String) -> crate::IResult<String> {
        match current_exe()?.parent() {
            #[cfg(windows)]
            Some(exe_dir) => Ok(format!("{}\\{command}.exe", exe_dir.display())),
            #[cfg(not(windows))]
            Some(exe_dir) => Ok(format!("{}/{command}", exe_dir.display())),
            None => Err("Could not evaluate executable dir".into()),
        }
    }

    impl From<Command> for StdCommand {
        fn from(cmd: Command) -> StdCommand {
            let mut command = StdCommand::new(cmd.program);
            command.args(cmd.args);
            command.stdout(Stdio::piped());
            command.stdin(Stdio::piped());
            command.stderr(Stdio::piped());
            if cmd.env_clear {
                command.env_clear();
            }
            command.envs(cmd.env);
            if let Some(current_dir) = cmd.current_dir {
                command.current_dir(current_dir);
            }
            #[cfg(windows)]
            command.creation_flags(CREATE_NO_WINDOW);
            command
        }
    }

    impl Command {
        /// Creates a new Command for launching the given program.
        pub fn new<S: Into<String>>(program: S) -> Self {
            Self {
                program: program.into(),
                args: Default::default(),
                env_clear: false,
                env: Default::default(),
                current_dir: None,
                encoding: None,
            }
        }

        /// Creates a new Command for launching the given sidecar program.
        ///
        /// A sidecar program is a embedded external binary in order to make your application work
        /// or to prevent users having to install additional dependencies (e.g. Node.js, Python, etc).
        pub fn new_sidecar<S: Into<String>>(program: S) -> crate::IResult<Self> {
            Ok(Self::new(relative_command_path(program.into())?))
        }

        /// Appends arguments to the command.
        #[must_use]
        pub fn args<I, S>(mut self, args: I) -> Self
        where
            I: IntoIterator<Item = S>,
            S: AsRef<str>,
        {
            for arg in args {
                self.args.push(arg.as_ref().to_string());
            }
            self
        }

        /// Clears the entire environment map for the child process.
        #[must_use]
        pub fn env_clear(mut self) -> Self {
            self.env_clear = true;
            self
        }

        /// Adds or updates multiple environment variable mappings.
        #[must_use]
        pub fn envs(mut self, env: HashMap<String, String>) -> Self {
            self.env = env;
            self
        }

        /// Sets the working directory for the child process.
        #[must_use]
        pub fn current_dir(mut self, current_dir: PathBuf) -> Self {
            self.current_dir.replace(current_dir);
            self
        }

        /// Sets the character encoding for stdout/stderr.
        #[must_use]
        pub fn encoding(mut self, encoding: &'static Encoding) -> Self {
            self.encoding.replace(encoding);
            self
        }

        /// Spawns the command.
        ///
        /// # Examples
        ///
        /// ```rust,no_run
        /// use tauri::api::process::{Command, CommandEvent};
        /// tauri::async_runtime::spawn(async move {
        ///   let (mut rx, mut child) = Command::new("cargo")
        ///     .args(["tauri", "dev"])
        ///     .spawn()
        ///     .expect("Failed to spawn cargo");
        ///
        ///   let mut i = 0;
        ///   while let Some(event) = rx.recv().await {
        ///     if let CommandEvent::Stdout(line) = event {
        ///       println!("got: {}", line);
        ///       i += 1;
        ///       if i == 4 {
        ///         child.write("message from Rust\n".as_bytes()).unwrap();
        ///         i = 0;
        ///       }
        ///     }
        ///   }
        /// });
        /// ```
        pub fn spawn(self) -> crate::IResult<(Receiver<CommandEvent>, CommandChild)> {
            let encoding = self.encoding;
            let mut command: StdCommand = self.into();
            let (stdout_reader, stdout_writer) = pipe()?;
            let (stderr_reader, stderr_writer) = pipe()?;
            let (stdin_reader, stdin_writer) = pipe()?;
            command.stdout(stdout_writer);
            command.stderr(stderr_writer);
            command.stdin(stdin_reader);

            let shared_child = SharedChild::spawn(&mut command)?;
            let child = Arc::new(shared_child);
            let child_ = child.clone();
            let guard = Arc::new(RwLock::new(()));

            commands().lock().unwrap().insert(child.id(), child.clone());

            let (tx, rx) = channel();

            spawn_pipe_reader(tx.clone(), guard.clone(), stdout_reader, CommandEvent::Stdout, encoding);
            spawn_pipe_reader(tx.clone(), guard.clone(), stderr_reader, CommandEvent::Stderr, encoding);

            spawn(move || {
                let _ = match child_.wait() {
                    Ok(status) => {
                        let _l = guard.write().unwrap();
                        commands().lock().unwrap().remove(&child_.id());
                        block_on_task(move || {
                            tx.send(CommandEvent::Terminated(TerminatedPayload {
                                code: status.code(),
                                #[cfg(windows)]
                                signal: None,
                                #[cfg(unix)]
                                signal: status.signal(),
                            }));
                        })
                    }
                    Err(e) => {
                        let _l = guard.write().unwrap();
                        block_on_task(move || {
                            tx.send(CommandEvent::Error(e.to_string()));
                        })
                    }
                };
            });

            Ok((rx, CommandChild { inner: child, stdin_writer }))
        }

        /// Executes a command as a child process, waiting for it to finish and collecting its exit status.
        /// Stdin, stdout and stderr are ignored.
        ///
        /// # Examples
        /// ```rust,no_run
        /// use tauri::api::process::Command;
        /// let status = Command::new("which").args(["ls"]).status().unwrap();
        /// println!("`which` finished with status: {:?}", status.code());
        /// ```
        pub fn status(self) -> crate::IResult<ExitStatus> {
            let (mut rx, _child) = self.spawn()?;
            let code = {
                let mut code = None;
                #[allow(clippy::collapsible_match)]
                while let Ok(event) = rx.recv() {
                    if let CommandEvent::Terminated(payload) = event {
                        code = payload.code;
                    }
                }
                code
            };
            Ok(ExitStatus { code })
        }

        /// Executes the command as a child process, waiting for it to finish and collecting all of its output.
        /// Stdin is ignored.
        ///
        /// # Examples
        ///
        /// ```rust,no_run
        /// use tauri::api::process::Command;
        /// let output = Command::new("echo").args(["TAURI"]).output().unwrap();
        /// assert!(output.status.success());
        /// assert_eq!(output.stdout, "TAURI");
        /// ```
        pub fn output(self) -> crate::IResult<Output> {
            let (mut rx, _child) = self.spawn()?;

            let output = {
                // block_on_task( move ||{
                let mut code = None;
                let mut stdout = String::new();
                let mut stderr = String::new();
                while let Ok(event) = rx.recv() {
                    match event {
                        CommandEvent::Terminated(payload) => {
                            code = payload.code;
                        }
                        CommandEvent::Stdout(line) => {
                            stdout.push_str(line.as_str());
                            stdout.push('\n');
                        }
                        CommandEvent::Stderr(line) => {
                            stderr.push_str(line.as_str());
                            stderr.push('\n');
                        }
                        CommandEvent::Error(_) => {}
                    }
                }
                Output {
                    status: ExitStatus { code },
                    stdout,
                    stderr,
                }
            };
            // });

            Ok(output)
        }
    }

    use std::io::BufRead;

    /// Read a line breaking in both \n and \r.
    ///
    /// Adapted from <https://doc.rust-lang.org/std/io/trait.BufRead.html#method.read_line>.
    pub fn read_line<R: BufRead + ?Sized>(r: &mut R, buf: &mut Vec<u8>) -> std::io::Result<usize> {
        let mut read = 0;
        loop {
            let (done, used) = {
                let available = match r.fill_buf() {
                    Ok(n) => n,
                    Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
                    Err(e) => return Err(e),
                };
                match memchr::memchr(b'\n', available) {
                    Some(i) => {
                        let end = i + 1;
                        buf.extend_from_slice(&available[..end]);
                        (true, end)
                    }
                    None => match memchr::memchr(b'\r', available) {
                        Some(i) => {
                            let end = i + 1;
                            buf.extend_from_slice(&available[..end]);
                            (true, end)
                        }
                        None => {
                            buf.extend_from_slice(available);
                            (false, available.len())
                        }
                    },
                }
            };
            r.consume(used);
            read += used;
            if done || used == 0 {
                if buf.ends_with(&[b'\n']) {
                    buf.pop();
                }
                return Ok(read);
            }
        }
    }

    fn spawn_pipe_reader<F: Fn(String) -> CommandEvent + Send + Copy + 'static>(
        tx: Sender<CommandEvent>,
        guard: Arc<RwLock<()>>,
        pipe_reader: PipeReader,
        wrapper: F,
        character_encoding: Option<&'static Encoding>,
    ) {
        spawn(move || {
            let _lock = guard.read().unwrap();
            let mut reader = BufReader::new(pipe_reader);

            let mut buf = Vec::new();
            loop {
                buf.clear();
                match read_line(&mut reader, &mut buf) {
                    Ok(n) => {
                        if n == 0 {
                            break;
                        }
                        let tx_ = tx.clone();
                        let line = match character_encoding {
                            Some(encoding) => Ok(encoding.decode_with_bom_removal(&buf).0.into()),
                            None => String::from_utf8(buf.clone()),
                        };
                        block_on_task(move || {
                            let _ = match line {
                                Ok(line) => tx_.send(wrapper(line)),
                                Err(e) => tx_.send(CommandEvent::Error(e.to_string())),
                            };
                        });
                    }
                    Err(e) => {
                        let tx_ = tx.clone();
                        let _ = block_on_task(move || tx_.send(CommandEvent::Error(e.to_string())));
                    }
                }
            }
        });
    }

    // tests for the commands functions.
    #[cfg(test)]
    mod test {
        #[cfg(not(windows))]
        use super::*;

        #[cfg(not(windows))]
        #[test]
        fn test_cmd_output() {
            // create a command to run cat.
            let cmd = Command::new("cat").args(["test/api/test.txt"]);
            let (mut rx, _) = cmd.spawn().unwrap();

            crate::async_runtime::block_on(async move {
                while let Some(event) = rx.recv().await {
                    match event {
                        CommandEvent::Terminated(payload) => {
                            assert_eq!(payload.code, Some(0));
                        }
                        CommandEvent::Stdout(line) => {
                            assert_eq!(line, "This is a test doc!".to_string());
                        }
                        _ => {}
                    }
                }
            });
        }

        #[cfg(not(windows))]
        #[test]
        // test the failure case
        fn test_cmd_fail() {
            let cmd = Command::new("cat").args(["test/api/"]);
            let (mut rx, _) = cmd.spawn().unwrap();

            crate::async_runtime::block_on(async move {
                while let Some(event) = rx.recv().await {
                    match event {
                        CommandEvent::Terminated(payload) => {
                            assert_eq!(payload.code, Some(1));
                        }
                        CommandEvent::Stderr(line) => {
                            assert_eq!(line, "cat: test/api/: Is a directory".to_string());
                        }
                        _ => {}
                    }
                }
            });
        }
    }
}
