//
// LDASTools AL - A library collection to provide an abstraction layer
//
// Copyright (C) 2018 California Institute of Technology
//
// LDASTools AL is free software; you may 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 (GPLv2) of the
// License or at your discretion, any later version.
//
// LDASTools AL is distributed in the hope that it will be useful, but
// without any warranty or even the implied warranty of merchantability
// or fitness for a particular purpose. See the GNU General Public
// License (GPLv2) for more details.
//
// Neither the names of the California Institute of Technology (Caltech),
// The Massachusetts Institute of Technology (M.I.T), The Laser
// Interferometer Gravitational-Wave Observatory (LIGO), nor the names
// of its contributors may be used to endorse or promote products derived
// from this software without specific prior written permission.
//
// You should have received a copy of the licensing terms for this
// software included in the file LICENSE located in the top-level
// directory of this package. If you did not, you can view a copy at
// http://dcc.ligo.org/M1500244/LICENSE
//

#if HAVE_CONFIG_H
#include <ldastoolsal_config.h>
#endif /* HAVE_CONFIG_H */

#include <sys/types.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <sys/wait.h>

#include <fcntl.h>
#include <poll.h>
#include <unistd.h>

#include <cstdarg>

#include <iostream>

#include "Exec.hh"

namespace /* Anonymous */
{
    using namespace LDASTools::AL;

    void
    child_cpu_time( Exec::time_type& User, Exec::time_type& System )
    {
        struct rusage ru;

        getrusage( RUSAGE_CHILDREN, &ru );

        User = (double)( ru.ru_utime.tv_sec ) +
            (double)( ru.ru_utime.tv_usec ) / 1000000.0;
        System = (double)( ru.ru_stime.tv_sec ) +
            (double)( ru.ru_stime.tv_usec ) / 1000000.0;
    }
} // namespace

namespace LDASTools
{
    namespace AL
    {
        const char* Exec::COMMAND_END = (const char*)NULL;

        Exec::Exec( arg_type Command, ... )
        {
            m_child_exit.s_exit_code = 0;
            m_child_exit.s_signal = 0;
            m_child_exit.s_core_dump = 0;
            m_child_exit.s_exited = false;

            arg_type arg;

            va_list ap;

            //-------------------------------------------------------------------
            // Preserve the command to execute
            //-------------------------------------------------------------------
            m_options.push_back( Command );

            va_start( ap, Command );
            while ( ( arg = va_arg( ap, arg_type ) ) != COMMAND_END )
            {
                //-----------------------------------------------------------------
                // Add arguments
                //-----------------------------------------------------------------
                m_options.push_back( arg );
            }

            m_options.push_back( COMMAND_END );

            va_end( ap );
        }

        void
        Exec::Spawn( )
        {
            //-------------------------------------------------------------------
            // Need to record some information to properly calculate
            // the amount of time spent in the sub-process
            //-------------------------------------------------------------------
            child_cpu_time( m_start_time_user, m_start_time_system );

            //-------------------------------------------------------------------
            // Do the usual stuff
            //-------------------------------------------------------------------
            Fork::Spawn( );
        }

        void
        Exec::evalChild( )
        {
            //-------------------------------------------------------------------
            // Replace stdin with pipe
            //-------------------------------------------------------------------
            close( 0 ); // Close stdin
            if ( dup( getStdIn( ) ) == -1 )
            {
                _exit( 1 );
            }
            closeStdIn( );

            //-------------------------------------------------------------------
            // Replace stdout with pipe
            //-------------------------------------------------------------------
            close( 1 ); // Close stdout
            if ( dup( getStdOut( ) ) == -1 )
            {
                _exit( 1 );
            }
            closeStdOut( );

            //-------------------------------------------------------------------
            // Replace stderr with pipe
            //-------------------------------------------------------------------
            close( 2 ); // Close stdout
            if ( dup( getStdErr( ) ) == -1 )
            {
                _exit( 1 );
            }
            closeStdErr( );

            //-------------------------------------------------------------------
            // Run the command in the new shell
            //-------------------------------------------------------------------
            char* const* xargs(
                const_cast< char* const* >( &( m_options[ 0 ] ) ) );
            execv( m_options[ 0 ], xargs );
            _exit( 1 );
        }

        void
        Exec::evalParent( )
        {
            //-------------------------------------------------------------------
            // Give time for the child process to spawn
            //-------------------------------------------------------------------
            usleep( 2 * 1000 );

            //-------------------------------------------------------------------
            // Wait for the child process to complete
            //-------------------------------------------------------------------
            int              fd_flags = 0;
            int              stat;
            pid_t            retval;
            bool             done = false;
            char             buffer[ 256 ];
            static const int MAX_MONITOR = 2;
            int              flag_modifier = ( O_NONBLOCK | O_ASYNC );

            struct pollfd monitor[ MAX_MONITOR ];

            monitor[ E_STDOUT ].fd = getStdOut( );
            monitor[ E_STDERR ].fd = getStdErr( );
            for ( int x = 0; x < MAX_MONITOR; ++x )
            {
                monitor[ x ].events = POLLIN;
                fd_flags = fcntl( monitor[ x ].fd, F_GETFL, 0 );
                fcntl( monitor[ x ].fd, F_SETFL, fd_flags | flag_modifier );
            }

            int open_monitors = MAX_MONITOR;
            while ( !done )
            {
                int timeout = 1000; /* milliseconds ( 1/1000 ) */

                int poll_retval = poll( monitor, MAX_MONITOR, timeout );

                if ( poll_retval > 0 )
                {
                    for ( int x = 0; x < MAX_MONITOR; ++x )
                    {
                        if ( monitor[ x ].revents != 0 )
                        {
                            if ( monitor[ x ].revents & POLLIN )
                            {
                                ssize_t count{ 0 };
                                do
                                {
                                    count = read( monitor[ x ].fd,
                                                  buffer,
                                                  sizeof( buffer ) );
                                    if ( count > 0 )
                                    {
                                        m_child_exit.s_output[ x ].append(
                                            buffer, count );
                                    }

                                } while ( count > 0 );
                                if ( ( count < 0 ) &&
                                     ( fcntl( monitor[ x ].fd, F_GETFD ) < 0 ) )
                                {
                                    monitor[ x ].fd = -1;
                                    --open_monitors;
                                }
                            }
                            if ( monitor[ x ].revents &
                                 ( POLLERR | POLLHUP | POLLNVAL ) )
                            {
                                --open_monitors;
                            }
                        }
                    }
                }
                else if ( poll_retval < 0 )
                {
                    done = true;
                }
                if ( ( open_monitors <= 0 ) || ( !IsChildAlive( ) ) )
                {
                    done = true;
                }
            }
            retval = waitpid( ChildPid( ), &stat, 0 );

            if ( retval == -1 )
            {
                return;
            }

            child_cpu_time( m_child_exit.s_time_user,
                            m_child_exit.s_time_system );

            m_child_exit.s_time_user =
                m_child_exit.s_time_user - m_start_time_user;
            m_child_exit.s_time_system =
                m_child_exit.s_time_system - m_start_time_system;

            if ( WIFEXITED( stat ) )
            {
                m_child_exit.s_exit_code = WEXITSTATUS( stat );
                m_child_exit.s_exited = true;
            }
            else if ( WIFSIGNALED( stat ) )
            {
                m_child_exit.s_signal = WTERMSIG( stat );
                m_child_exit.s_core_dump = WCOREDUMP( stat );
            }
        }
    } // namespace AL
} // namespace LDASTools
