///////////////////////////////////////////////////////////////////////////
//
// Copyright (c) 2005-2012, Industrial Light & Magic, a division of Lucas
// Digital Ltd. LLC
// 
// All rights reserved.
// 
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
// *       Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// *       Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// *       Neither the name of Industrial Light & Magic nor the names of
// its contributors may be used to endorse or promote products derived
// from this software without specific prior written permission. 
// 
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
///////////////////////////////////////////////////////////////////////////

//-----------------------------------------------------------------------------
//
//  class Task, class ThreadPool, class TaskGroup
//
//-----------------------------------------------------------------------------

#include "IlmThread.h"
#include "IlmThreadMutex.h"
#include "IlmThreadSemaphore.h"
#include "IlmThreadPool.h"
#include "Iex.h"
#include <vector>
#ifndef ILMBASE_FORCE_CXX03
# include <memory>
# include <atomic>
# include <thread>
#else
# ifndef _WIN32
#  include <unistd.h>
# endif
#endif

using namespace std;

ILMTHREAD_INTERNAL_NAMESPACE_SOURCE_ENTER

#if defined(__GNU_LIBRARY__) && ( __GLIBC__ < 2 || ( __GLIBC__ == 2 && __GLIBC_MINOR__ < 21 ) )
# define ENABLE_SEM_DTOR_WORKAROUND
#endif

struct TaskGroup::Data
{
     Data ();
    ~Data ();
    
    void    addTask () ;
    void    removeTask ();
#ifndef ILMBASE_FORCE_CXX03
    std::atomic<int> numPending;
#else
    int              numPending;     // number of pending tasks to still execute
#endif
    Semaphore        isEmpty;        // used to signal that the taskgroup is empty
#if defined(ENABLE_SEM_DTOR_WORKAROUND) || defined(ILMBASE_FORCE_CXX03)
    // this mutex is also used to lock numPending in the legacy c++ mode...
    Mutex            dtorMutex;      // used to work around the glibc bug:
                                     // http://sources.redhat.com/bugzilla/show_bug.cgi?id=12674
#endif
};


struct ThreadPool::Data
{
    typedef ThreadPoolProvider *TPPointer;

     Data ();
    ~Data();
    Data (const Data&) = delete;
    Data &operator= (const Data&)  = delete;
    Data (Data&&) = delete;
    Data &operator= (Data&&)  = delete;

    struct SafeProvider
    {
        SafeProvider (Data *d, ThreadPoolProvider *p) : _data( d ), _ptr( p )
        {
        }

        ~SafeProvider()
        {
            if ( _data )
                _data->coalesceProviderUse();
        }
        SafeProvider (const SafeProvider &o)
            : _data( o._data ), _ptr( o._ptr )
        {
            if ( _data )
                _data->bumpProviderUse();
        }
        SafeProvider &operator= (const SafeProvider &o)
        {
            if ( this != &o )
            {
                if ( o._data )
                    o._data->bumpProviderUse();
                if ( _data )
                    _data->coalesceProviderUse();
                _data = o._data;
                _ptr = o._ptr;
            }
            return *this;
        }
#ifndef ILMBASE_FORCE_CXX03
        SafeProvider( SafeProvider &&o )
            : _data( o._data ), _ptr( o._ptr )
        {
            o._data = nullptr;
        }
        SafeProvider &operator=( SafeProvider &&o )
        {
            std::swap( _data, o._data );
            std::swap( _ptr, o._ptr );
            return *this;
        }
#endif
        inline ThreadPoolProvider *get () const
        {
            return _ptr;
        }
        ThreadPoolProvider *operator-> () const
        {
            return get();
        }

        Data *_data;
        ThreadPoolProvider *_ptr;
    };

    // NB: In C++20, there is full support for atomic shared_ptr, but that is not
    // yet in use or finalized. Once stabilized, add appropriate usage here
    inline SafeProvider getProvider ();
    inline void coalesceProviderUse ();
    inline void bumpProviderUse ();
    inline void setProvider (ThreadPoolProvider *p);

#ifdef ILMBASE_FORCE_CXX03
    Semaphore provSem;
    Mutex provMutex;
    int provUsers;
    ThreadPoolProvider *provider;
    ThreadPoolProvider *oldprovider;
#else
    std::atomic<int> provUsers;
    std::atomic<ThreadPoolProvider *> provider;
#endif
};



namespace {

class DefaultWorkerThread;

struct DefaultWorkData
{
    Semaphore taskSemaphore;        // threads wait on this for ready tasks
    mutable Mutex taskMutex;        // mutual exclusion for the tasks list
    vector<Task*> tasks;            // the list of tasks to execute

    Semaphore threadSemaphore;      // signaled when a thread starts executing
    mutable Mutex threadMutex;      // mutual exclusion for threads list
    vector<DefaultWorkerThread*> threads;  // the list of all threads
    
#ifdef ILMBASE_FORCE_CXX03
    bool stopping;                  // flag indicating whether to stop threads
    mutable Mutex stopMutex;        // mutual exclusion for stopping flag
#else
    std::atomic<bool> hasThreads;
    std::atomic<bool> stopping;
#endif

    inline bool stopped () const
    {
#ifdef ILMBASE_FORCE_CXX03
        Lock lock (stopMutex);
        return stopping;
#else
        return stopping.load( std::memory_order_relaxed );
#endif
    }

    inline void stop ()
    {
#ifdef ILMBASE_FORCE_CXX03
        Lock lock (stopMutex);
#endif
        stopping = true;
    }
};

//
// class WorkerThread
//
class DefaultWorkerThread: public Thread
{
  public:

    DefaultWorkerThread (DefaultWorkData* data);

    virtual void    run ();
    
  private:

    DefaultWorkData *  _data;
};


DefaultWorkerThread::DefaultWorkerThread (DefaultWorkData* data):
    _data (data)
{
    start();
}


void
DefaultWorkerThread::run ()
{
    //
    // Signal that the thread has started executing
    //

    _data->threadSemaphore.post();

    while (true)
    {
        //
        // Wait for a task to become available
        //

        _data->taskSemaphore.wait();

        {
            Lock taskLock (_data->taskMutex);
    
            //
            // If there is a task pending, pop off the next task in the FIFO
            //

            if (!_data->tasks.empty())
            {
                Task* task = _data->tasks.back();
                _data->tasks.pop_back();
                taskLock.release();

                TaskGroup* taskGroup = task->group();
                task->execute();

                delete task;

                taskGroup->_data->removeTask ();
            }
            else if (_data->stopped())
            {
                break;
            }
        }
    }
}


//
// class DefaultThreadPoolProvider
//
class DefaultThreadPoolProvider : public ThreadPoolProvider
{
  public:
    DefaultThreadPoolProvider(int count);
    virtual ~DefaultThreadPoolProvider();

    virtual int numThreads() const;
    virtual void setNumThreads(int count);
    virtual void addTask(Task *task);

    virtual void finish();

  private:
    DefaultWorkData _data;
};

DefaultThreadPoolProvider::DefaultThreadPoolProvider (int count)
{
    setNumThreads(count);
}

DefaultThreadPoolProvider::~DefaultThreadPoolProvider ()
{
    finish();
}

int
DefaultThreadPoolProvider::numThreads () const
{
    Lock lock (_data.threadMutex);
    return static_cast<int> (_data.threads.size());
}

void
DefaultThreadPoolProvider::setNumThreads (int count)
{
    //
    // Lock access to thread list and size
    //

    Lock lock (_data.threadMutex);

    size_t desired = static_cast<size_t>(count);
    if (desired > _data.threads.size())
    {
        //
        // Add more threads
        //

        while (_data.threads.size() < desired)
            _data.threads.push_back (new DefaultWorkerThread (&_data));
    }
    else if ((size_t)count < _data.threads.size())
    {
        //
        // Wait until all existing threads are finished processing,
        // then delete all threads.
        //
        finish ();

        //
        // Add in new threads
        //

        while (_data.threads.size() < desired)
            _data.threads.push_back (new DefaultWorkerThread (&_data));
    }
#ifndef ILMBASE_FORCE_CXX03
    _data.hasThreads = !(_data.threads.empty());
#endif
}

void
DefaultThreadPoolProvider::addTask (Task *task)
{
    //
    // Lock the threads, needed to access numThreads
    //
#ifdef ILMBASE_FORCE_CXX03
    bool doPush;
    {
        Lock lock (_data.threadMutex);
        doPush = !_data.threads.empty();
    }
#else
    bool doPush = _data.hasThreads.load( std::memory_order_relaxed );
#endif

    if ( doPush )
    {
        //
        // Get exclusive access to the tasks queue
        //

        {
            Lock taskLock (_data.taskMutex);

            //
            // Push the new task into the FIFO
            //
            _data.tasks.push_back (task);
        }
        
        //
        // Signal that we have a new task to process
        //
        _data.taskSemaphore.post ();
    }
    else
    {
        // this path shouldn't normally happen since we have the
        // NullThreadPoolProvider, but just in case...
        task->execute ();
        task->group()->_data->removeTask ();
        delete task;
    }
}

void
DefaultThreadPoolProvider::finish ()
{
    _data.stop();

    //
    // Signal enough times to allow all threads to stop.
    //
    // Wait until all threads have started their run functions.
    // If we do not wait before we destroy the threads then it's
    // possible that the threads have not yet called their run
    // functions.
    // If this happens then the run function will be called off
    // of an invalid object and we will crash, most likely with
    // an error like: "pure virtual method called"
    //

    size_t curT = _data.threads.size();
    for (size_t i = 0; i != curT; ++i)
    {
        _data.taskSemaphore.post();
        _data.threadSemaphore.wait();
    }

    //
    // Join all the threads
    //
    for (size_t i = 0; i != curT; ++i)
        delete _data.threads[i];

    Lock lock1 (_data.taskMutex);
#ifdef ILMBASE_FORCE_CXX03
    Lock lock2 (_data.stopMutex);
#endif
    _data.threads.clear();
    _data.tasks.clear();

    _data.stopping = false;
}


class NullThreadPoolProvider : public ThreadPoolProvider
{
    virtual ~NullThreadPoolProvider() {}
    virtual int numThreads () const { return 0; }
    virtual void setNumThreads (int count)
    {
    }
    virtual void addTask (Task *t)
    {
        t->execute ();
        t->group()->_data->removeTask ();
        delete t;
    }
    virtual void finish () {}
}; 

} //namespace


//
// struct TaskGroup::Data
//

TaskGroup::Data::Data () : numPending (0), isEmpty (1)
{
    // empty
}


TaskGroup::Data::~Data ()
{
    //
    // A TaskGroup acts like an "inverted" semaphore: if the count
    // is above 0 then waiting on the taskgroup will block.  This
    // destructor waits until the taskgroup is empty before returning.
    //

    isEmpty.wait ();

#ifdef ENABLE_SEM_DTOR_WORKAROUND
    // Update: this was fixed in v. 2.2.21, so this ifdef checks for that
    //
    // Alas, given the current bug in glibc we need a secondary
    // syncronisation primitive here to account for the fact that
    // destructing the isEmpty Semaphore in this thread can cause
    // an error for a separate thread that is issuing the post() call.
    // We are entitled to destruct the semaphore at this point, however,
    // that post() call attempts to access data out of the associated
    // memory *after* it has woken the waiting threads, including this one,
    // potentially leading to invalid memory reads.
    // http://sources.redhat.com/bugzilla/show_bug.cgi?id=12674

    Lock lock (dtorMutex);
#endif
}


void
TaskGroup::Data::addTask () 
{
    //
    // in c++11, we use an atomic to protect numPending to avoid the
    // extra lock but for c++98, to add the ability for custom thread
    // pool we add the lock here
    //
#ifdef ILMBASE_FORCE_CXX03
    Lock lock (dtorMutex);
#endif
    if (numPending++ == 0)
        isEmpty.wait ();
}


void
TaskGroup::Data::removeTask ()
{
    // Alas, given the current bug in glibc we need a secondary
    // syncronisation primitive here to account for the fact that
    // destructing the isEmpty Semaphore in a separate thread can
    // cause an error. Issuing the post call here the current libc
    // implementation attempts to access memory *after* it has woken
    // waiting threads.
    // Since other threads are entitled to delete the semaphore the
    // access to the memory location can be invalid.
    // http://sources.redhat.com/bugzilla/show_bug.cgi?id=12674
    // Update: this bug has been fixed, but how do we know which
    // glibc version we're in?

    // Further update:
    //
    // we could remove this if it is a new enough glibc, however 
    // we've changed the API to enable a custom override of a
    // thread pool. In order to provide safe access to the numPending,
    // we need the lock anyway, except for c++11 or newer
#ifdef ILMBASE_FORCE_CXX03
    Lock lock (dtorMutex);

    if (--numPending == 0)
        isEmpty.post ();
#else
    if (--numPending == 0)
    {
#ifdef ENABLE_SEM_DTOR_WORKAROUND
        Lock lock (dtorMutex);
#endif
        isEmpty.post ();
    }
#endif
}
    

//
// struct ThreadPool::Data
//

ThreadPool::Data::Data ():
    provUsers (0), provider (NULL)
#ifdef ILMBASE_FORCE_CXX03
    , oldprovider (NULL)
#else
#endif
{
    // empty
}


ThreadPool::Data::~Data()
{
#ifdef ILMBASE_FORCE_CXX03
    provider->finish();
    delete provider;
#else
    ThreadPoolProvider *p = provider.load( std::memory_order_relaxed );
    p->finish();
    delete p;
#endif
}

inline ThreadPool::Data::SafeProvider
ThreadPool::Data::getProvider ()
{
#ifdef ILMBASE_FORCE_CXX03
    Lock provLock( provMutex );
    ++provUsers;
    return SafeProvider( this, provider );
#else
    provUsers.fetch_add( 1, std::memory_order_relaxed );
    return SafeProvider( this, provider.load( std::memory_order_relaxed ) );
#endif
}


inline void
ThreadPool::Data::coalesceProviderUse ()
{
#ifdef ILMBASE_FORCE_CXX03
    Lock provLock( provMutex );
    --provUsers;
    if ( provUsers == 0 )
    {
        if ( oldprovider )
            provSem.post();
    }
#else
    int ov = provUsers.fetch_sub( 1, std::memory_order_relaxed );
    // ov is the previous value, so one means that now it might be 0
    if ( ov == 1 )
    {
        
    }
#endif
}


inline void
ThreadPool::Data::bumpProviderUse ()
{
#ifdef ILMBASE_FORCE_CXX03
    Lock lock (provMutex);
    ++provUsers;
#else
    provUsers.fetch_add( 1, std::memory_order_relaxed );
#endif
}


inline void
ThreadPool::Data::setProvider (ThreadPoolProvider *p)
{
#ifdef ILMBASE_FORCE_CXX03
    Lock provLock( provMutex );

    if ( oldprovider )
        throw IEX_INTERNAL_NAMESPACE::ArgExc ("Attempt to set the thread pool provider while"
                                              " another thread is currently setting the provider.");

    oldprovider = provider;
    provider = p;

    while ( provUsers > 0 )
    {
        provLock.release();
        provSem.wait();
        provLock.acquire();
    }
    if ( oldprovider )
    {
        oldprovider->finish();
        delete oldprovider;
        oldprovider = NULL;
    }
#else
    ThreadPoolProvider *old = provider.load( std::memory_order_relaxed );
    // work around older gcc bug just in case
    do
    {
        if ( ! provider.compare_exchange_weak( old, p, std::memory_order_release, std::memory_order_relaxed ) )
            continue;
    } while (false); // NOSONAR - suppress SonarCloud bug report.

    // wait for any other users to finish prior to deleting, given
    // that these are just mostly to query the thread count or push a
    // task to the queue (so fast), just spin...
    //
    // (well, and normally, people don't do this mid stream anyway, so
    // this will be 0 99.999% of the time, but just to be safe)
    // 
    while ( provUsers.load( std::memory_order_relaxed ) > 0 )
        std::this_thread::yield();

    if ( old )
    {
        old->finish();
        delete old;
    }

    // NB: the shared_ptr mechanism is safer and means we don't have
    // to have the provUsers counter since the shared_ptr keeps that
    // for us. However, gcc 4.8/9 compilers which many people are
    // still using even though it is 2018 forgot to add the shared_ptr
    // functions... once that compiler is fully deprecated, switch to
    // using the below, change provider to a std::shared_ptr and remove
    // provUsers...
    //
//    std::shared_ptr<ThreadPoolProvider> newp( p );
//    std::shared_ptr<ThreadPoolProvider> curp = std::atomic_load_explicit( &provider, std::memory_order_relaxed );
//    do
//    {
//        if ( ! std::atomic_compare_exchange_weak_explicit( &provider, &curp, newp, std::memory_order_release, std::memory_order_relaxed ) )
//            continue;
//    } while ( false );
//    if ( curp )
//        curp->finish();
#endif
}

//
// class Task
//

Task::Task (TaskGroup* g): _group(g)
{
    if ( g )
        g->_data->addTask ();
}


Task::~Task()
{
    // empty
}


TaskGroup*
Task::group ()
{
    return _group;
}


TaskGroup::TaskGroup ():
    _data (new Data())
{
    // empty
}


TaskGroup::~TaskGroup ()
{
    delete _data;
}


void
TaskGroup::finishOneTask ()
{
    _data->removeTask ();
}

//
// class ThreadPoolProvider
//


ThreadPoolProvider::ThreadPoolProvider()
{
}


ThreadPoolProvider::~ThreadPoolProvider()
{
}


//
// class ThreadPool
//

ThreadPool::ThreadPool (unsigned nthreads):
    _data (new Data)
{
    if ( nthreads == 0 )
        _data->setProvider( new NullThreadPoolProvider );
    else
        _data->setProvider( new DefaultThreadPoolProvider( int(nthreads) ) );
}


ThreadPool::~ThreadPool ()
{
    delete _data;
}


int
ThreadPool::numThreads () const
{
    return _data->getProvider ()->numThreads ();
}


void
ThreadPool::setNumThreads (int count)
{
    if (count < 0)
        throw IEX_INTERNAL_NAMESPACE::ArgExc ("Attempt to set the number of threads "
               "in a thread pool to a negative value.");

    bool doReset = false;
    {
        Data::SafeProvider sp = _data->getProvider ();
        int curT = sp->numThreads ();
        if ( curT == count )
            return;

        if ( curT == 0 )
        {
            NullThreadPoolProvider *npp = dynamic_cast<NullThreadPoolProvider *>( sp.get() );
            if ( npp )
                doReset = true;
        }
        else if ( count == 0 )
        {
            DefaultThreadPoolProvider *dpp = dynamic_cast<DefaultThreadPoolProvider *>( sp.get() );
            if ( dpp )
                doReset = true;
        }
        if ( ! doReset )
            sp->setNumThreads( count );
    }

    if ( doReset )
    {
        if ( count == 0 )
            _data->setProvider( new NullThreadPoolProvider );
        else
            _data->setProvider( new DefaultThreadPoolProvider( count ) );
    }
}


void
ThreadPool::setThreadProvider (ThreadPoolProvider *provider)
{
    _data->setProvider (provider);
}


void
ThreadPool::addTask (Task* task) 
{
    _data->getProvider ()->addTask (task);
}


ThreadPool&
ThreadPool::globalThreadPool ()
{
    //
    // The global thread pool
    //
    
    static ThreadPool gThreadPool (0);

    return gThreadPool;
}


void
ThreadPool::addGlobalTask (Task* task)
{
    globalThreadPool().addTask (task);
}

unsigned
ThreadPool::estimateThreadCountForFileIO ()
{
#ifdef ILMBASE_FORCE_CXX03
#    if defined (_WIN32) || defined (_WIN64)
    SYSTEM_INFO sysinfo;
    GetSystemInfo (&sysinfo);
    return static_cast<unsigned> (sysinfo.dwNumberOfProcessors);
#    elif defined(_SC_NPROCESSORS_ONLN)
    int count = sysconf (_SC_NPROCESSORS_ONLN);
    return static_cast<unsigned>( count < 0 ? 0 : count );
#    else
    return 0;
#    endif
#else
    return std::thread::hardware_concurrency ();
#endif
}

ILMTHREAD_INTERNAL_NAMESPACE_SOURCE_EXIT
