// 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 <cstdint> // uint16_t

#include <butl/export>

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

namespace butl
{
  // 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
  // - 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
  //
  class LIBBUTL_EXPORT fdbuf: public std::basic_streambuf<char>
  {
  public:
    virtual
    ~fdbuf ();
    fdbuf () = default;
    fdbuf (int fd) {open (fd);}

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

    void
    close ();

    void
    open (int fd);

    bool
    is_open () const {return fd_ != -1;}

  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 ();

  private:
    bool
    save ();

  private:
    int fd_ = -1;
    char buf_[4096];
  };

  // 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.
  //
  // 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.
  //
  enum class fdstream_mode: std::uint16_t
  {
    text   = 0x01,
    binary = 0x02,
    skip   = 0x04
  };

  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 (int fd): buf_ (fd) {}
    fdstream_base (int, fdstream_mode);

  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 -1 as a 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.
  //
  // @@ Need to make sure performance is on par with fstream on both
  //    Linux and Windows.
  //
  // @@ Do we need to increase default buffer size? Make it customizable?
  //    Wonder what it is in libstdc++ and MSVC?

  // Note that ifdstream destructor will close an open file descriptor but
  // will ignore any errors. To detect such errors, call close() explicitly.
  //
  class LIBBUTL_EXPORT ifdstream: fdstream_base, public std::istream
  {
  public:
    // Create an unopened object with iostate = badbit | failbit (we cannot
    // have the iostate as an argument since it clashes with int fd) To create
    // an unopened object with non-default exception mask one can do:
    //
    // ifdstream (-1, ...);
    //
    ifdstream ();

    explicit
    ifdstream (int fd, iostate e = badbit | failbit);
    ifdstream (int 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 (int fd) {buf_.open (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: fdstream_base, public std::ostream
  {
  public:
    // Create an unopened object with iostate = badbit | failbit (we cannot
    // have the iostate as an argument since it clashes with int fd). To create
    // an unopened object with non-default exception mask one can do:
    //
    // ofdstream (-1, ...);
    //
    ofdstream ();

    explicit
    ofdstream (int fd, iostate e = badbit | failbit);
    ofdstream (int 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 (int fd) {buf_.open (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 the 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 int
  fdopen (const char*,
          fdopen_mode,
          permissions = permissions::ru | permissions::wu |
                        permissions::rg | permissions::wg |
                        permissions::ro | permissions::wo);

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

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

  // Set the translation mode for the file descriptor. Return the previous
  // mode on success, throw ios::failure otherwise.
  //
  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.
  //
#ifndef _WIN32
  LIBBUTL_EXPORT int
  fdnull () noexcept;
#else
  LIBBUTL_EXPORT int
  fdnull (bool temp = false) noexcept;
#endif
}

#include <butl/fdstream.ixx>

#endif // BUTL_FDSTREAM