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

#include <butl/pager>

#ifndef _WIN32
#  include <unistd.h>    // STDOUT_FILENO
#  include <sys/ioctl.h> // ioctl()

#  include <chrono>
#  include <thread> // this_thread::sleep_for()
#else
#  include <butl/win32-utility>
#endif

#include <cstring>      // strchr()
#include <system_error>

#include <butl/fdstream> // fdclose()

using namespace std;

namespace butl
{
  pager::
  pager (const string& name,
         bool verbose,
         const string* pager,
         const vector<string>* pager_options)
  {
    // If we are using the default pager, try to get the terminal width
    // so that we can center the output.
    //
    if (pager == nullptr)
    {
      size_t col (0);

#ifndef _WIN32
#  ifdef TIOCGWINSZ
      struct winsize w;
      if (ioctl (STDOUT_FILENO, TIOCGWINSZ, &w) == 0)
        col = static_cast<size_t> (w.ws_col);
#  endif
#else
      // This works properly on PowerShell, while for the regular console
      // (cmd.exe) it always returns 80 columns.
      //
      CONSOLE_SCREEN_BUFFER_INFO w;
      if (GetConsoleScreenBufferInfo (GetStdHandle (STD_OUTPUT_HANDLE), &w))
        col = static_cast<size_t> (w.srWindow.Right - w.srWindow.Left + 1);
#endif
      if (col > 80)
        indent_.assign ((col - 80) / 2, ' ');
    }

    vector<const char*> args;
    string prompt;

    if (pager != nullptr)
    {
      if (pager->empty ())
        return; // No pager should be used.

      args.push_back (pager->c_str ());
    }
    else
    {
      // By default try less (again, no pun intended).
      //
      prompt = "-Ps" + name + " (press q to quit, h for help)";

      args.push_back ("less");
      args.push_back ("-R");    // Handle ANSI color.

      args.push_back (prompt.c_str ());
    }

    // Add extra pager options.
    //
    if (pager_options != nullptr)
      for (const string& o: *pager_options)
        args.push_back (o.c_str ());

    args.push_back (nullptr);

    if (verbose)
    {
      for (const char* const* p (args.data ()); *p != nullptr; ++p)
      {
        if (p != args.data ())
          cerr << ' ';

        // Quote if empty or contains spaces.
        //
        bool q (**p == '\0' || strchr (*p, ' ') != nullptr);

        if (q)
          cerr << '"';

        cerr << *p;

        if (q)
          cerr << '"';
      }
      cerr << endl;
    }

    // Ignore errors and go without a pager unless the pager was specified
    // by the user.
    //
    try
    {
      p_ = process (args.data (), -1); // Redirect child's STDIN to a pipe.

      // Wait a bit and see if the pager has exited before reading anything
      // (e.g., because exec() couldn't find the program). If you know a
      // cleaner way to handle this, let me know (and no, a select()-based
      // approach doesn't work; the pipe is buffered and therefore is always
      // ready for writing).
      //
      // MINGW GCC 4.9 doesn't implement this_thread so use Win32 Sleep().
      //
#ifndef _WIN32
      this_thread::sleep_for (chrono::milliseconds (50));
#else
      Sleep (50);
#endif

      bool r;
      if (p_.try_wait (r))
      {
        fdclose (p_.out_fd);

        if (pager != nullptr)
          throw system_error (ECHILD, system_category ());
      }
      else
        os_.open (p_.out_fd);
    }
    catch (const process_error& e)
    {
      if (e.child ())
      {
        cerr << args[0] << ": unable to execute: " << e.what () << endl;
        exit (1);
      }

      // Ignore unless it was a user-specified pager.
      //
      if (pager != nullptr)
        throw; // Re-throw as system_error.
    }

    // Setup the indentation machinery.
    //
    if (!indent_.empty ())
      buf_ = stream ().rdbuf (this);
  }

  bool pager::
  wait (bool ie)
  {
    // Teardown the indentation machinery.
    //
    if (buf_ != nullptr)
    {
      stream ().rdbuf (buf_);
      buf_ = nullptr;
    }

    // Prevent ofdstream::close() from throwing in the ignore errors mode.
    //
    if (ie)
      os_.exceptions (ofdstream::goodbit);

    os_.close ();
    return p_.wait (ie);
  }

  pager::int_type pager::
  overflow (int_type c)
  {
    if (prev_ == '\n' && c != '\n') // Don't indent blanks.
    {
      auto n (static_cast<streamsize> (indent_.size ()));

      if (buf_->sputn (indent_.c_str (), n) != n)
        return traits_type::eof ();
    }

    prev_ = c;
    return buf_->sputc (c);
  }

  int pager::
  sync ()
  {
    return buf_->pubsync ();
  }
}