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

#include <ldastoolsal_config.h>

#include <sys/types.h>

#include <dirent.h> // for DIR*
#include <limits.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>

#include <cstddef>

#include <list>
#include <stdexcept>
#include <sstream>
#include <string>

#include <boost/shared_ptr.hpp>

#include "ldastoolsal/MemChecker.hh"
#include "ldastoolsal/Directory.hh"
#include "ldastoolsal/mutexlock.hh"
#include "ldastoolsal/TriStatePool.hh"
#include "ldastoolsal/ErrorLog.hh"
#include "ldastoolsal/Singleton.hh"
#include "ldastoolsal/System.hh"
#include "ldastoolsal/SystemCallTask.hh"
#include "ldastoolsal/Timeout.hh"

using LDASTools::AL::ErrorLog;
using LDASTools::AL::StdErrLog;
using LDASTools::System::ErrnoMessage;

#define DYNAMIC_BUFFER 1
#define READDIR_R_DEPRICATED 1

#ifndef NAME_MAX
#ifdef FILENAME_MAX
#define NAME_MAX FILENAME_MAX
#endif
#endif

typedef struct dirent dirent_type;
using LDASTools::AL::Directory;
using LDASTools::AL::SystemCallTask;
using LDASTools::AL::Timeout;
using LDASTools::AL::TIMEOUT_COMPLETED;
using LDASTools::AL::TimeoutException;
using LDASTools::AL::TriStatePool;

namespace LDASTools
{
    namespace AL
    {
        class Directory::Internals : public SystemCallTask,
                                     public TriStateInterface
        {
        public:
            static int m_wait_time_max;

            enum mode_type
            {
                MODE_BLOCK_READ,
                MODE_OPEN,
                MODE_CLOSE,
                MODE_NEXT
            };

            enum state_type
            {
                STATE_OPEN,
                STATE_OPENING,
                STATE_READING,
                STATE_CLOSING,
                STATE_CLOSED
            };

            mode_type m_mode;
            union
            {
                bool next;
            } m_retval;

            block_read_type m_block_read;

            Internals( );

            virtual ~Internals( );

            //-----------------------------------------------------------------
            /// \brief Store the name of the directory to be read.
            //-----------------------------------------------------------------
            void DirectoryName( const std::string& Name );

            //-----------------------------------------------------------------
            /// \brief Return the current directory state
            //-----------------------------------------------------------------
            state_type State( ) const;

            //-----------------------------------------------------------------
            /// \brief Establish the state of the object;
            //-----------------------------------------------------------------
            void State( state_type State );

            inline const std::string&
            DirName( ) const
            {
                return m_dirname;
            }

            inline int
            Fd( ) const
            {
                return m_fd;
            }

            //-----------------------------------------------------------------
            /// \brief Obtain the name of the file in the directory buffer.
            //-----------------------------------------------------------------
            const char* Filename( ) const;

            int Eval( mode_type Mode );

            virtual bool makeAvailable( ) const;

        protected:
            //-----------------------------------------------------------------
            /// \brief Do the system call
            ///
            /// \return
            ///     Upon successful completion, the value zero is returned.
            ///     Upon failure, the value -1 is returned and errno
            ///     is set.
            //-----------------------------------------------------------------
            virtual int eval( );
            state_type  stateNoLock( ) const;
            void        stateNoLock( state_type State );

        private:
            std::string m_dirname;
            DIR*        m_dirp;
            int         m_fd;
#if DYNAMIC_BUFFER
            dirent_type* m_buffer;
#else /* DYNAMIC_BUFFER */
            union
            {
                dirent_type entry;
                char        padding[ 8 * 1024 ];
            } m_buffer;
#endif /* DYNAMIC_BUFFER */
            state_type                    m_state;
            mutable MutexLock::baton_type m_state_baton;

            void mode( mode_type Mode );
        };

        //===================================================================
        // D I R E C T O R Y : : I N T E R N A L S
        //===================================================================
        inline void
        Directory::Internals::DirectoryName( const std::string& Name )
        {
            m_dirname = Name;
        }

        inline int
        Directory::Internals::Eval( mode_type Mode )
        {
            m_mode = Mode;
            return eval( );
        }

        inline const char*
        Directory::Internals::Filename( ) const
        {
#if DYNAMIC_BUFFER
            return m_buffer->d_name;
#else /* DYNAMIC_BUFFER */
            return m_buffer.entry.d_name;
#endif /* DYNAMIC_BUFFER */
        }

        inline Directory::Internals::state_type
        Directory::Internals::State( ) const
        {
            MutexLock l( m_state_baton, __FILE__, __LINE__ );

            return stateNoLock( );
        }

        inline void
        Directory::Internals::State( Directory::Internals::state_type State )
        {
            MutexLock l( m_state_baton, __FILE__, __LINE__ );

            stateNoLock( State );
        }

        inline Directory::Internals::state_type
        Directory::Internals::stateNoLock( ) const
        {
            return m_state;
        }

        inline void
        Directory::Internals::stateNoLock(
            Directory::Internals::state_type State )
        {
            m_state = State;
        }

        inline void
        Directory::Internals::mode( mode_type Mode )
        {
            m_mode = Mode;
        }

        //===================================================================
        // D I R E C T O R Y
        //===================================================================
        int
        Directory::Fd( ) const
        {
            return m_internals->Fd( );
        }

        const char*
        Directory::EntryName( ) const
        {
            return m_internals->Filename( );
        }

        const std::string&
        Directory::Name( ) const
        {
            return m_internals->DirName( );
        }

        int
        Directory::Timeout( )
        {
            return Internals::m_wait_time_max;
        }

        void
        Directory::Timeout( int Value )
        {
            Internals::m_wait_time_max = Value;
        }
    } // namespace AL
} // namespace LDASTools

namespace
{
    typedef boost::shared_ptr< Directory::Internals > value_type;

    //---------------------------------------------------------------------
    /// \brief Collection of reusable buffers
    //---------------------------------------------------------------------
    class DirectoryPool : public TriStatePool< value_type >
    {
        SINGLETON_TS_DECL( DirectoryPool );
    };

#if 0
  //---------------------------------------------------------------------
  /// \brief Collection of buffers that have timed out
  ///
  /// This is only a stop gap solution to prevent the memory resource
  /// from being recycled while being used by another thread.
  ///
  /// \todo
  ///    Instead of collecting these buffers in a pool where they will
  ///    be leaking memory, the thread needs to take ownership of the
  ///    object and destroy it once the thread completes.
  //---------------------------------------------------------------------
  class DirectoryTrashPool
    : public Pool< value_type >
  {
    SINGLETON_TS_DECL( DirectoryTrashPool );
  };
#endif /* 0 */

    //---------------------------------------------------------------------
    /// \brief Create a new instance for use in the pool
    ///
    /// This routine is used to create an new instance of the lStatVFS
    /// class if none are available in the pool.
    //---------------------------------------------------------------------
    value_type
    create_value_type( )
    {
        value_type retval( new value_type::element_type );

        return retval;
    }

    int process_request( value_type DirInfo );

} // namespace

namespace LDASTools
{
    namespace AL
    {
        //===================================================================
        // D I R E C T O R Y : : I N T E R N A L S
        //===================================================================
        int Directory::Internals::m_wait_time_max = 20;

        Directory::Internals::Internals( )
            : m_dirname( "" ), m_dirp( (DIR*)NULL ), m_fd( -1 ),
#if DYNAMIC_BUFFER
              m_buffer( (dirent_type*)NULL ),
#endif /* DYNAMIC_BUFFER */
              m_state( STATE_CLOSED ), m_state_baton( __FILE__, __LINE__ )
        {
        }

        Directory::Internals::~Internals( )
        {
            bool do_close( false );

            {
                MutexLock l( m_state_baton, __FILE__, __LINE__ );

                if ( m_state == STATE_OPEN )
                {
                    m_mode = MODE_CLOSE;
                    do_close = true;
                }
            }
            if ( do_close )
            {
                eval( );
            }
        }

        int
        Directory::Internals::eval( )
        {
            int retval = -1;

            //-------------------------------------------------------------------
            //-------------------------------------------------------------------
            switch ( m_mode )
            {
            case MODE_BLOCK_READ:
            {
                bool do_it( false );
                {
                    MutexLock l( m_state_baton, __FILE__, __LINE__ );

                    if ( ( stateNoLock( ) == STATE_OPEN ) && ( m_dirp ) )
                    {
                        do_it = true;
                        m_state = STATE_READING;
                    }
                    else
                    {
                        errno = EBADF;
                        retval = -1;
                    }
                }
                if ( do_it )
                {
                    //-------------------------------------------------------------
                    // Read entries from the directory
                    //-------------------------------------------------------------
                    m_block_read.erase( m_block_read.begin( ),
                                        m_block_read.end( ) );
                    dirent_type* result = (dirent_type*)NULL;
                    while ( true )
                    {
#if READDIR_R_DEPRICATED
                        result = readdir( m_dirp );
#else /* READDIR_R_DEPRICATED */
#if DYNAMIC_BUFFER
                        retval = readdir_r( m_dirp, m_buffer, &result );
#else /* DYNAMIC_BUFFER */
                        retval =
                            readdir_r( m_dirp, &( m_buffer.entry ), &result );
#endif /* DYNAMIC_BUFFER */
#endif /* READDIR_R_DEPRICATED */
                        if ( result == (dirent_type*)NULL )
                        {
                            break;
                        }
                        m_block_read.push_back( result->d_name );
                    }
                    {
                        //-----------------------------------------------------------
                        // Need to reset the state since the reading has
                        // completed.
                        //-----------------------------------------------------------
                        MutexLock l( m_state_baton, __FILE__, __LINE__ );

                        if ( m_state == STATE_READING )
                        {
                            m_state = STATE_OPEN;
                        }
                    }
                }
            }
            break;
            case MODE_CLOSE:
            {
                //---------------------------------------------------------------
                // Close the directory
                //---------------------------------------------------------------
                if ( State( ) != STATE_CLOSED )
                {
                    bool do_close( false );
                    {
                        //-----------------------------------------------------------
                        // Need to be careful about state transition.
                        // Also needs to prevent multiple threads from trying
                        // to close.
                        //-----------------------------------------------------------
                        MutexLock l( m_state_baton, __FILE__, __LINE__ );

                        if ( m_state == STATE_OPEN )
                        {
                            do_close = true;
                            m_state = STATE_CLOSING;
                        }
                    }
                    if ( do_close && m_dirp )
                    {
                        int err = closedir( m_dirp );
                        if ( err != 0 )
                        {
                            //---------------------------------------------------------
                            // Look at the system error code to determine if it
                            // is a recoverable error.
                            //---------------------------------------------------------
                            switch ( errno )
                            {
                            case EBADF:
                            default:
                                // These are non recoverable errors
                                State( STATE_CLOSED );
                                break;
                            }
                        }
                        else
                        {
                            State( STATE_CLOSED );
                        }
                        {
                            MutexLock l( m_state_baton, __FILE__, __LINE__ );

                            if ( m_state == STATE_CLOSED )
                            {
                                m_dirp = (DIR*)NULL;
                                m_fd = -1;
                            }
                        }
                    }
                }
            }
            break;
            case MODE_NEXT:
            {
                bool do_next( false );
                {
                    MutexLock l( m_state_baton, __FILE__, __LINE__ );

                    if ( ( m_state == STATE_OPEN ) && ( m_dirp ) )
                    {
                        do_next = true;
                        m_state = STATE_READING;
                    }
                    else
                    {
                        errno = EBADF;
                        retval = -1;
                    }
                }
                if ( do_next )
                {
                    //-------------------------------------------------------------
                    // Read the next entry from the directory
                    //-------------------------------------------------------------
                    dirent_type* result;

#if READDIR_R_DEPRICATED
                    result = readdir( m_dirp );
#else /* READDIR_R_DEPRICATED */
#if DYNAMIC_BUFFER
                    retval = readdir_r( m_dirp, m_buffer, &result );
#else /* DYNAMIC_BUFFER */
                    retval = readdir_r( m_dirp, &( m_buffer.entry ), &result );
#endif /* DYNAMIC_BUFFER */
#endif /* READDIR_R_DEPRICATED */
                    m_retval.next =
                        ( ( retval == 0 ) && ( result != (dirent_type*)NULL ) );
                    {
                        //-----------------------------------------------------------
                        // Need to reset the state since the reading has
                        // completed.
                        //-----------------------------------------------------------
                        MutexLock l( m_state_baton, __FILE__, __LINE__ );

                        if ( m_state == STATE_READING )
                        {
                            m_state = STATE_OPEN;
                        }
                    }
                }
            }
            break;
            case MODE_OPEN:
            {
                //---------------------------------------------------------------
                // Open the directory
                //---------------------------------------------------------------
                bool do_open( false );
                bool do_rewind( false );
                {
                    MutexLock l( m_state_baton, __FILE__, __LINE__ );

                    if ( m_state == STATE_OPEN )
                    {
                        if ( m_dirp )
                        {
                            do_rewind = true;
                            m_state = STATE_OPENING;
                        }
                    }
                    else if ( m_state == STATE_CLOSED )
                    {
                        do_open = true;
                        do_rewind = true;
                        m_state = STATE_OPENING;
                    }
                }
                if ( do_open )
                {
#if HAVE_DIRFD || HAVE_DIRFD_MACRO
                    m_dirp = opendir( m_dirname.c_str( ) );
#elif HAVE_FDOPENDIR /* HAVE_DIRFD */
                    m_fd = open( m_dirname.c_str( ), O_RDONLY );
                    if ( m_fd >= 0 )
                    {
                        m_dirp = fdopendir( m_fd );
                    }
#else
#error Unable to obtain the directory file descriptor
#endif /* HAVE_DIRFD */

                    if ( m_dirp == 0 )
                    {
                        State( STATE_CLOSED );

                        std::ostringstream msg;

                        msg << "Directory::Internals::Internals( ): "
                            << "opendir( ) failed for '" << m_dirname << "'"
                            << ErrnoMessage( );

                        throw std::runtime_error( msg.str( ) );
                    }
#if HAVE_DIRFD || HAVE_DIRFD_MACRO
                    m_fd = dirfd( m_dirp );
#endif /* HAVE_DIRFD */
                }
                if ( do_rewind && m_dirp )
                {
                    //-------------------------------------------------------------
                    // Ensure that we start reading at the start of the
                    // directory
                    //-------------------------------------------------------------
                    rewinddir( m_dirp );
                }
                // Allocate the buffer only once.
#if DYNAMIC_BUFFER
                if ( !m_buffer )
                {
                    //-------------------------------------------------------------
                    // Need to find out the size of the buffer that needs to be
                    //    allocated.
                    //-------------------------------------------------------------
                    long name_max = -1;
#if defined( HAVE_FPATHCONF ) && defined( _PC_NAME_MAX )
                    name_max = fpathconf( m_fd, _PC_NAME_MAX );
                    if ( name_max == -1 )
                    {
#if defined( NAME_MAX )
                        name_max = NAME_MAX;
#else
                        State( STATE_OPEN );

                        std::ostringstream msg;

                        msg << "Directory::Internals::Internals( ): "
                            << "Unable to allocate directory buffer";

                        throw std::runtime_error( msg.str( ) );
#endif /* defined(NAME_MAX) */
                    }
#else
#if defined( NAME_MAX )
                    name_max = NAME_MAX;
#else
#error "buffer size for readdir_r cannot be determined"
#endif
#endif
                    size_t struct_size =
                        (size_t)offsetof( dirent_type, d_name ) + name_max + 1;
                    if ( struct_size < sizeof( *m_buffer ) )
                    {
                        struct_size = sizeof( *m_buffer );
                    }
                    //-------------------------------------------------------------
                    // After finding the size of the buffer, it is time to
                    // allocate
                    //   the memory for the buffer for reading directory
                    //   information.
                    //-------------------------------------------------------------
                    m_buffer = (dirent_type*)malloc( struct_size );
                }
#endif /* DYNAMIC_BUFFER */
                State( STATE_OPEN );
            }
            break;
            }
            //-------------------------------------------------------------------
            // The call has completed, return the information to the caller.
            //-------------------------------------------------------------------
            return retval;
        }

        bool
        Directory::Internals::makeAvailable( ) const
        {
            if ( State( ) == STATE_OPEN )
            {
                const_cast< Internals* >( this )->Eval( MODE_CLOSE );
            }
            return ( State( ) == STATE_CLOSED );
        }

        //=====================================================================
        // D I R E C T O R Y
        //=====================================================================
        inline void
        Directory::eval_request( const int Request )
        {
            if ( m_blocking_mode == MODE_NON_BLOCKING )
            {
                m_internals->m_mode =
                    static_cast< Internals::mode_type >( Request );

                process_request( m_internals );
            }
            else
            {
                m_internals->Eval(
                    static_cast< Internals::mode_type >( Request ) );
            }
        }

        Directory::Directory( const std::string& DirName, bool OpenDirectory )
            : m_internals( (Internals*)NULL ),
              m_blocking_mode( MODE_NON_BLOCKING )
        {
            m_internals =
                DirectoryPool::Instance( ).Request( create_value_type );
            m_internals->State( Internals::STATE_CLOSED );
            m_internals->DirectoryName( DirName );

            if ( OpenDirectory )
            {
                try
                {
                    Open( );
                }
                catch ( ... )
                {
                }
            }
        }

        Directory::~Directory( )
        {
            if ( m_internals )
            {
#if 0
	if ( m_internals->State( ) == Internals::STATE_CLOSED )
	{
	  DirectoryPool::Instance( ).Relinquish( m_internals );
	}
	else
	{
	  DirectoryTrashPool::Instance( ).Relinquish( m_internals );
	}
#else /* 0 */
                DirectoryPool::Instance( ).Relinquish( m_internals );
#endif /* 0 */
            }
        }

        void
        Directory::Close( )
        {
            eval_request( Internals::MODE_CLOSE );
        }

        bool
        Directory::Next( )
        {
            m_internals->m_retval.next = false;

            eval_request( Internals::MODE_NEXT );

            return m_internals->m_retval.next;
        }

        void
        Directory::Next( block_read_type& Entries )
        {
            eval_request( Internals::MODE_BLOCK_READ );

            //-------------------------------------------------------------------
            // copy the entries that have been discovered.
            //-------------------------------------------------------------------
            Entries = m_internals->m_block_read;
        }

        void
        Directory::Open( )
        {
            eval_request( Internals::MODE_OPEN );
        }
    } // namespace AL
} // namespace LDASTools

namespace
{
    using LDASTools::AL::MemChecker;

    //---------------------------------------------------------------------
    //---------------------------------------------------------------------
    SINGLETON_TS_INST( DirectoryPool );

    DirectoryPool::DirectoryPool( )
    {
        MemChecker::Append( singleton_suicide,
                            "<anonymous>::DirectoryPool::singleton_suicide",
                            5 );
    }

#if 0
  //---------------------------------------------------------------------
  //---------------------------------------------------------------------
  SINGLETON_TS_INST( DirectoryTrashPool );

  DirectoryTrashPool::
  DirectoryTrashPool( )
  {
    MemChecker::Append( singleton_suicide,
			"<anonymous>::DirectoryTrashPool::singleton_suicide",
			5 );
  }
#endif /* 0 */

    int
    process_request( value_type DirInfo )
    {
        int retval( -1 );
        int status = TIMEOUT_COMPLETED;

        try
        {
            Timeout( DirInfo, DirInfo->m_wait_time_max, status );
        }
        catch ( const TimeoutException& Exception )
        {
            errno = EINTR;
            std::ostringstream msg;
            msg << "TimeoutException for: " << DirInfo->DirName( ) << " ("
                << Exception.what( ) << ")";

            if ( StdErrLog.IsOpen( ) )
            {
                StdErrLog( ErrorLog::DEBUG, __FILE__, __LINE__, msg.str( ) );
            }
            throw;
        }
        catch ( const std::exception& Exception )
        {
            std::ostringstream msg;
            msg << "std::exception for: " << DirInfo->DirName( ) << " ("
                << Exception.what( ) << ")";

            if ( StdErrLog.IsOpen( ) )
            {
                StdErrLog( ErrorLog::DEBUG, __FILE__, __LINE__, msg.str( ) );
            }
            throw;
        }
        catch ( ... )
        {
            std::ostringstream msg;
            msg << "Unknown exception for: " << DirInfo->DirName( );

            if ( StdErrLog.IsOpen( ) )
            {
                StdErrLog( ErrorLog::DEBUG, __FILE__, __LINE__, msg.str( ) );
            }
            throw;
        }

        return retval;
    }
} // namespace
