// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.

use super::io::ChildStderrResource;
use super::io::ChildStdinResource;
use super::io::ChildStdoutResource;
use super::process::Stdio;
use super::process::StdioOrRid;
use crate::permissions::Permissions;
use deno_core::error::AnyError;
use deno_core::op;
use deno_core::Extension;
use deno_core::OpState;
use deno_core::Resource;
use deno_core::ResourceId;
use deno_core::ZeroCopyBuf;
use serde::Deserialize;
use serde::Serialize;
use std::borrow::Cow;
use std::cell::RefCell;
use std::process::ExitStatus;
use std::rc::Rc;

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

pub fn init() -> Extension {
  Extension::builder()
    .ops(vec![
      op_spawn_child::decl(),
      op_spawn_wait::decl(),
      op_spawn_sync::decl(),
    ])
    .build()
}

struct ChildResource(tokio::process::Child);

impl Resource for ChildResource {
  fn name(&self) -> Cow<str> {
    "child".into()
  }
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SpawnArgs {
  cmd: String,
  args: Vec<String>,
  cwd: Option<String>,
  clear_env: bool,
  env: Vec<(String, String)>,
  #[cfg(unix)]
  gid: Option<u32>,
  #[cfg(unix)]
  uid: Option<u32>,

  #[serde(flatten)]
  stdio: ChildStdio,
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChildStdio {
  stdin: Stdio,
  stdout: Stdio,
  stderr: Stdio,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ChildStatus {
  success: bool,
  code: i32,
  signal: Option<String>,
}

impl TryFrom<ExitStatus> for ChildStatus {
  type Error = AnyError;

  fn try_from(status: ExitStatus) -> Result<Self, Self::Error> {
    let code = status.code();
    #[cfg(unix)]
    let signal = status.signal();
    #[cfg(not(unix))]
    let signal: Option<i32> = None;

    let status = if let Some(signal) = signal {
      ChildStatus {
        success: false,
        code: 128 + signal,
        #[cfg(unix)]
        signal: Some(
          crate::ops::signal::signal_int_to_str(signal)?.to_string(),
        ),
        #[cfg(not(unix))]
        signal: None,
      }
    } else {
      let code = code.expect("Should have either an exit code or a signal.");

      ChildStatus {
        success: code == 0,
        code,
        signal: None,
      }
    };

    Ok(status)
  }
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SpawnOutput {
  status: ChildStatus,
  stdout: Option<ZeroCopyBuf>,
  stderr: Option<ZeroCopyBuf>,
}

fn create_command(
  state: &mut OpState,
  args: SpawnArgs,
) -> Result<std::process::Command, AnyError> {
  super::check_unstable(state, "Deno.spawn");
  state.borrow_mut::<Permissions>().run.check(&args.cmd)?;

  let mut command = std::process::Command::new(args.cmd);
  command.args(args.args);

  if let Some(cwd) = args.cwd {
    command.current_dir(cwd);
  }

  if args.clear_env {
    command.env_clear();
  }
  command.envs(args.env);

  #[cfg(unix)]
  if let Some(gid) = args.gid {
    super::check_unstable(state, "Deno.spawn.gid");
    command.gid(gid);
  }
  #[cfg(unix)]
  if let Some(uid) = args.uid {
    super::check_unstable(state, "Deno.spawn.uid");
    command.uid(uid);
  }
  #[cfg(unix)]
  // TODO(bartlomieju):
  #[allow(clippy::undocumented_unsafe_blocks)]
  unsafe {
    command.pre_exec(|| {
      libc::setgroups(0, std::ptr::null());
      Ok(())
    });
  }

  command.stdin(args.stdio.stdin.as_stdio());
  command.stdout(match args.stdio.stdout {
    Stdio::Inherit => StdioOrRid::Rid(1).as_stdio(state)?,
    value => value.as_stdio(),
  });
  command.stderr(match args.stdio.stderr {
    Stdio::Inherit => StdioOrRid::Rid(2).as_stdio(state)?,
    value => value.as_stdio(),
  });

  Ok(command)
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Child {
  rid: ResourceId,
  pid: u32,
  stdin_rid: Option<ResourceId>,
  stdout_rid: Option<ResourceId>,
  stderr_rid: Option<ResourceId>,
}

#[op]
fn op_spawn_child(
  state: &mut OpState,
  args: SpawnArgs,
) -> Result<Child, AnyError> {
  let mut command = tokio::process::Command::from(create_command(state, args)?);
  // TODO(@crowlkats): allow detaching processes.
  //  currently deno will orphan a process when exiting with an error or Deno.exit()
  // We want to kill child when it's closed
  command.kill_on_drop(true);

  let mut child = command.spawn()?;
  let pid = child.id().expect("Process ID should be set.");

  let stdin_rid = child
    .stdin
    .take()
    .map(|stdin| state.resource_table.add(ChildStdinResource::from(stdin)));

  let stdout_rid = child
    .stdout
    .take()
    .map(|stdout| state.resource_table.add(ChildStdoutResource::from(stdout)));

  let stderr_rid = child
    .stderr
    .take()
    .map(|stderr| state.resource_table.add(ChildStderrResource::from(stderr)));

  let child_rid = state.resource_table.add(ChildResource(child));

  Ok(Child {
    rid: child_rid,
    pid,
    stdin_rid,
    stdout_rid,
    stderr_rid,
  })
}

#[op]
async fn op_spawn_wait(
  state: Rc<RefCell<OpState>>,
  rid: ResourceId,
) -> Result<ChildStatus, AnyError> {
  let resource = state
    .borrow_mut()
    .resource_table
    .take::<ChildResource>(rid)?;
  Rc::try_unwrap(resource)
    .ok()
    .unwrap()
    .0
    .wait()
    .await?
    .try_into()
}

#[op]
fn op_spawn_sync(
  state: &mut OpState,
  args: SpawnArgs,
) -> Result<SpawnOutput, AnyError> {
  let stdout = matches!(args.stdio.stdout, Stdio::Piped);
  let stderr = matches!(args.stdio.stderr, Stdio::Piped);
  let output = create_command(state, args)?.output()?;

  Ok(SpawnOutput {
    status: output.status.try_into()?,
    stdout: if stdout {
      Some(output.stdout.into())
    } else {
      None
    },
    stderr: if stderr {
      Some(output.stderr.into())
    } else {
      None
    },
  })
}