// file      : butl/fdstream -*- C++ -*-
// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
// license   : MIT; see accompanying LICENSE file

#ifndef BUTL_FDSTREAM
#define BUTL_FDSTREAM

#include <string>
#include <istream>
#include <ostream>
#include <utility> // move()
#include <cstdint> // uint16_t

#include <butl/export>

#include <butl/path>
#include <butl/filesystem> // permissions

namespace butl
{
  // RAII type for file descriptors. Note that failure to close the descriptor
  // is silently ignored by both the destructor and reset().
  //
  // The descriptor can be negative. Such a descriptor is treated as unopened
  // and is not closed.
  //
  struct nullfd_t {constexpr explicit nullfd_t (int) {}};
  constexpr const nullfd_t nullfd (-1);

  class LIBBUTL_EXPORT auto_fd
  {
  public:
    auto_fd (nullfd_t = nullfd) noexcept: fd_ (-1) {}

    explicit
    auto_fd (int fd) noexcept: fd_ (fd) {}

    auto_fd (auto_fd&& fd) noexcept: fd_ (fd.release ()) {}
    auto_fd& operator= (auto_fd&&) noexcept;

    auto_fd (const auto_fd&) = delete;
    auto_fd& operator= (const auto_fd&) = delete;

    ~auto_fd () noexcept;

    int
    get () const noexcept {return fd_;}

    void
    reset (int fd = -1) noexcept;

    int
    release () noexcept
    {
      int r (fd_);
      fd_ = -1;
      return r;
    }

    // Close an open file descriptor. Throw ios::failure on the underlying OS
    // error. Reset the descriptor to -1 whether the exception is thrown or
    // not.
    //
    void
    close ();

  private:
    int fd_;
  };

  // An [io]fstream that can be initialized with a file descriptor in addition
  // to a file name and that also by default enables exceptions on badbit and
  // failbit. So instead of a dance like this:
  //
  // ifstream ifs;
  // ifs.exceptions (ifstream::badbit | ifstream::failbit);
  // ifs.open (path.string ());
  //
  // You can simply do:
  //
  // ifdstream ifs (path);
  //
  // Notes and limitations:
  //
  // - char only
  // - input or output but not both
  // - no support for put back
  // - non-blocking file descriptor is supported only by showmanyc() function
  //   and only on POSIX
  // - throws ios::failure in case of open()/read()/write()/close() errors
  // - exception mask has at least badbit
  // - after catching an exception caused by badbit the stream is no longer
  //   used
  // - not movable, though can be easily supported
  // - passing to constructor auto_fd with a negative file descriptor is valid
  //   and results in the creation of an unopened object
  //
  class LIBBUTL_EXPORT fdbuf: public std::basic_streambuf<char>
  {
  public:
    fdbuf () = default;
    fdbuf (auto_fd&&);

    void
    close ();

    void
    open (auto_fd&&);

    bool
    is_open () const {return fd_.get () >= 0;}

    int
    fd () const {return fd_.get ();}

  public:
    using int_type = std::basic_streambuf<char>::int_type;
    using traits_type = std::basic_streambuf<char>::traits_type;

    // basic_streambuf input interface.
    //
  public:
    virtual std::streamsize
    showmanyc ();

    virtual int_type
    underflow ();

  private:
    bool
    load ();

    // basic_streambuf output interface.
    //
  public:
    virtual int_type
    overflow (int_type);

    virtual int
    sync ();

    virtual std::streamsize
    xsputn (const char_type*, std::streamsize);

  private:
    bool
    save ();

  private:
    auto_fd fd_;
    char buf_[8192];
    bool non_blocking_ = false;
  };

  // File stream mode.
  //
  // The text/binary flags have the same semantics as those in std::fstream.
  // Specifically, this is a noop for POSIX systems where the two modes are
  // the same. On Windows, when reading in the text mode the sequence of 0xD,
  // 0xA characters is translated into the single OxA character and 0x1A is
  // interpreted as EOF. When writing in the text mode the OxA character is
  // translated into the 0xD, 0xA sequence.
  //
  // The skip flag instructs the stream to skip to the end before closing the
  // file descriptor. This is primarily useful when working with pipes where
  // you may want not to "offend" the other end by closing your end before
  // reading all the data.
  //
  // The blocking/non_blocking flags determine whether the IO operation should
  // block or return control if currently there is no data to read or no room
  // to write. Only the istream::readsome() function supports the semantics of
  // non-blocking operations. We also only support this on POSIX (Windows does
  // not provide means for the non-blocking reading from a file descriptor so
  // these flags are noop there). IO stream operations other than readsome()
  // are illegal for non_blocking mode and result in the badbit being set.
  //
  enum class fdstream_mode: std::uint16_t
  {
    text         = 0x01,
    binary       = 0x02,
    skip         = 0x04,
    blocking     = 0x08,
    non_blocking = 0x10
  };

  inline fdstream_mode operator& (fdstream_mode, fdstream_mode);
  inline fdstream_mode operator| (fdstream_mode, fdstream_mode);
  inline fdstream_mode operator&= (fdstream_mode&, fdstream_mode);
  inline fdstream_mode operator|= (fdstream_mode&, fdstream_mode);

  // Extended (compared to ios::openmode) file open flags.
  //
  enum class fdopen_mode: std::uint16_t
  {
    in         = 0x01, // Open for reading.
    out        = 0x02, // Open for writing.
    append     = 0x04, // Seek to the end of file before each write.
    truncate   = 0x08, // Discard the file contents on open.
    create     = 0x10, // Create a file if not exists.
    exclusive  = 0x20, // Fail if the file exists and the create flag is set.
    binary     = 0x40, // Set binary translation mode.
    at_end     = 0x80, // Seek to the end of stream immediately after open.

    none = 0           // Usefull when build the mode incrementally.
  };

  inline fdopen_mode operator& (fdopen_mode, fdopen_mode);
  inline fdopen_mode operator| (fdopen_mode, fdopen_mode);
  inline fdopen_mode operator&= (fdopen_mode&, fdopen_mode);
  inline fdopen_mode operator|= (fdopen_mode&, fdopen_mode);

  class LIBBUTL_EXPORT fdstream_base
  {
  protected:
    fdstream_base () = default;
    fdstream_base (auto_fd&& fd): buf_ (std::move (fd)) {}
    fdstream_base (auto_fd&&, fdstream_mode);

  public:
    int
    fd () const {return buf_.fd ();}

  protected:
    fdbuf buf_;
  };

  // iofdstream constructors and open() functions that take openmode as an
  // argument mimic the corresponding iofstream functions in terms of the
  // openmode mask interpretation. They throw std::invalid_argument for an
  // invalid combination of flags (as per the standard). Note that the in and
  // out flags are always added implicitly for ifdstream and ofdstream,
  // respectively.
  //
  // iofdstream constructors and open() functions that take fdopen_mode as an
  // argument interpret the mask literally just ignoring some flags which are
  // meaningless in the absense of others (read more on that in the comment
  // for fdopen()). Note that the in and out flags are always added implicitly
  // for ifdstream and ofdstream, respectively.
  //
  // iofdstream constructors and open() functions that take file path as a
  // const std::string& or const char* may throw the invalid_path exception.
  //
  // Passing auto_fd with a negative file descriptor is valid and results in
  // the creation of an unopened object.
  //
  // Also note that open() and close() functions can be successfully called
  // for an opened and unopened objects respectively. That is in contrast with
  // iofstream that sets failbit in such cases.
  //

  // Note that ifdstream destructor will close an open file descriptor but
  // will ignore any errors. To detect such errors, call close() explicitly.
  //
  // This is a sample usage of iofdstreams with process. Note that here it is
  // expected that the child process reads from STDIN first and writes to
  // STDOUT afterwards.
  //
  // try
  // {
  //   process pr (args, -1, -1);
  //
  //   try
  //   {
  //     // In case of exception, skip and close input after output.
  //     //
  //     ifdstream is (move (pr.in_ofd), fdstream_mode::skip);
  //     ofdstream os (move (pr.out_fd));
  //
  //     // Write.
  //
  //     os.close (); // Don't block the other end.
  //
  //     // Read.
  //
  //     is.close (); // Skip till end and close.
  //
  //     if (pr.wait ())
  //     {
  //       return ...; // Good.
  //     }
  //
  //     // Non-zero exit, diagnostics presumably issued, fall through.
  //   }
  //   catch (const failure&)
  //   {
  //     // IO failure, child exit status doesn't matter. Just wait for the
  //     // process completion and fall through.
  //     //
  //     // Note that this is optional if the process_error handler simply
  //     // falls through since process destructor will wait (but will ignore
  //     // any errors).
  //     //
  //     pr.wait ();
  //   }
  //
  //   error <<  .... ;
  //
  //   // Fall through.
  // }
  // catch (const process_error& e)
  // {
  //   error << ... << e.what ();
  //
  //   if (e.child ())
  //     exit (1);
  //
  //   // Fall through.
  // }
  //
  // throw failed ();
  //
  class LIBBUTL_EXPORT ifdstream: public fdstream_base, public std::istream
  {
  public:
    // Create an unopened object.
    //
    explicit
    ifdstream (iostate e = badbit | failbit);

    explicit
    ifdstream (auto_fd&&, iostate e = badbit | failbit);
    ifdstream (auto_fd&&, fdstream_mode m, iostate e = badbit | failbit);

    explicit
    ifdstream (const char*,
               openmode = in,
               iostate e = badbit | failbit);

    explicit
    ifdstream (const std::string&,
               openmode = in,
               iostate e = badbit | failbit);

    explicit
    ifdstream (const path&,
               openmode = in,
               iostate e = badbit | failbit);

    ifdstream (const char*,
               fdopen_mode,
               iostate e = badbit | failbit);

    ifdstream (const std::string&,
               fdopen_mode,
               iostate e = badbit | failbit);

    ifdstream (const path&,
               fdopen_mode,
               iostate e = badbit | failbit);

    ~ifdstream () override;

    void
    open (const char*, openmode = in);

    void
    open (const std::string&, openmode = in);

    void
    open (const path&, openmode = in);

    void
    open (const char*, fdopen_mode);

    void
    open (const std::string&, fdopen_mode);

    void
    open (const path&, fdopen_mode);

    void
    open (auto_fd&& fd) {buf_.open (std::move (fd)); clear ();}

    void close ();
    bool is_open () const {return buf_.is_open ();}

  private:
    bool skip_ = false;
  };

  // Note that ofdstream requires that you explicitly call close() before
  // destroying it. Or, more specifically, the ofdstream object should not be
  // in the opened state by the time its destructor is called, unless it is in
  // the "not good" state (good() == false) or the destructor is being called
  // during the stack unwinding due to an exception being thrown
  // (std::uncaught_exception() == true). This is enforced with assert() in
  // the ofdstream destructor.
  //
  class LIBBUTL_EXPORT ofdstream: public fdstream_base, public std::ostream
  {
  public:
    // Create an unopened object.
    //
    explicit
    ofdstream (iostate e = badbit | failbit);

    explicit
    ofdstream (auto_fd&&, iostate e = badbit | failbit);
    ofdstream (auto_fd&&, fdstream_mode m, iostate e = badbit | failbit);

    explicit
    ofdstream (const char*,
               openmode = out,
               iostate e = badbit | failbit);

    explicit
    ofdstream (const std::string&,
               openmode = out,
               iostate e = badbit | failbit);

    explicit
    ofdstream (const path&,
               openmode = out,
               iostate e = badbit | failbit);

    ofdstream (const char*,
               fdopen_mode,
               iostate e = badbit | failbit);

    ofdstream (const std::string&,
               fdopen_mode,
               iostate e = badbit | failbit);

    ofdstream (const path&,
               fdopen_mode,
               iostate e = badbit | failbit);

    ~ofdstream () override;

    void
    open (const char*, openmode = out);

    void
    open (const std::string&, openmode = out);

    void
    open (const path&, openmode = out);

    void
    open (const char*, fdopen_mode);

    void
    open (const std::string&, fdopen_mode);

    void
    open (const path&, fdopen_mode);

    void
    open (auto_fd&& fd) {buf_.open (std::move (fd)); clear ();}

    void close () {if (is_open ()) flush (); buf_.close ();}
    bool is_open () const {return buf_.is_open ();}
  };

  // The std::getline() replacement that provides a workaround for libstdc++'s
  // ios::failure ABI fiasco (#66145) by throwing ios::failure, as it is
  // defined at libbutl build time (new ABI on recent distributions) rather
  // than libstdc++ build time (still old ABI on most distributions).
  //
  // Notes:
  //
  // - This relies of ADL so if the stream is used via the std::istream
  //   interface, then std::getline() will still be used. To put it another
  //   way, this is "the best we can do" until GCC folks get their act
  //   together.
  //
  // - The fail and eof bits may be left cleared in the stream exception mask
  //   when the function throws because of badbit.
  //
  LIBBUTL_EXPORT ifdstream&
  getline (ifdstream&, std::string&, char delim = '\n');

  // Open a file returning an auto_fd that holds its file descriptor on
  // success and throwing ios::failure otherwise.
  //
  // The mode argument should have at least one of the in or out flags set.
  // The append and truncate flags are meaningless in the absense of the out
  // flag and are ignored without it. The exclusive flag is meaningless in the
  // absense of the create flag and is ignored without it. Note also that if
  // the exclusive flag is specified then a dangling symbolic link is treated
  // as an existing file.
  //
  // The permissions argument is taken into account only if the file is
  // created. Note also that permissions can be adjusted while being set in a
  // way specific for the OS. On POSIX systems they are modified with the
  // process' umask, so effective permissions are permissions & ~umask. On
  // Windows permissions other than ru and wu are unlikelly to have effect.
  //
  LIBBUTL_EXPORT auto_fd
  fdopen (const char*,
          fdopen_mode,
          permissions = permissions::ru | permissions::wu |
                        permissions::rg | permissions::wg |
                        permissions::ro | permissions::wo);

  LIBBUTL_EXPORT auto_fd
  fdopen (const std::string&,
          fdopen_mode,
          permissions = permissions::ru | permissions::wu |
                        permissions::rg | permissions::wg |
                        permissions::ro | permissions::wo);

  LIBBUTL_EXPORT auto_fd
  fdopen (const path&,
          fdopen_mode,
          permissions = permissions::ru | permissions::wu |
                        permissions::rg | permissions::wg |
                        permissions::ro | permissions::wo);

  // Duplicate an open file descriptor. Throw ios::failure on the underlying
  // OS error.
  //
  LIBBUTL_EXPORT auto_fd
  fddup (int fd);

  // Set the translation mode for the file descriptor. Throw invalid_argument
  // for an invalid combination of flags. Return the previous mode on success,
  // throw ios::failure otherwise.
  //
  // The text and binary flags are mutually exclusive on Windows. Due to
  // implementation details at least one of them should be specified. On POSIX
  // system the two modes are the same and so no check is performed.
  //
  // The blocking and non-blocking flags are mutually exclusive on POSIX
  // system. Non-blocking mode is not supported on Windows and so the blocking
  // mode is assumed regardless of the flags.
  //
  LIBBUTL_EXPORT fdstream_mode
  fdmode (int, fdstream_mode);

  // Convenience functions for setting the translation mode for standard
  // streams.
  //
  LIBBUTL_EXPORT fdstream_mode stdin_fdmode  (fdstream_mode);
  LIBBUTL_EXPORT fdstream_mode stdout_fdmode (fdstream_mode);
  LIBBUTL_EXPORT fdstream_mode stderr_fdmode (fdstream_mode);

  // Low-level, nothrow file descriptor API.
  //

  // Close the file descriptor. Return true on success, set errno and return
  // false otherwise.
  //
  LIBBUTL_EXPORT bool
  fdclose (int) noexcept;

  // Open the null device (e.g., /dev/null) that discards all data written to
  // it and provides no data for read operations (i.e., yelds EOF on read).
  // Return file descriptor on success, set errno and return -1 otherwise.
  // Note that it's the caller's responsibility to close the returned file
  // descriptor.
  //
  // On Windows the null device is NUL and writing anything substantial to it
  // (like redirecting a process' output) is extremely slow, as in, an order
  // of magnitude slower than writing to disk. If you are using the descriptor
  // yourself this can be mitigated by setting the binary mode (already done
  // by fdopen()) and using a buffer of around 64K. However, sometimes you
  // have no control of how the descriptor will be used. For instance, it can
  // be used to redirect a child's stdout and the way the child sets up its
  // stdout is out of your control (on Windows). For such cases, there is an
  // emulation via a temporary file. Mostly it functions as a proper null
  // device with the file automatically removed once the descriptor is
  // closed. One difference, however, would be if you were to both write to
  // and read from the descriptor.
  //
  // @@ You may have noticed that this interface doesn't match fdopen() (it
  //    does not throw and return int instead of auto_fd). The reason for this
  //    is process and its need to translate everything to process_error).
  //    Once we solve that we can clean this up as well.
  //
#ifndef _WIN32
  LIBBUTL_EXPORT int
  fdnull () noexcept;
#else
  LIBBUTL_EXPORT int
  fdnull (bool temp = false) noexcept;
#endif

  struct fdpipe
  {
    auto_fd in;
    auto_fd out;
  };

  // Create a pipe. Throw ios::failure on the underlying OS error. By default
  // both ends of the pipe are opened in the text mode. Pass the binary flag
  // to instead open them in the binary mode. Passing a mode other than none
  // or binary is illegal.
  //
  // Note that on Windows both ends of the created pipe are not inheritable.
  // In particular, the process class that uses fdpipe underneath makes the
  // appropriate end of pipe (the one being passed to the child) inheritable.
  //
  LIBBUTL_EXPORT fdpipe
  fdopen_pipe (fdopen_mode = fdopen_mode::none);
}

#include <butl/fdstream.ixx>

#endif // BUTL_FDSTREAM