use super::*;

pub(crate) trait CommandExt {
  fn export(
    &mut self,
    settings: &Settings,
    dotenv: &BTreeMap<String, String>,
    scope: &Scope,
    unexports: &HashSet<String>,
  ) -> &mut Command;

  fn export_scope(&mut self, settings: &Settings, scope: &Scope, unexports: &HashSet<String>);

  fn output_guard(self) -> (io::Result<process::Output>, Option<Signal>);

  fn output_guard_stdout(self) -> Result<String, OutputError>;

  fn resolve(program: impl AsRef<OsStr>) -> Command;

  fn status_guard(self) -> (io::Result<ExitStatus>, Option<Signal>);
}

impl CommandExt for Command {
  fn export(
    &mut self,
    settings: &Settings,
    dotenv: &BTreeMap<String, String>,
    scope: &Scope,
    unexports: &HashSet<String>,
  ) -> &mut Command {
    for (name, value) in dotenv {
      self.env(name, value);
    }

    if let Some(parent) = scope.parent() {
      self.export_scope(settings, parent, unexports);
    }

    self
  }

  fn export_scope(&mut self, settings: &Settings, scope: &Scope, unexports: &HashSet<String>) {
    if let Some(parent) = scope.parent() {
      self.export_scope(settings, parent, unexports);
    }

    for unexport in unexports {
      self.env_remove(unexport);
    }

    for binding in scope.bindings() {
      if binding.export || (settings.export && !binding.prelude) {
        self.env(binding.name.lexeme(), &binding.value);
      }
    }
  }

  fn output_guard(self) -> (io::Result<process::Output>, Option<Signal>) {
    SignalHandler::spawn(self, process::Child::wait_with_output)
  }

  fn output_guard_stdout(self) -> Result<String, OutputError> {
    let (result, caught) = self.output_guard();

    let output = result.map_err(OutputError::Io)?;

    OutputError::result_from_exit_status(output.status)?;

    let output = str::from_utf8(&output.stdout).map_err(OutputError::Utf8)?;

    if let Some(signal) = caught {
      return Err(OutputError::Interrupted(signal));
    }

    Ok(
      output
        .strip_suffix("\r\n")
        .or_else(|| output.strip_suffix("\n"))
        .unwrap_or(output)
        .into(),
    )
  }

  fn resolve(program: impl AsRef<OsStr>) -> Self {
    let program = Path::new(program.as_ref());

    if !cfg!(windows) {
      return Self::new(program);
    }

    let mut candidates = vec![program.into()];

    let mut components = program.components();
    if matches!(components.next(), Some(Component::Normal(_))) && components.next().is_none() {
      if let Some(path) = env::var_os("PATH") {
        for path in env::split_paths(&path) {
          candidates.push(path.join(program));
        }
      }
    }

    let extensions = if program.extension().is_none() {
      let pathext = env::var_os("PATHEXT")
        .unwrap_or(".COM;.EXE;.BAT;.CMD".into())
        .to_string_lossy()
        .into_owned();
      let mut extensions = Vec::new();
      for extension in pathext.split(';') {
        if let Some(extension) = extension.strip_prefix('.') {
          extensions.push(extension.to_owned());
        }
      }
      Some(extensions)
    } else {
      None
    };

    for candidate in candidates {
      if let Some(extensions) = &extensions {
        for extension in extensions {
          let path = candidate.with_extension(extension);
          if path.is_file() {
            return Self::new(path);
          }
        }
      } else if candidate.is_file() {
        return Self::new(candidate);
      }
    }

    Self::new(program)
  }

  fn status_guard(self) -> (io::Result<ExitStatus>, Option<Signal>) {
    SignalHandler::spawn(self, |mut child| child.wait())
  }
}
