/*

Copyright (C) 2015, 2016 Olaf Till <i7tiol@t-online.de>

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; If not, see <http://www.gnu.org/licenses/>.

*/

// PKG_ADD: autoload ("select_sockets", "parallel_interface.oct");
// PKG_DEL: autoload ("select_sockets", "parallel_interface.oct", "remove");

#include <octave/oct.h>
#include <octave/dirfns.h>
#include <octave/oct-map.h>
#include <octave/oct-env.h>

#include "parallel-gnutls.h"

#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <unistd.h>

DEFUN_DLD (select_sockets, args, ,
           "-*- texinfo -*-\n\
@deftypefn {Loadable Function} {} select_sockets (@var{connections}, @var{timeout})\n\
@deftypefnx {Loadable Function} {} select_sockets (@var{connections}, @var{timeout}, @var{nfds})\n\
Calls Unix @code{select} for data connections in a parallel cluster.\n\
\n\
This function is for advanced usage (and therefore has minimal\n\
documentation), typically the programming of schedulers. It can be\n\
called at the client or at a server.\n\
\n\
@var{connections}: valid connections object (see @code{pconnect} and\n\
@code{pserver}, possibly indexed).\n\
\n\
@var{timeout}: seconds, negative for infinite.\n\
\n\
@var{nfds}: Passed to Unix @code{select} as first argument, see\n\
documentation of Uix @code{select}. Default: @code{FD_SETSIZE}\n\
(platform specific).\n\
\n\
An error is returned if nfds or a watched filedescriptor plus one\n\
exceeds FD_SETSIZE.\n\
\n\
Returns an index vector to @var{connections} indicating connections\n\
with pending input, readable with @code{precv}.\n\
\n\
If called at the client, the command connections are included into the\n\
UNIX @code{select} call and checked for error flags, and\n\
@code{select_sockets} returns an error if a flag for a remote error is\n\
received.\n\
\n\
@seealso{pconnect, pserver, reval, psend, precv, sclose, parallel_generate_srp_data}\n\
@end deftypefn")
{
  std::string fname ("select_sockets");

  octave_value retval;

  if (args.length () < 2 || args.length () > 3 ||
      args(0).type_id () != octave_parallel_connections::static_type_id ())
    {
      print_usage ();

      return retval;
    }

  double atimeout = args(1).double_value ();
  int nfds = FD_SETSIZE;
  bool err;
  if (args.length () == 3)
    {
      SET_ERR (nfds = args(2).int_value (), err);
    }
  if (err)
    {
      print_usage ();

      return retval;
    }
  if (nfds <= 0)
    {
      error ("%s: 'nfds' must be a positive integer", fname.c_str ());

      return retval;
    }
  if (nfds > FD_SETSIZE)
    {
      error ("%s: 'nfds' exceeds systems maximum given by FD_SETSIZE",
             fname.c_str ());

      return retval;
    }

  const octave_base_value &rep = args(0).get_rep ();
  const octave_parallel_connections &cconns =
    dynamic_cast<const octave_parallel_connections&> (rep);
  octave_parallel_connections_rep *conns = cconns.get_rep ();
  int nconns = conns->get_connections ().numel ();

  if (conns->get_whole_network ()->is_closed ())
    {
      error ("%s: network is closed", fname.c_str ());

      return retval;
    }

  if (nconns < conns->get_all_connections ().numel ()) // it's a subnet
    for (int i = 0; i < nconns; i++)
      if (conns->get_connections ()(i)->is_pseudo_connection ())
        {
          error ("%s: using a subnet and local machine was given as one of the connections (index %i)",
                 fname.c_str (), i + 1);

          return retval;
        }

  timeval tout;
  timeval *timeout = &tout;
  if (atimeout < 0)
    timeout = NULL;
  else
    {
      double ipart, fpart;
      fpart = modf (atimeout, &ipart);
      tout.tv_sec = lrint (ipart);
      tout.tv_usec = lrint (fpart * 1000);
    }

  fd_set rfds, wfds, efds;
  FD_ZERO (&rfds); FD_ZERO (&wfds); FD_ZERO (&efds);

  inthandler_dont_restart_syscalls __inthandler_guard__;

  for (int i = 0; i < nconns; i++)
    {
      if (conns->get_connections ()(i)->is_pseudo_connection ())
        continue;

      int fid = conns->get_connections ()(i)->get_data_stream ()->
        get_strbuf ()->get_fid ();

      if (fid >= nfds)
        {
          error ("%s: file descriptor >= nfds or FD_SETSIZE", fname.c_str ());

          return retval;
        }

      FD_SET (fid, &rfds);

      if (! conns->is_server ())
        {
          fid = conns->get_connections ()(i)->get_cmd_stream ()->
            get_strbuf ()->get_fid ();

          if (fid >= nfds)
            {
              error ("%s: file descriptor >= nfds or FD_SETSIZE",
                     fname.c_str ());

              return retval;
            }

          FD_SET (fid, &rfds);
        }
    }

  // don't restart interrupted select()
  int n = select (nfds, &rfds, &wfds, &efds, timeout);
  if (n == -1)
    {
      error ("%s: select system call returned an error", fname.c_str ());

      return retval;
    }

  bool command_errors = false;
  RowVector ridx (n);
  double *fvec;
  int i;
  for (i = 0, fvec = ridx.fortran_vec (); i < nconns; i++)
    {
      if (conns->get_connections ()(i)->is_pseudo_connection ())
        continue;

      if (FD_ISSET
          (conns->get_connections ()(i)->get_data_stream ()->
           get_strbuf ()->get_fid (),
           &rfds))
        {
          *fvec = double (i + 1);
          fvec++;
        }

      if (! conns->is_server ())
        {
          if (FD_ISSET
              (conns->get_connections ()(i)->get_cmd_stream ()->
               get_strbuf ()->get_fid (),
               &rfds))
            {
              int err;
              if ((err = conns->get_connections ()(i)->poll_for_errors ()) < 0)
                {
                  conns->close ();
                  error ("%s: communication error with server with index %i",
                         fname.c_str (), i + 1);
                  return retval;
                }
              else if (err > 0)
                {
                  c_verror ("%s: a previous command at server with index %i caused an error",
                            fname.c_str (), i + 1);
                  command_errors = true;
                }
            }
        }
    }

  if (command_errors)
    {
      error ("command error");
      
      return retval;
    }

  return octave_value (ridx);
}
