/***************************************************************************
**
**  This file is part of dinver.
**
**  dinver is free software: you can redistribute it and/or modify
**  it under the terms of the GNU General Public License as published by
**  the Free Software Foundation, either version 3 of the License, or
**  (at your option) any later version.
**
**  dinver 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 General Public License for more details.
**
**  You should have received a copy of the GNU General Public License
**  along with Foobar.  If not, see <http://www.gnu.org/licenses/>
**
**  See http://www.geopsy.org for more information.
**
**  Created: 2004-10-19
**  Copyright: 2004-2019
**    Marc Wathelet
**    Marc Wathelet (ULg, Liège, Belgium)
**    Marc Wathelet (LGIT, Grenoble, France)
**
***************************************************************************/

#include <DinverCore.h>
#include <QGpGuiTools.h>

#include "WindowEnvironment.h"
#include "MainWindow.h"
#include "InversionThread.h"
#include "ModelThreadInfo.h"

const QString InversionThread::xmlInversionThreadTag="InversionThread";

/*!
  \class InversionThread InversionThread.h
  \brief Inversion thread with Neighbourhood tuning parameters


*/

InversionThread::InversionThread(QObject * parent)
   : Thread(parent)
{
  TRACE;
  // Create log stream for this thread
  MainWindow * w=WindowEnvironment::window(this);
  ASSERT(w);
  w->logs()->addView(this, "unamed");

  _itmax=0; // Kept for compatibility, required when importing an old report file
  _ns0=50;
  _ns=10000;
  _nr=50;
  _nw=2;
  _giveUp=0.9;
  _hasModelsToImport=false;
  _importOnly=false;
  _reportValidModelCount=0;
  _reportBestMisfit=0.0;
}

InversionThread::~InversionThread()
{
  // Remove log stream for this thread
  WindowEnvironment::window(this)->logs()->removeView(this);
}

/*!
  Removes all files from disk.
*/
void InversionThread::removeFiles(bool reportOnly)
{
  TRACE;
  QFileInfo fi(_reportFileName);
  QDir d(fi.path());
  QString fn;
  // Remove report file
  fn=objectName()+".report";
  App::log(tr("Removing %1\n").arg(fn) );
  d.remove(fn);
  if(!reportOnly) {
    // Remove parameter file
    fn=objectName()+".param";
    App::log(tr("Removing %1\n").arg(fn) );
    d.remove(fn);
    // Remove target file
    fn=objectName()+".target";
    App::log(tr("Removing %1\n").arg(fn) );
    d.remove(fn);
  }
}

/*!
  \internal
  Rename report file (and attached files).

  Used only by setObjectName(). The argument is rather counter intuitive.
  This is effectively the old name that must be provided. The name of the
  report file is already changed when entering this function.
*/
void InversionThread::renameFiles(QDir& d, const QString& oldName)
{
  TRACE;
  QString fn, oldfn;
  // Rename report file
  oldfn=oldName+".report";
  fn=objectName()+".report";
  App::log(tr("Renaming %1 to %2\n").arg(oldfn).arg(fn) );
  d.rename(oldfn, fn);
  // Rename parameter file
  oldfn=oldName+".param";
  fn=objectName()+".param";
  App::log(tr("Renaming %1 to %2\n").arg(oldfn).arg(fn) );
  d.rename(oldfn, fn);
  // Rename target file
  oldfn=oldName+".target";
  fn=objectName()+".target";
  App::log(tr("Renaming %1 to %2\n").arg(oldfn).arg(fn) );
  d.rename(oldfn, fn);
}

void InversionThread::start()
{
  TRACE;
  if(_importOnly) {
    App::log(this, tr("\n---------------------- Getting information at ")
                   +QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")+"\n\n"
                   +tr(" Dimension of parameter space        =%1\n").arg(_models.variableParameterCount())
                   +tr(" Model file                          =%1\n\n").arg(_reportFileName));
  } else {
    App::log(this, tr("\n---------------------- Starting at ")
                   +QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")+"\n\n"
                   +tr(" Num samples Monte-Carlo             =%1\n").arg(_ns0)
                   +tr(" Num samples Neighborhood            =%1\n").arg(_ns)
                   +tr(" Num cells to resample               =%1\n").arg(_nr)
                   +tr(" Num of walks before accepting model =%1\n").arg(_nw)
                   +tr(" Max reject ratio before cell give up=%1\n").arg(_giveUp)
                   +tr(" Dimension of parameter space        =%1\n").arg(_models.variableParameterCount())
                   +tr(" Parameterization checksum           =%1\n").arg(_models.checksum())
                   +tr(" Total number of models              =%1\n").arg(_models.expectedModelCount())
                   +tr(" Model file                          =%1\n\n").arg(_reportFileName));
    if(_models.variableParameterCount()==0) {
      App::log(this, tr("Number of variable parameters is null, nothing to optimize, aborting.\n"));
      emit stopped();
      return;
    }
  }
  _models.init();
  QThread::start();
  //run(); // for debug only, to run it without threading
}

bool InversionThread::setForward(AbstractForward * forward)
{
  if(!_models.setForward(forward)) {
    return false;
  }

  MainWindow * w=WindowEnvironment::window(this);
  QFileInfo fi(_reportFileName);
  QDir d(fi.path());
  w->exportTargets(d.absoluteFilePath(objectName()+".target"));
  w->exportParameters(d.absoluteFilePath(objectName()+".param"));
  return true;
}

void InversionThread::setStorage()
{
  _models.setStorage();
}

bool InversionThread::setObjectName(QString name)
{
  TRACE;
  if(name==objectName()) {
    return false;
  }
  if(isRunning()) {
    App::log(tr("run %1: cannot change the name of a running inversion.\n")
                     .arg(objectName()));
    return false;
  }
  // Check if name is unique
  const ThreadList threads=WindowEnvironment::window(this)->threads();
  for(ThreadList::const_iterator it=threads.begin(); it<threads.end(); it++ ) {
    if((*it)->objectName()==name) {
      App::log(tr("The new name for run %1 is not unique, nothing changed.\n").arg(objectName()));
      return false;
    }
  }
  QString oldName=objectName();
  QObject::setObjectName(name);
  QFileInfo fi(_reportFileName);
  QDir d(fi.path());
  _reportFileName=d.absoluteFilePath(name+".report");
  renameFiles(d, oldName);
  return true;
}

bool InversionThread::setReportDir(const QDir& d)
{
  TRACE;
  QString newFileName=d.absoluteFilePath(objectName()+".report");
  if(_reportFileName.isEmpty()) {
    ASSERT(!isRunning());
    _reportFileName=newFileName;
    return true;
  } else {
    if(newFileName!=_reportFileName) {
      if(isRunning()) {
        App::log(tr("%1 is running, cannot copy report to %2.\n")
                    .arg(objectName()).arg(d.absolutePath()));
        return false;
      } else {
        QFileInfo fi(_reportFileName);
        if(fi.exists()) {
          QDir oldDir(fi.path());
          if(oldDir!=d) { // Normally always true, objectName() is not changed here
                          // if newFileName!=_reportFileName, then direcotries have changed
            QFile::copy(_reportFileName, newFileName);
            QString target=objectName()+".target";
            QString param=objectName()+".param";
            QFile::copy(oldDir.absoluteFilePath(target), d.absoluteFilePath(target));
            QFile::copy(oldDir.absoluteFilePath(param), d.absoluteFilePath(param));
          }
        }
        _reportFileName=newFileName;
        return true;
      }
    } else {
      return true;
    }
  }
}

QDir InversionThread::reportDir() const
{
  TRACE;
  QFileInfo fi(_reportFileName);
  return fi.absoluteDir();
}

void InversionThread::setTuningParameters(InversionThread * t)
{
  TRACE;
  setNs0(t->ns0());
  setNs(t->ns());
  setNr(t->nr());
  setNw(t->nw());
  setGiveUp(t->giveUp());
}

void InversionThread::terminate()
{
  TRACE;
  if(isRunning()) {
    _models.stop();
    wait();
    App::log(this, tr("Thread manually stopped.\n"));
  } else {
    App::log(this, tr("WARNING: Trying to stop it but thread is not running\n"));
  }
}

void InversionThread::clear()
{
  TRACE;
  if(isRunning()) {
    terminate();
  }
  removeFiles(false);
  _models.clear();
  _hasModelsToImport=false;
  _reportBestMisfit=0.0;
  App::log(this, tr("Thread cleared.\n"));
}

void InversionThread::run()
{
  TRACE;
  emit started();
  MessageContext();
  // Effectively import models if there are available in my report
  if(_hasModelsToImport) {
    _models.importModels(_reportFileName, false);
    if(_importOnly) {
      _reportValidModelCount=_models.validModelCount();
      _reportBestMisfit=_models.misfit(_models.bestModelIndex());
      _models.clear();
    } else {
      _hasModelsToImport=false;
    }
  }
  if(_importOnly) {
    _importOnly=false;
    return; // Do not emit stop because process was not started through ThreadLauncher
  }
  if(!_reportFileName.isEmpty()) {
    if(!_models.openReport(_reportFileName)) {
      emit stopped();
      return;
    }
  }
  Application::instance()->setStreamPrefix(QString());
  App::log(tr("Initialization of parameter space...\n") );
  _models.setBestModelCount(_nr);
  _models.setWalkCount(_nw);
  _models.setGiveUp(_giveUp);
  _models.setMaximumQueueLength(50);
  if(_ns0>0) {
    App::log(tr("Generating %1 models randomly...\n").arg(_ns0));
    _models.setMaximumModelCount(_models.validModelCount()+_ns0);
    _models.start(Thread::idealThreadCount(), Generator::MonteCarlo);
    _models.wait();
    App::log(tr("Generated %1 models randomly\n").arg(_ns0));
  }
  if(_ns>0) {
    if(_models.validModelCount()>0) {
      Application::instance()->setStreamPrefix(QString());
      App::log(tr("Generating %1 models with Neighborhood...\n").arg(_ns));
      _models.setMaximumModelCount(_models.validModelCount()+_ns);
      _models.start(Thread::idealThreadCount(), Generator::Neighborhood);
      _models.wait();
      _models.printActiveModels();
      App::log(tr("Generated %1 models with Neighborhood\n").arg(_ns));
    } else {
      App::log(tr("ERROR: at least one valid model is required to start neighborhood.\n"));
    }
  }
  Application::instance()->setStreamPrefix(QString());
  //printBestModels();
  emit stopped();
}

/*!
  Check if the run has an attached report and if models can be imported
  from it. Models will be imported only if the run is started.
*/
void InversionThread::checkImportModels()
{
  StreamRedirection sr(App::stream(this));
  QFileInfo fi(_reportFileName);
  if(fi.exists()) {
    _hasModelsToImport=true;
  } else {
    // Try to translate the path
    PathTranslatorOptions options;
    options.setTitle(tr("Loading reports..."));
    options.setFileType(tr("Report file (%1)"));
    QString fn=Application::instance()->translatePath(_reportFileName, options);
    fi.setFile(fn);
    if(!fn.isEmpty() && fi.exists()) {
      _reportFileName=fn;
      _hasModelsToImport=true;
    }
  }
}


/*!
  \fn bool InversionThread::isReportExists()
  Check whether or not the report file exist.
*/

void InversionThread::xml_writeProperties(XML_WRITEPROPERTIES_ARGS) const
{
  TRACE;
  writeProperty(s, "ns0",_ns0);
  writeProperty(s, "ns",_ns);
  writeProperty(s, "nr",_nr);
  writeProperty(s, "nw",_nw);
  writeProperty(s, "giveUp",_giveUp);
  writeProperty(s, "report",reportFileName());
  writeProperty(s, "checksum",(int)_models.checksum());
  _models.forward()->xml_writeProperties(s, context);
}

void InversionThread::xml_writeChildren(XML_WRITECHILDREN_ARGS) const
{
  TRACE;
  _models.forward()->xml_writeChildren(s, context);
}

XMLMember InversionThread::xml_member(XML_MEMBER_ARGS)
{
  TRACE;
  if(tag=="ns0") return XMLMember(1);
  else if(tag=="ns") return XMLMember(2);
  else if(tag=="nr") return XMLMember(3);
  else if(tag=="nw") return XMLMember(4);
  else if(tag=="giveUp") return XMLMember(5);
  else if(tag=="report") return XMLMember(7);
  else if(tag=="checksum") return XMLMember(9);
  else if(tag=="hasReportFile") return XMLMember(10);   // Kept for compatibility
  else if(tag=="itmax") return XMLMember(0);            // Kept for compatibility
  else if(tag=="seed") return XMLMember(8);             // Kept for compatibility
  else if(tag=="runType") return XMLMember(8);          // Kept for compatibility
  else if(tag=="jointRun") return XMLMember(8);         // Kept for compatibility
  else if(tag=="acceptableMisfit") return XMLMember(8); // Kept for compatibility
  else if(tag=="scaling") return XMLMember(8);          // Kept for compatibility
  else if(tag=="dynScale") return XMLMember(8);         // Kept for compatibility
  else if(tag=="cellDist") return XMLMember(8);         // Kept for compatibility
  else {
    return _models.forward()->xml_member(tag, attributes, context)+15;
  }
}

bool InversionThread::xml_setProperty(XML_SETPROPERTY_ARGS)
{
  TRACE;
  switch (memberID) {
  case 0:
    _itmax=content.toInt(); // Kept for compatibility
    return true;
  case 1:
    _ns0=content.toInt();
    return true;
  case 2:
    _ns=content.toInt();
    return true;
  case 3:
    _nr=content.toInt();
    return true;
  case 4:
    _nw=content.toInt();
    return true;
  case 5:
    _giveUp=content.toDouble();
    return true;
  case 7: {
      _reportFileName=content.toString();
      QFileInfo fi(_reportFileName);
      QObject::setObjectName(fi.baseName());
      WindowEnvironment::window(this)->logs()->setViewName(this, fi.baseName());
    }
    return true;
  case 8:   // Kept for compatibility
    return true;
  case 9:
    // Checksum not read from file
    return true;
  case 10:
    // Obsolete feature
    return true;
  default:
    return _models.forward()->xml_setProperty(memberID-15, tag, attributes, content, context);
  }
}

bool InversionThread::xml_polish(XML_POLISH_ARGS)
{
  TRACE;
  if(_itmax>0) {  // Kept for compatibility
    _ns*=_itmax;
    _itmax=0;
  }
  StreamRedirection sr(App::stream(this));
  if(_models.forward()->xml_polish(context)) {
    _models.setStorage();
    return true;
  } else {
    return false;
  }
}

int InversionThread::validModelCount() const
{
  if(_hasModelsToImport) {
    return _reportValidModelCount;
  } else {
    return _models.validModelCount();
  }
}

/*!
  If \a strict is true, the parameter space checksum must exactly match
*/
void InversionThread::importModels(QString fileName, bool strict)
{
  TRACE;
  _models.importModels(fileName, strict);
  _hasModelsToImport=false;

}
