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

#include <ios>
#include <string>
#include <vector>
#include <iomanip>
#include <cassert>
#include <sstream>
#include <fstream>
#include <iostream>
#include <exception>

#include <butl/path>
#include <butl/fdstream>
#include <butl/timestamp>
#include <butl/filesystem>

using namespace std;
using namespace butl;

static const string text1 ("ABCDEF\nXYZ");
static const string text2 ("12");            // Keep shorter than text1.
static const string text3 ("ABCDEF\r\nXYZ");

static string
from_file (const path& f, fdopen_mode m = fdopen_mode::none)
{
  ifdstream ifs (f, m, ifdstream::badbit);

  string s;
  getline (ifs, s, '\0');
  ifs.close (); // Not to miss failed close of the underlying file descriptor.
  return s;
}

static void
to_file (const path& f, const string& s, fdopen_mode m = fdopen_mode::none)
{
  ofdstream ofs (f, m);
  ofs << s;
  ofs.close ();
}

template <typename S, typename T>
static duration
write_time (const path& p, const T& s, size_t n)
{
  timestamp t (timestamp::clock::now ());
  S os (p.string (), ofstream::out);
  os.exceptions (S::failbit | S::badbit);

  for (size_t i (0); i < n; ++i)
  {
    if (i > 0)
      os << '\n'; // Important not to use endl as it syncs a stream.

    os << s;
  }

  os.close ();
  return timestamp::clock::now () - t;
}

template <typename S, typename T>
static duration
read_time (const path& p, const T& s, size_t n)
{
  vector<T> v (n);

  timestamp t (timestamp::clock::now ());
  S is (p.string (), ofstream::in);
  is.exceptions (S::failbit | S::badbit);

  for (auto& ve: v)
    is >> ve;

  assert (is.eof ());

  is.close ();
  duration d (timestamp::clock::now () - t);

  for (const auto& ve: v)
    assert (ve == s);

  return d;
}

int
main (int argc, const char* argv[])
{
  if (!(argc == 1 || (argc == 2 && argv[1] == string ("-v"))))
  {
    cerr << "usage: " << argv[0] << " [-v]" << endl;
    return 1;
  }

  bool v (argc == 2);
  dir_path td (dir_path::temp_directory () / dir_path ("butl-fdstream"));

  // Recreate the temporary directory (that possibly exists from the previous
  // faulty run) for the test files. Delete the directory only if the test
  // succeeds to simplify the failure research.
  //
  try_rmdir_r (td);
  assert (try_mkdir (td) == mkdir_status::success);

  path f (td / path ("file"));

  try
  {
    fdopen (f, fdopen_mode::out); // fdopen_mode::create is missed.
    assert (false);
  }
  catch (const ios::failure&)
  {
  }

  // Read from the newly created empty file.
  //
  assert (from_file (f, fdopen_mode::create) == "");
  assert (try_rmfile (f) == rmfile_status::success);

  // Read from the newly created non-empty file.
  //
  to_file (f, text1, fdopen_mode::create);
  assert (from_file (f) == text1);

  // Check that skip on close as requested.
  //
  {
    ifdstream ifs (fdopen (f, fdopen_mode::in), fdstream_mode::skip);

    string s;
    getline (ifs, s);
    assert (!ifs.eof ());

    ifs.close ();
    assert (ifs.eof ());
  }

  // Check that don't skip on close by default.
  //
  {
    ifdstream ifs (fdopen (f, fdopen_mode::in));

    string s;
    getline (ifs, s);
    assert (!ifs.eof ());

    ifs.close ();
    assert (!ifs.eof ());
  }

  // Read from the file opened in R/W mode.
  //
  assert (from_file (f, fdopen_mode::out) == text1);

  // Read starting from the file's end.
  //
  assert (from_file (f, fdopen_mode::at_end) == "");

  try
  {
    // Fail to create if the file already exists.
    //
    fdopen (
      f, fdopen_mode::out | fdopen_mode::create | fdopen_mode::exclusive);

    assert (false);
  }
  catch (const ios::failure&)
  {
  }

  // Write text2 over text1.
  //
  to_file (f, text2);
  string s (text2);
  s += string (text1, text2.size ());
  assert (from_file (f) == s);

  // Truncate before reading.
  //
  assert (from_file (f, fdopen_mode::out | fdopen_mode::truncate) == "");

  // Append to the file.
  //
  to_file (f, text1, fdopen_mode::truncate);
  to_file (f, text2, fdopen_mode::append);
  assert (from_file (f) == text1 + text2);

  // Append to the file with the yet another way.
  //
  to_file (f, text1, fdopen_mode::truncate);
  to_file (f, text2, fdopen_mode::at_end);
  assert (from_file (f) == text1 + text2);

  // Check creating unopened ifdstream with a non-default exception mask.
  //
  to_file (f, "", fdopen_mode::truncate);

  {
    ifdstream ifs (-1, ifdstream::badbit);
    ifs.open (f);

    string s;
    assert (!getline (ifs, s));
  }

  {
    ifdstream ifs (-1, fdstream_mode::text, ifdstream::badbit);
    ifs.open (f);

    string s;
    assert (!getline (ifs, s));
  }

  // Check creating unopened ofdstream with a non-default exception mask.
  //
  {
    ofdstream ofs (-1, ifdstream::badbit);
    ofs.open (f);

    istringstream is;
    ofs << is.rdbuf (); // Sets failbit if no characters is inserted.
    ofs.close ();
  }

  {
    ofdstream ofs (-1, fdstream_mode::binary, ifdstream::badbit);
    ofs.open (f);

    istringstream is;
    ofs << is.rdbuf (); // Sets failbit if no characters is inserted.
    ofs.close ();
  }

  // Fail to write to a read-only file.
  //
  // Don't work well for MinGW GCC (5.2.0) that throws ios::failure, which in
  // combination with libstdc++'s ios::failure ABI fiasco (#66145) make it
  // impossible to properly catch in this situation.
  //
  try
  {
    {
      ofdstream ofs (fdopen (f, fdopen_mode::in));
      ofs << text1;
      ofs.flush ();
    }

    assert (false);
  }
#if !defined(_WIN32) || !defined(__GLIBCXX__)
  catch (const ios::failure&)
  {
  }
#else
  catch (const std::exception&)
  {
  }
#endif

  try
  {
    ofdstream ofs;
    ofs.open (fdopen (f, fdopen_mode::in));
    ofs << text1;
    ofs.close ();

    assert (false);
  }
#if !defined(_WIN32) || !defined(__GLIBCXX__)
  catch (const ios::failure&)
  {
  }
#else
  catch (const std::exception&)
  {
  }
#endif

  // Fail to read from a write-only file.
  //
  try
  {
    ifdstream ifs (fdopen (f, fdopen_mode::out));
    ifs.peek ();

    assert (false);
  }
  catch (const ios::failure&)
  {
  }

  try
  {
    ifdstream ifs;
    ifs.open (fdopen (f, fdopen_mode::out));
    ifs.peek ();

    assert (false);
  }
  catch (const ios::failure&)
  {
  }

  // Dtor of a not opened ofdstream doesn't terminate a program.
  //
  {
    ofdstream ofs;
  }

  // Dtor of an opened ofdstream doesn't terminate a program during the stack
  // unwinding.
  //
  try
  {
    ofdstream ofs (f);
    throw ios::failure ("test");
  }
  catch (const ios::failure&)
  {
  }

  // Dtor of an opened but being in a bad state ofdstream doesn't terminate a
  // program.
  //
  {
    ofdstream ofs (f, fdopen_mode::out, ofdstream::badbit);
    ofs.clear (ofdstream::failbit);
  }

#ifndef _WIN32

  // Fail for an existing symlink to unexistent file.
  //
  path link (td / path ("link"));
  mksymlink (td / path ("unexistent"), link);

  try
  {
    fdopen (
      link, fdopen_mode::out | fdopen_mode::create | fdopen_mode::exclusive);

    assert (false);
  }
  catch (const ios::failure&)
  {
  }

#else

  // Check translation modes.
  //
  to_file (f, text1, fdopen_mode::truncate);
  assert (from_file (f, fdopen_mode::binary) == text3);

  to_file (f, text3, fdopen_mode::truncate | fdopen_mode::binary);
  assert (from_file (f) == text1);

#endif

  // Compare fdstream and fstream operations performance.
  //
  duration fwd (0);
  duration dwd (0);
  duration frd (0);
  duration drd (0);

  path ff (td / path ("fstream"));
  path fd (td / path ("fdstream"));

  // Make several measurements with different ordering for each benchmark to
  // level fluctuations.
  //
  // Write/read ~10M-size files by 100, 1000, 10 000, 100 000 byte-length
  // strings.
  //
  size_t sz (100);
  for (size_t i (0); i < 4; ++i)
  {
    string s;
    s.reserve (sz);

    // Fill string with characters from '0' to 'z'.
    //
    for (size_t i (0); i < sz; ++i)
      s.push_back ('0' + i % (123 - 48));

    size_t n (10 * 1024 * 1024 / sz);

    for (size_t i (0); i < 4; ++i)
    {
      if (i % 2 == 0)
      {
        fwd += write_time<ofstream>  (ff, s, n);
        dwd += write_time<ofdstream> (fd, s, n);
        frd += read_time<ifstream>   (ff, s, n);
        drd += read_time<ifdstream>  (fd, s, n);
      }
      else
      {
        dwd += write_time<ofdstream> (fd, s, n);
        fwd += write_time<ofstream>  (ff, s, n);
        drd += read_time<ifdstream>  (fd, s, n);
        frd += read_time<ifstream>   (ff, s, n);
      }
    }

    sz *= 10;
  }

  // Write/read ~10M-size files by 64-bit integers.
  //
  uint64_t u (0x1234567890123456);
  size_t n (10 * 1024 * 1024 / sizeof (u));

  for (size_t i (0); i < 4; ++i)
  {
    if (i % 2 == 0)
    {
      fwd += write_time<ofstream>  (ff, u, n);
      dwd += write_time<ofdstream> (fd, u, n);
      frd += read_time<ifstream>   (ff, u, n);
      drd += read_time<ifdstream>  (fd, u, n);
    }
    else
    {
      dwd += write_time<ofdstream> (fd, u, n);
      fwd += write_time<ofstream>  (ff, u, n);
      drd += read_time<ifdstream>  (fd, u, n);
      frd += read_time<ifstream>   (ff, u, n);
    }
  }

  if (v)
    cerr << "fdstream/fstream write and read duration ratios are "
         << fixed << setprecision (2)
         << static_cast<double> (dwd.count ()) / fwd.count () << " and "
         << static_cast<double> (drd.count ()) / frd.count () << endl;

  rmdir_r (td);
}