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

#ifndef GENERAL__READ_WRITE_LOCK_BATON_CC
#define GENERAL__READ_WRITE_LOCK_BATON_CC

#include <ldastoolsal_config.h>

#include <pthread.h>

#include <cassert>

#include "ldastoolsal/DeadLockDetector.hh"

typedef struct timespec timespec_type;

#if HAVE_PTHREAD_RW_LOCK
#if defined( _POSIX_TIMEOUTS ) && ( _POSIX_TIMEOUTS - 200112L ) >= 0L
/* POSIX Timeouts are supported - option group [TMO] */
#if defined( _POSIX_THREADS ) && ( _POSIX_THREADS - 200112L ) >= 0L
/* POSIX threads are supported - option group [THR] */
#define HAVE_TIMED_LOCK_SUPPORT 1

namespace
{
    inline void
    normalize( timespec_type& Time )
    {
        static const long ns = 1000000000;
        if ( Time.tv_nsec > ns )
        {
            Time.tv_sec += Time.tv_sec / ns; // Add second component
            Time.tv_nsec = Time.tv_nsec % ns; // truncate seconds
        }
    }

    // #if defined(_POSIX_TIMEOUTS) && (_POSIX_TIMEOUTS - 200112L) >= 0L
    // #if defined(_POSIX_THREADS) && (_POSIX_THREADS - 200112L) >= 0L
    inline bool
    lt( const timespec_type& LHS, const timespec_type& RHS )
    {
        if ( LHS.tv_sec < RHS.tv_sec )
        {
            return true;
        }
        else if ( LHS.tv_sec > RHS.tv_sec )
        {
            return false;
        }
        else if ( LHS.tv_sec < RHS.tv_sec )
        {
            return true;
        }
        else
        {
            return false;
        }
    }

    inline timespec_type
    operator+=( timespec_type& LHS, const timespec_type& RHS )
    {
        // Add two values together
        LHS.tv_sec += RHS.tv_sec;
        LHS.tv_nsec += RHS.tv_nsec;
        normalize( LHS );
        return LHS;
    }
    // #endif /* defined(_POSIX_THREADS) && (_POSIX_THREADS - 200112L) >= 0L */
    // #endif /* defined(_POSIX_TIMEOUTS) && (_POSIX_TIMEOUTS - 200112L) >= 0L
    // */

    inline void
    now( timespec_type& Now )
    {
#if HAVE_CLOCK_GETTIME
        clock_gettime( CLOCK_REALTIME, &Now );
#else /* HAVE_CLOCK-GETTIME */
        {
            struct timeval tv;

            gettimeofday( &tv, NULL );

            Now.tv_sec = tv.tv_sec;
            Now.tv_nsec = tv.tv_usec * 1000;
        }
#endif /* HAVE_CLOCK_GETTIME */
    }
} // namespace

#endif /* */
#endif /* */
#else /* HAVE_PTHREAD_RW_LOCK */
//-----------------------------------------------------------------------
// This is the case where there is no read/write locks so implement
// using Mutex locking semantics.
//-----------------------------------------------------------------------

typedef pthread_mutex_t     pthread_rwlock_t;
typedef pthread_mutexattr_t pthread_rwlockattr_t;

inline int
pthread_rwlock_destroy( pthread_rwlock_t* Lock )
{
    return pthread_mutex_destroy( Lock );
}

inline int
pthread_rwlock_init( pthread_rwlock_t* Lock, void* Attr )
{
    return pthread_mutex_init( Lock,
                               static_cast< pthread_mutexattr_t* >( Attr ) );
}

inline int
pthread_rwlock_unlock( pthread_rwlock_t* Lock )
{
    return pthread_mutex_unlock( Lock );
}

inline int
pthread_rwlock_tryrdlock( pthread_rwlock_t* Lock )
{
    return pthread_mutex_trylock( Lock );
}

inline int
pthread_rwlock_trywrlock( pthread_rwlock_t* Lock )
{
    return pthread_mutex_trylock( Lock );
}

inline int
pthread_rwlock_rdlock( pthread_rwlock_t* Lock )
{
    return pthread_mutex_lock( Lock );
}

inline int
pthread_rwlock_wrlock( pthread_rwlock_t* Lock )
{
    return pthread_mutex_lock( Lock );
}

inline int
pthread_rwlock_timedrdlock( pthread_rwlock_t* Lock, timespec_type* Timeout )
{
    return pthread_mutex_timedlock( Lock, Timeout );
}

inline int
pthread_rwlock_timedwrlock( pthread_rwlock_t* Lock, timespec_type* Timeout )
{
    return pthread_mutex_timedlock( Lock, Timeout );
}

#undef PTHREAD_RWLOCK_INITIALIZER
#define PTHREAD_RWLOCK_INITIALIZER PTHREAD_MUTEX_INITIALIZER

#endif /* HAVE_PTHREAD_RW_LOCK */

namespace LDASTools
{
    namespace AL
    {
        class ReadWriteLock::baton_type::impl
        {
        public:
            bool logging;

            static timespec_type LockAcquisitionInterval;

            impl( bool Logging ) : logging( Logging )
            {
                pthread_rwlock_init(
                    *this, static_cast< pthread_rwlockattr_t* >( NULL ) );
            }

            inline void
            Lock( mode_type Mode, bool TryLock )
            {
                int err = 0;

                switch ( Mode )
                {
                case NONE:
                    /// \todo
                    ///    Should never be trying to set the lock to NONE;
                    ///    should throw an exception.
                    break;
                case READ:
                    err = ( ( TryLock ) ? pthread_rwlock_tryrdlock( *this )
                                        : pthread_rwlock_rdlock( *this ) );
                    break;
                case WRITE:
                    err = ( ( TryLock ) ? pthread_rwlock_trywrlock( *this )
                                        : pthread_rwlock_wrlock( *this ) );
                    break;
                }
                error( err );
            }

            inline void
            Lock( mode_type Mode, size_t Timeout )
            {
                int err = 0;

                if ( Timeout == 0 )
                {
                    //---------------------------------------------------------------
                    // Wait for lock
                    //---------------------------------------------------------------
                    Lock( Mode, false /* TryLock */ );
                }
                else
                {
                    //---------------------------------------------------------------
                    // Non-Blocking mode
                    //---------------------------------------------------------------
#if HAVE_TIMED_LOCK_SUPPORT
                    timespec_type stop;
                    size_t        iteration = 0;

                    now( stop );
                    stop.tv_sec += Timeout / 1000;
                    stop.tv_nsec += ( Timeout % 1000 ) * 1000000;
                    normalize( stop );

                    while ( true )
                    {
                        try
                        {
                            // log_lock_extended( info,
                            // DeadLockDetector::PENDING, 0 );

                            timespec_type timeout;

                            now( timeout );

                            if ( lt( stop, timeout ) )
                            {
                                throw std::runtime_error( "TIMEDOUT" );
                            }
                            timeout += LockAcquisitionInterval;

                            switch ( Mode )
                            {
                            case lock_mode::NONE:
                                break;
                            case lock_mode::READ:
                                err = pthread_rwlock_timedrdlock( *this,
                                                                  &timeout );
                                break;
                            case lock_mode::WRITE:
                                err = pthread_rwlock_timedwrlock( *this,
                                                                  &timeout );
                                break;
                            }
                            if ( err == 0 )
                            {
                                break;
                            }
                            iteration++;
                        }
                        catch ( ... )
                        {
                            err = ETIMEDOUT;
                            break;
                        }
                    } // while
#else /* HAVE_TIMED_LOCK_SUPPORT */
                    Lock( Mode, false /* TryLock */ );
#endif /* HAVE_TIMED_LOCK_SUPPORT */
                } // if blocking/non-blocking
                error( err );
            }

            inline bool
            Logging( ) const
            {
                return logging;
            }

            void
            Unlock( )
            {
                int err = pthread_rwlock_unlock( *this );

                error( err );
            }

            operator pthread_rwlock_t*( )
            {
                return &handle;
            }

            operator void*( )
            {
                return &handle;
            }

        private:
            mutable pthread_rwlock_t handle;

            inline void
            error( int Code )
            {
                switch ( Code )
                {
                case 0:
                    // No error
                    break;
                case EBUSY:
                    throw ReadWriteLock::BusyError( );
                    break;
                case EDEADLK:
                    throw ReadWriteLock::DeadLockError( );
                    break;
                case ETIMEDOUT:
                    throw ReadWriteLock::TimedoutError( );
                    break;
                default:
                {
                    std::ostringstream msg;

                    msg << /* Action << ": " << */ strerror( Code );
                    throw std::runtime_error( msg.str( ) );
                }
                break;
                }
            }
        };
    } // namespace AL
} // namespace LDASTools

#endif /* GENERAL__READ_WRITE_LOCK_BATON_CC */
