//
// process/stdio.hpp
// ~~~~~~~~
//
// Copyright (c) 2021 Klemens D. Morgenstern (klemens dot morgenstern at gmx dot net)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)

#ifndef BOOST_PROCESS_V2_STDIO_HPP
#define BOOST_PROCESS_V2_STDIO_HPP

#include <boost/process/v2/detail/config.hpp>
#include <boost/process/v2/default_launcher.hpp>

#if defined(BOOST_PROCESS_V2_STANDALONE)
#include <asio/connect_pipe.hpp>
#else
#include <boost/asio/connect_pipe.hpp>
#endif

#if defined(BOOST_PROCESS_V2_POSIX)
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#endif

BOOST_PROCESS_V2_BEGIN_NAMESPACE
namespace detail
{
#if defined(BOOST_PROCESS_V2_WINDOWS)

extern "C" intptr_t _get_osfhandle(int fd);

struct handle_closer
{
  handle_closer() = default;
  handle_closer(bool close) : close(close) {}
  handle_closer(DWORD flags) : close(false), flags{flags} {}


  void operator()(HANDLE h) const
  {
    if (close)
      ::CloseHandle(h);
    else if (flags != 0xFFFFFFFFu)
      ::SetHandleInformation(h, 0xFFFFFFFFu, flags);

  }

  bool close{false};
  DWORD flags{0xFFFFFFFFu};
};

template<DWORD Io>
struct process_io_binding
{
  HANDLE prepare()
  {
    auto hh =  h.get();
    ::SetHandleInformation(hh, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
    return hh;
  }

  std::unique_ptr<void, handle_closer> h{::GetStdHandle(Io), false};

  static DWORD get_flags(HANDLE h)
  {
    DWORD res;
    if (!::GetHandleInformation(h, &res))
      detail::throw_last_error("get_flags");
    return res;
  }

  process_io_binding() = default;

  template<typename Stream>
  process_io_binding(Stream && str, decltype(std::declval<Stream>().native_handle()) = nullptr)
      : process_io_binding(str.native_handle())
  {}

  process_io_binding(FILE * f) : process_io_binding(_get_osfhandle(_fileno(f))) {}
  process_io_binding(HANDLE h) : h{h, get_flags(h)} {}
  process_io_binding(std::nullptr_t) : process_io_binding(filesystem::path("NUL")) {}
  process_io_binding(const filesystem::path & pth)
    : h(::CreateFileW(
        pth.c_str(),
        Io == STD_INPUT_HANDLE ? GENERIC_READ : GENERIC_WRITE,
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        nullptr,
        OPEN_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,
        nullptr
        ), true)
  {
  }


  template<typename Executor>
  process_io_binding(BOOST_PROCESS_V2_ASIO_NAMESPACE::basic_readable_pipe<Executor> & readable_pipe,
                     typename std::enable_if<Target != STD_INPUT_HANDLE, Executor*>::type = 0)
  {
    BOOST_PROCESS_V2_ASIO_NAMESPACE::detail::native_pipe_handle p[2];
    BOOST_PROCESS_V2_ASIO_NAMESPACE::detail::create_pipe(p, ec);
    if (ec)
      return ;
    h = std::unique_ptr<void, handle_closer>{p[1], true};
    readable_pipe.assign(p[0], ec);
  }


  template<typename Executor>
  process_io_binding(BOOST_PROCESS_V2_ASIO_NAMESPACE::basic_writable_pipe<Executor> & writable_pipe,
                     typename std::enable_if<Target == STD_INPUT_HANDLE, Executor*>::type = 0)
  {
    BOOST_PROCESS_V2_ASIO_NAMESPACE::detail::native_pipe_handle p[2];
    BOOST_PROCESS_V2_ASIO_NAMESPACE::detail::create_pipe(p, ec);
    if (ec)
      return ;
    h = std::unique_ptr<void, handle_closer>{p[0], true};
    writable_pipe.assign(p[1], ec);
  }
};

typedef process_io_binding<STD_INPUT_HANDLE>  process_input_binding;
typedef process_io_binding<STD_OUTPUT_HANDLE> process_output_binding;
typedef process_io_binding<STD_ERROR_HANDLE>  process_error_binding;

#else

template<int Target>
struct process_io_binding
{
  constexpr static int target = Target;
  int fd{target};
  bool fd_needs_closing{false};
  error_code ec;

  ~process_io_binding()
  {
    if (fd_needs_closing)
      ::close(fd);
  }

  process_io_binding() = default;

  template<typename Stream>
  process_io_binding(Stream && str, decltype(std::declval<Stream>().native_handle()) = -1)
          : process_io_binding(str.native_handle())
  {}

  process_io_binding(FILE * f) : process_io_binding(fileno(f)) {}
  process_io_binding(int fd) : fd(fd) {}
  process_io_binding(std::nullptr_t) : process_io_binding(filesystem::path("/dev/null")) {}
  process_io_binding(const filesystem::path & pth)
          : fd(::open(pth.c_str(),
                      Target == STDIN_FILENO ? O_RDONLY : (O_WRONLY | O_CREAT),
                      0660)), fd_needs_closing(true)
  {
  }

  template<typename Executor>
  process_io_binding(BOOST_PROCESS_V2_ASIO_NAMESPACE::basic_readable_pipe<Executor> & readable_pipe,
                     typename std::enable_if<Target != STDIN_FILENO, Executor*>::type = 0)
  {
    BOOST_PROCESS_V2_ASIO_NAMESPACE::detail::native_pipe_handle p[2];
    BOOST_PROCESS_V2_ASIO_NAMESPACE::detail::create_pipe(p, ec);
    if (ec)
      return ;
    fd = p[1];
    if (::fcntl(p[0], F_SETFD, FD_CLOEXEC) == -1)
    {
      ec = detail::get_last_error();
      return ;
    }
    fd_needs_closing = true;
    readable_pipe.assign(p[0], ec);
  }


  template<typename Executor>
  process_io_binding(BOOST_PROCESS_V2_ASIO_NAMESPACE::basic_writable_pipe<Executor> & writable_pipe,
                     typename std::enable_if<Target == STDIN_FILENO, Executor*>::type = 0)
  {
    BOOST_PROCESS_V2_ASIO_NAMESPACE::detail::native_pipe_handle p[2];
    BOOST_PROCESS_V2_ASIO_NAMESPACE::detail::create_pipe(p, ec);
    if (ec)
      return ;
    fd = p[0];
    if (::fcntl(p[1], F_SETFD, FD_CLOEXEC) == -1)
    {
      ec = detail::get_last_error();
      return ;
    }
    fd_needs_closing = true;
    writable_pipe.assign(p[1], ec);
  }

  error_code on_setup(posix::default_launcher &,
                      const filesystem::path &, const char * const *)
  {
      return ec;
  }

  error_code on_exec_setup(posix::default_launcher & launcher,
                           const filesystem::path &, const char * const *)
  {
    if (::dup2(fd, target) == -1)
      return get_last_error();
    else
      return error_code();
  }
};

typedef process_io_binding<STDIN_FILENO>  process_input_binding;
typedef process_io_binding<STDOUT_FILENO> process_output_binding;
typedef process_io_binding<STDERR_FILENO> process_error_binding;

#endif

}


/// The initializer for the stdio of a subprocess
/** The subprocess initializer has three members:
 * 
 *  - in for stdin
 *  - out for stdout
 *  - err for stderr
 * 
 * If the initializer is present all three will be set for the subprocess.
 * By default they will inherit the stdio handles from the parent process. 
 * This means that this will forward stdio to the subprocess:
 * 
 * @code {.cpp}
 * asio::io_context ctx;
 * v2::process proc(ctx, "/bin/bash", {}, v2::process_stdio{});
 * @endcode
 * 
 * No constructors are provided in order to support designated initializers
 * in later version of C++.
 * 
 * * @code {.cpp}
 * asio::io_context ctx;
 * /// C++17
 * v2::process proc17(ctx, "/bin/bash", {}, v2::process_stdio{.stderr=nullptr});
 * /// C++11 & C++14
 * v2::process proc17(ctx, "/bin/bash", {}, v2::process_stdio{ {}, {}, nullptr});
 *                                                        stdin ^  ^ stderr
 * @endcode
 * 
 * Valid initializers for any stdio are:
 * 
 *  - `std::nullptr_t` assigning a null-device
 *  - `FILE*` any open file, including `stdin`, `stdout` and `stderr`
 *  - a filesystem::path, which will open a readable or writable depending on the direction of the stream
 *  - `native_handle` any native file handle (`HANDLE` on windows) or file descriptor (`int` on posix)
 *  - any io-object with a .native_handle() function that is comptaiblie with the above. E.g. a asio::ip::tcp::socket
 *  - an asio::basic_writeable_pipe for stdin or asio::basic_readable_pipe for stderr/stdout. 
 * 
 * 
 */ 
struct process_stdio
{
  detail::process_input_binding in;
  detail::process_output_binding out;
  detail::process_error_binding err;

#if defined(BOOST_PROCESS_V2_WINDOWS)
  error_code on_setup(windows::default_launcher & launcher, const filesystem::path &, const std::wstring &)
  {

    launcher.startup_info.StartupInfo.dwFlags |= STARTF_USESTDHANDLES;
    launcher.startup_info.StartupInfo.hStdInput  = in.prepare();
    launcher.startup_info.StartupInfo.hStdOutput = out.prepare();
    launcher.startup_info.StartupInfo.hStdError  = err.prepare();
    launcher.inherit_handles = true;
    return error_code {};
  };
#else
  error_code on_exec_setup(posix::default_launcher & launcher, const filesystem::path &, const char * const *)
  {
    if (::dup2(in.fd, in.target) == -1)
      return error_code(errno, system_category());

    if (::dup2(out.fd, out.target) == -1)
      return error_code(errno, system_category());

    if (::dup2(err.fd, err.target) == -1)
      return error_code(errno, system_category());

        
    launcher.fd_whitelist.push_back(STDIN_FILENO);
    launcher.fd_whitelist.push_back(STDOUT_FILENO);
    launcher.fd_whitelist.push_back(STDERR_FILENO);

    return error_code {};
  };
#endif

};

BOOST_PROCESS_V2_END_NAMESPACE

#endif //  BOOST_PROCESS_V2_STDIO_HPP