#pragma once
#include <ext/stdio_filebuf.h>
#include <sys/wait.h>
#include <torch/csrc/profiler/unwind/unwind_error.h>
#include <unistd.h>
#include <memory>

namespace torch::unwind {
// helper to open a process with stdin/stdout/stderr streams.
struct Communicate {
  Communicate(const char* command, const char** args) {
    if (pipe(inpipe_) < 0 || pipe(outpipe_) < 0 || pipe(errpipe_) < 0) {
      throw UnwindError("pipe() failed");
    }
    pid_t pid = fork();
    if (pid < 0) {
      throw UnwindError("fork() failed");
    } else if (pid == 0) { // child process
      close(inpipe_[1]);
      close(outpipe_[0]);
      close(errpipe_[0]);

      dup2(inpipe_[0], STDIN_FILENO);
      dup2(outpipe_[1], STDOUT_FILENO);
      dup2(errpipe_[1], STDERR_FILENO);
      execvp(command, (char* const*)args);
      throw UnwindError("failed execvp");
    } else { // parent process
      close(inpipe_[0]);
      close(outpipe_[1]);
      close(errpipe_[1]);
      outbuf_.reset(
          new __gnu_cxx::stdio_filebuf<char>(inpipe_[1], std::ios::out));
      inbuf_.reset(
          new __gnu_cxx::stdio_filebuf<char>(outpipe_[0], std::ios::in));
      errbuf_.reset(
          new __gnu_cxx::stdio_filebuf<char>(errpipe_[0], std::ios::in));
      in_.reset(new std::istream(inbuf_.get()));
      out_.reset(new std::ostream(outbuf_.get()));
      err_.reset(new std::ostream(errbuf_.get()));
    }
  }
  ~Communicate() {
    close(inpipe_[1]);
    close(outpipe_[0]);
    close(errpipe_[0]);
  }
  std::ostream& out() {
    return *out_;
  }
  std::ostream& err() {
    return *err_;
  }
  std::istream& in() {
    return *in_;
  }

 private:
  int inpipe_[2];
  int outpipe_[2];
  int errpipe_[2];
  std::unique_ptr<__gnu_cxx::stdio_filebuf<char>> outbuf_, inbuf_, errbuf_;
  std::unique_ptr<std::istream> in_;
  std::unique_ptr<std::ostream> out_;
  std::unique_ptr<std::ostream> err_;
};

} // namespace torch::unwind
