//
// 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/detail/last_error.hpp>
#include <boost/process/v2/default_launcher.hpp>
#include <cstddef>
#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

#if defined(BOOST_PROCESS_V2_WINDOWS)
#include <io.h>
#endif

BOOST_PROCESS_V2_BEGIN_NAMESPACE


template<typename T>
struct is_readable_pipe : std::false_type
{
};

template<typename Executor>
struct is_readable_pipe<asio::basic_readable_pipe<Executor>> : std::true_type
{
};


template<typename T>
struct is_writable_pipe : std::false_type
{
};

template<typename Executor>
struct is_writable_pipe<asio::basic_writable_pipe<Executor>> : std::true_type
{
};

namespace detail
{


#if defined(BOOST_PROCESS_V2_WINDOWS)

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 Target>
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(Target), false};

  static DWORD get_flags(HANDLE h)
  {
    DWORD res;
    if (!::GetHandleInformation(h, &res))
    {
      error_code ec;
      BOOST_PROCESS_V2_ASSIGN_LAST_ERROR(ec);
      throw system_error(ec, "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(reinterpret_cast<HANDLE>(::_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")) {}
  template<typename T, typename = typename std::enable_if<std::is_same<T, filesystem::path>::value>::type>
  process_io_binding(const T & pth)
    : h(::CreateFileW(
        pth.c_str(),
        Target == STD_INPUT_HANDLE ? GENERIC_READ : GENERIC_WRITE,
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        nullptr,
        OPEN_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,
        nullptr
        ), true)
  {
  }


  template<typename ReadablePipe>
  process_io_binding(ReadablePipe & pipe,
                     typename std::enable_if<is_readable_pipe<ReadablePipe>::value && Target != STD_INPUT_HANDLE>::type * = nullptr)
  {
    net::detail::native_pipe_handle p[2];
    error_code ec;
    net::detail::create_pipe(p, ec);
    if (ec)
      detail::throw_error(ec, "create_pipe");
      
    h = std::unique_ptr<void, handle_closer>{p[1], true};
    pipe.assign(p[0]);
  }


  template<typename WritablePipe>
  process_io_binding(WritablePipe & pipe,
                     typename std::enable_if<is_writable_pipe<WritablePipe>::value && Target == STD_INPUT_HANDLE>::type * = nullptr)
  {
    net::detail::native_pipe_handle p[2];
    error_code ec;
    net::detail::create_pipe(p, ec);
    if (ec)
      detail::throw_error(ec, "create_pipe");

    h = std::unique_ptr<void, handle_closer>{p[0], true};
    pipe.assign(p[1]);
  }
};

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;
  process_io_binding(const process_io_binding &) = delete;
  process_io_binding & operator=(const process_io_binding &) = delete;

  process_io_binding(process_io_binding && other) noexcept
          : fd(other.fd), fd_needs_closing(other.fd_needs_closing), ec(other.ec)
  {
    other.fd = target;
    other.fd_needs_closing = false;
    other.ec = {};
  }

  process_io_binding & operator=(process_io_binding && other) noexcept
  {
    if (fd_needs_closing)
      ::close(fd);

    fd = other.fd;
    fd_needs_closing = other.fd_needs_closing;
    ec = other.ec;

    other.fd = target;
    other.fd_needs_closing = false;
    other.ec = {};
    return *this;
  }

  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(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 ReadablePipe>
  process_io_binding(ReadablePipe & readable_pipe,
                     typename std::enable_if<is_readable_pipe<ReadablePipe>::value && Target != STDIN_FILENO>::type * = nullptr)
  {
    net::detail::native_pipe_handle p[2];
    net::detail::create_pipe(p, ec);
    if (ec)
      detail::throw_error(ec, "create_pipe");

    fd = p[1];
    if (::fcntl(p[0], F_SETFD, FD_CLOEXEC) == -1)
    {
      BOOST_PROCESS_V2_ASSIGN_LAST_ERROR(ec);
      return ;
    }
    fd_needs_closing = true;
    readable_pipe.assign(p[0], ec);
  }


  template<typename WritablePipe>
  process_io_binding(WritablePipe & writable_pipe,
                     typename std::enable_if<is_writable_pipe<WritablePipe>::value && Target == STDIN_FILENO>::type * = nullptr)
  {
    net::detail::native_pipe_handle p[2];
    error_code ec;
    net::detail::create_pipe(p, ec);
    if (ec)
      detail::throw_error(ec, "create_pipe");

    fd = p[0];
    if (::fcntl(p[1], F_SETFD, FD_CLOEXEC) == -1)
    {
      BOOST_PROCESS_V2_ASSIGN_LAST_ERROR(ec);
      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 &,
                           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{.err=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 compatible 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.inherited_handles.reserve(launcher.inherited_handles.size() + 3);
    launcher.inherited_handles.push_back(launcher.startup_info.StartupInfo.hStdInput);
    launcher.inherited_handles.push_back(launcher.startup_info.StartupInfo.hStdOutput);
    launcher.inherited_handles.push_back(launcher.startup_info.StartupInfo.hStdError);
    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());

    return error_code {};
  };
#endif

};

BOOST_PROCESS_V2_END_NAMESPACE

#endif //  BOOST_PROCESS_V2_STDIO_HPP
