/***************************************************************************
**
**  This file is part of QGpCoreTools.
**
**  This library is free software; you can redistribute it and/or
**  modify it under the terms of the GNU Lesser General Public
**  License as published by the Free Software Foundation; either
**  version 2.1 of the License, or (at your option) any later version.
**
**  This file 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 Lesser General Public
**  License for more details.
**
**  You should have received a copy of the GNU Lesser General Public
**  License along with this library; if not, write to the Free Software
**  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
**
**  See http://www.geopsy.org for more information.
**
**  Created: 2007-06-20
**  Copyright: 2007-2019
**    Marc Wathelet
**    Marc Wathelet (LGIT, Grenoble, France)
**
***************************************************************************/

#include "ParallelLoop.h"
#include "CoreApplication.h"
#include "Global.h"
#include "Trace.h"
#include "MemoryChecker.h"

namespace QGpCoreTools {

  /*!
    \class LoopWorker ParallelLoop.h
    \brief Task of a parallel loop
  */

  /*!
    To check affinities:
    for i in $(pgrep geopsy-fk);do ps -mo pid,tid,fname,user,psr -p $i;done | tail -n 40
  */
  void LoopWorker::run()
  {
    TRACE;
    if(_affinity>=0) {
#ifdef Q_OS_LINUX
      cpu_set_t cpuset;
      pthread_t thread=pthread_self();
      CPU_ZERO(&cpuset);
      CPU_SET(_affinity, &cpuset);
      if(pthread_setaffinity_np(thread, sizeof(cpu_set_t), &cpuset)!=0) {
        App::log(tr("Thead affinity mask cannot be set to %1, skipping CPU affinity optimization\n").arg(_affinity));
      }
#endif
    }
    while(true) {
      if(terminated()) break;
      _indexMutex->MUTEX_LOCK;
      int i=*_index;
      (*_index)++;
      _indexMutex->unlock();
      if(i<_endIndex) {
        run(i);
      } else {
        break;
      }
    }
  }

  /*!
    \fn int LoopWorker::endIndex() const
    Returns the final index of the loop. It is the same for all children LoopWorker.
    This might be usefull to revert the order of execution in run(int index) implementations.
  */

  /*!
    \class ParallelLoop ParallelLoop.h
    \brief Class that helps to parallelize loops
  */

  /*!
    Initialize a loop. The parameters \a param will be provided as a pointer to each task.
  */
  ParallelLoop::ParallelLoop(QObject * parent)
    : QObject(parent)
  {
  }

  ParallelLoop::~ParallelLoop()
  {
    TRACE;
    for(QMap<LoopWorker *,TaskInfo>::iterator it=_workers.begin(); it!=_workers.end(); it++) {
      it.key()->QThread::terminate();
      delete (it.key());
    }
  }

  void ParallelLoop::terminate()
  {
    TRACE;
    for(QMap<LoopWorker *,TaskInfo>::iterator it=_workers.begin(); it!=_workers.end(); it++) {
      if(!it.key()->terminated()) it.key()->terminate();
    }
  }

  void ParallelLoop::workerFinished(LoopWorker * t)
  {
    TRACE;
    if(!t) {
      t=static_cast<LoopWorker *>(sender());
    }
    QMap<LoopWorker *,TaskInfo>::iterator it=_workers.find(t);
    if(it!=_workers.end()) {
      _workers.erase(it);
      delete t;
      if(_workers.isEmpty()) {
        emit finished();
      }
    }
  }

  void ParallelLoop::waitFinished()
  {
    while(!_workers.isEmpty()) {
      App::sleep(500);
      QCoreApplication::processEvents();
    }
  }

  inline LoopWorker * ParallelLoop::newWorker(int iEnd)
  {
    LoopWorker * w=newWorker();
    connect(w, SIGNAL(finished()), this, SLOT(workerFinished()));
    connect(w, SIGNAL(statusChanged(QString)), this, SLOT(statusChanged(QString)));
    connect(w, SIGNAL(progressInit(int)), this, SLOT(progressInit(int)));
    connect(w, SIGNAL(progressChanged(int)), this, SLOT(progressChanged(int)));
    w->setIndex(&_index, &_indexMutex);
    w->setEndIndex(iEnd);
    return w;
  }

  /*!
    Start loop from \a iStart to \a iEnd-1
  */
  void ParallelLoop::start(int iStart, int iEnd, bool forceParallel)
  {
    TRACE;
    int nThreads=idealThreadCount(iEnd-iStart+1);
    _index=iStart;
    LoopWorker * w;
    TaskInfo infos;
    if(nThreads<=1 && !forceParallel) {
      App::log(1, tr("Running loop without parallel threads\n"));
      w=newWorker(iEnd);
      infos.index=0;
      infos.maximumProgress=0;
      _workers.insert(w, infos);
      w->run();
      workerFinished(w);
    } else {
      App::log(1, tr("Running loop with %1 parallel threads\n").arg(nThreads));
      int affinityShift=0;
      for(int i=0; i<nThreads; i++) {
        //App::log("starting new thread\n");
        w=newWorker(iEnd);
        w->setObjectName("parallelLoop");
        infos.index=i;
        infos.maximumProgress=0;
        w->setObjectName(QString("LoopWorker_%1").arg(_workers.count()));
        _workers.insert(w, infos);
        w->setIndex(&_index, &_indexMutex);
        if(!Thread::cpuAffinities().isEmpty()) {
          w->setAffinity(Thread::cpuAffinities().at(i-affinityShift));
          APP_LOG(2, tr("Assign affinity %1 to task %2\n").arg(w->affinity()).arg(w->objectName()))
          if(i==Thread::cpuAffinities().count()-1) {
            affinityShift+=Thread::cpuAffinities().count();
          }
        }
        w->start();
        // for debug run them sequentially
        //t->run();
        //workerFinished(t);
      }
    }
  }

  void ParallelLoop::statusChanged(QString msg)
  {
    TRACE;
    LoopWorker * t=qobject_cast<LoopWorker *>(sender());
    QMap<LoopWorker *,TaskInfo>::iterator it=_workers.find(t);
    if(it!=_workers.end()) {
      emit statusChanged(it.value().index, msg);
    }
  }

  void ParallelLoop::progressChanged(int value)
  {
    TRACE;
    LoopWorker * t=qobject_cast<LoopWorker *>(sender());
    QMap<LoopWorker *,TaskInfo>::iterator it=_workers.find(t);
    if(it!=_workers.end()) {
      emit progressChanged(it.value().index, value);
    }
  }

  void ParallelLoop::progressInit(int maximumValue)
  {
    TRACE;
    LoopWorker * t=qobject_cast<LoopWorker *>(sender());
    QMap<LoopWorker *,TaskInfo>::iterator it=_workers.find(t);
    if(it!=_workers.end()) {
      // Store maximum value for workerFinished()
      if(it.value().maximumProgress!=maximumValue) {
        it.value().maximumProgress=maximumValue;
        emit progressInit(it.value().index, maximumValue);
      }
    }
  }

} // namespace QGpCoreTools
