/***************************************************************************
**
**  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: 2005-10-18
**  Copyright: 2005-2019
**    Marc Wathelet
**    Marc Wathelet (LGIT, Grenoble, France)
**
***************************************************************************/

#include <DinverGui.h>
#include <SciFigs.h>

#include "dinverInstallPath.h"
#include "dinverVersion.h"
#include "WindowEnvironment.h"
#include "MainWindow.h"
#include "Engine.h"
#include "PluginSelector.h"

PACKAGE_INFO("dinver", DINVER)

ApplicationHelp * help();
int modeGui(int argc, char ** argv);
int modeNeighborhoodOptimization(int argc, char ** argv);
int modeImportanceSampling(int argc, char ** argv, const QString& baseFileName);
bool checkRemainingArguments(int argc, char ** argv);

static QString pluginTag;
static bool debugStream=false;

int main(int argc, char ** argv)
{
  // Basic options: selection of the main modes and plugin
  enum Mode {Gui, NeighborhoodOptimization, ImportanceSampling};
  Mode mode=Gui;
  QString baseFileName;

  // Usually inside Application but here it is not yet created
  CoreApplication::setArgumentList(argc, argv);
  // Check arguments
  int j=1;
  for(int i=1; i<argc; i++) {
    QByteArray arg=argv[i];
    if(arg[0]=='-') {
      if(arg=="-debug-stream") {
        debugStream= true;
      } else if(arg=="-plugin-list") {
        CoreApplication a(argc, argv,help);
        DinverCoreEngine::printPluginList();
        return 0;
     } else if(arg=="-i") {
        CoreApplication::checkOptionArg(i, argc, argv);
        pluginTag=argv[i];
      } else if(arg=="-na-optimization" || arg=="-optimization") { // Kept for compatibility
        mode=NeighborhoodOptimization;
      } else if(arg=="-importance-sampling") {
        CoreApplication::checkOptionArg(i, argc, argv);
        baseFileName=argv[i];
        mode=ImportanceSampling;
      } else if(arg=="-h" || arg=="--help") {
        mode=NeighborhoodOptimization; // avoid GUI mode to provide help even if there is no X server
        argv[j++]=argv[i];             // Do not remove option from arguments
      } else {
        argv[j++]=argv[i];
      }
    } else {
      argv[j++]=argv[i];
    }
  }
  if(j<argc) {
    argv[j]=nullptr;
    argc=j;
  }

  switch(mode) {
  case Gui:
    return modeGui(argc, argv);
  case NeighborhoodOptimization:
    return modeNeighborhoodOptimization(argc, argv);
  case ImportanceSampling:
    return modeImportanceSampling(argc, argv, baseFileName);
  }
  return 0;
}

int modeGui(int argc, char ** argv)
{
  Application a(argc, argv, help);
  a.setConsoleMessage();

  // Options
  QString dinverFile;
  // Check arguments
  int j=1;
  for(int i=1; i<argc; i++) {
    QByteArray arg=argv[i];
    if(arg[0]=='-') {
      if(arg=="-env") {
        Application::checkOptionArg(i, argc, argv);
        dinverFile=argv[i];
      } else if(arg=="-clear-plugins") {
        DinverPluginSettings reg;
        reg.clear();
        return 0;
      } else {
        App::log(tr("dinver: bad option %1, see --help\n").arg(argv[i]) );
        return 2;
      }
    } else {
      argv[j++]=argv[i];
    }
  }
  if(j < argc) {
    argv[j]=nullptr;
    argc=j;
  }
  if(!checkRemainingArguments(argc,argv)) {
    return 2;
  }

  SciFigsGlobal s;
  Application::instance()->setGuiMessage();

  QString pluginFile=MainWindow::pluginSelector(pluginTag, debugStream);
  if(pluginFile.isEmpty()) {
    App::log(tr("No plugin selected\n") );
    return 2;
  }

  Engine dv;
  WindowEnvironment we;
  MainWindow * w=static_cast<MainWindow *>(we.addWindow());
  if(!w->setPlugin(pluginFile)) {
    delete w;
    App::log(tr("Cannot load plugin %1\n").arg(pluginFile) );
    return 2;
  }
  w->logs()->setDebugMode(debugStream);
  // Add main message board (after debug mode switch)
  w->logs()->addView(QThread::currentThread(), tr("Messages"));
  if(!dinverFile.isEmpty()) {
    w->open(dinverFile);
  }
  return qApp->exec();
}

int modeNeighborhoodOptimization(int argc, char ** argv)
{
  CoreApplication a(argc, argv, help);

  if(pluginTag.isEmpty()) {
    App::log(tr("dinver: no plugin selected (option -i)\n") );
    return 2;
  }

  App::setStream(new StandardStream(stdout));
  DinverCoreEngine dv;
  QString pluginFile=dv.pluginFile(pluginTag, debugStream);
  if(!dv.setDefaultPlugin(pluginFile, debugStream)) {
    App::log(tr("dinver: cannot set plugin with tag %1\n").arg(pluginTag) );
    return 2;
  }

  // Options
  QString paramFile;
  QString targetFile;
  int expectedBestModelCount=INT_MAX;
  double maximumSavedMisfit=std::numeric_limits<double>::infinity();
  int ns0=50;
  int ns=2500;
  int nr=50;
  ReportWriter::Action reportAction=ReportWriter::Ask;
  QString outputFileName="run.report";
  // Check arguments
  int j=1;
  for(int i=1; i<argc; i++) {
    QByteArray arg=argv[i];
    if(arg[0]=='-') {
      if(arg=="-param") {
        Application::checkOptionArg(i, argc, argv);
        paramFile=argv[i];
      } else if(arg=="-target") {
        Application::checkOptionArg(i, argc, argv);
        targetFile=argv[i];
      } else if(arg=="-ns0") {
        CoreApplication::checkOptionArg(i, argc, argv);
        ns0=atoi(argv[i]);
      } else if(arg=="-ns") {
        CoreApplication::checkOptionArg(i, argc, argv);
        ns=atoi(argv[i]);
      } else if(arg=="-nr") {
        CoreApplication::checkOptionArg(i, argc, argv);
        nr=atoi(argv[i]);
      } else if(arg=="-max-nr") {
        CoreApplication::checkOptionArg(i, argc, argv);
        expectedBestModelCount=atoi(argv[i]);
      } else if(arg=="-max-misfit") {
        CoreApplication::checkOptionArg(i, argc, argv);
        maximumSavedMisfit=atof(argv[i])*(1.0+1e-15);
      } else if(arg=="-o") {
        CoreApplication::checkOptionArg(i, argc, argv);
        outputFileName=argv[i];
      } else if(arg=="-f") {
        reportAction=ReportWriter::Overwrite;
      } else if(arg=="-resume") {
        reportAction=ReportWriter::Append;
      } else {
        argv[j++]=argv[i];
      }
    } else {
      argv[j++]=argv[i];
    }
  }
  argc=j;
  if(!dv.defaultPlugin()->setArguments(argc, argv) ||
     !checkRemainingArguments(argc, argv)) {
    return 2;
  }
  BatchRun r;
  if(!paramFile.isEmpty()) {
    if(!r.setParameters(paramFile)) {
      App::log(tr("dinver: error reading parameters from file %1\n").arg(paramFile) );
      return 2;
    }
  }
  if(!targetFile.isEmpty()) {
    if(!r.setTargets(targetFile)) {
      App::log(tr("dinver: error reading targets from file %1\n").arg(targetFile) );
      return 2;
    }
  }
  if(!r.neighborhoodOptimization(ns0, ns, nr, expectedBestModelCount, maximumSavedMisfit,
                                  outputFileName, reportAction)) {
    return 2;
  }
  return 0;
}

int modeImportanceSampling(int argc, char ** argv, const QString& baseFileName)
{
  CoreApplication a(argc, argv, help);

  if(pluginTag.isEmpty()) {
    App::log(tr("dinver: no plugin selected (option -i)\n") );
    return 2;
  }

  DinverCoreEngine dv;
  QString pluginFile=dv.pluginFile(pluginTag, debugStream);
  if(!dv.setDefaultPlugin(pluginFile, debugStream)) {
    App::log(tr("dinver: cannot set plugin with tag %1\n").arg(pluginTag) );
    return 2;
  }

  // Options
  QString paramFile;
  int ns=5000;
  int seed=0;
  QString outputFileName="run.report";
  ReportWriter::Action reportAction=ReportWriter::Ask;
  // Check arguments
  int j=1;
  for(int i=1; i<argc; i++) {
    QByteArray arg=argv[i];
    if(arg[0]=='-') {
      if(arg=="-param") {
        Application::checkOptionArg(i, argc, argv);
        paramFile=argv[i];
      } else if(arg=="-ns") {
        CoreApplication::checkOptionArg(i, argc, argv);
        ns=atoi(argv[i]);
      } else if(arg=="-seed") {
        CoreApplication::checkOptionArg(i, argc, argv);
        seed=atoi(argv[i]);
      } else if(arg=="-o") {
        CoreApplication::checkOptionArg(i, argc, argv);
        outputFileName=argv[i];
      } else if(arg=="-f") {
        reportAction=ReportWriter::Overwrite;
      } else if(arg=="-resume") {
        reportAction=ReportWriter::Append;
      } else if(arg=="dof") {
        CoreApplication::checkOptionArg(i, argc, argv);
        PdfPoint::setDegreesOfFreedom(atof(argv[i]));
      }
    } else {
      argv[j++]=argv[i];
    }
  }
  if(paramFile.isEmpty()) {
    App::log(tr("dinver: missing parameters, option '-param'\n") );
    return 2;
  }
  BatchRun r;
  if(!r.setParameters(paramFile)) {
    App::log(tr("dinver: error reading parameters from file %1\n").arg(paramFile) );
    return 2;
  }
  if(!r.importanceSampling(ns, seed, baseFileName, outputFileName, reportAction)) {
    return 2;
  }
  return 0;
}

bool checkRemainingArguments(int argc, char ** argv)
{
  if(argc>1) {
    for(int i=1; i<argc; i++) {
      App::log(tr("dinver: bad option '%1', see -h\n").arg(argv[i]) );
    }
    return false;
  } else {
    return true;
  }
}

ApplicationHelp * help()
{
  ApplicationHelp * h=new ApplicationHelp;
  h->setOptionSummary( "[OPTIONS]" );
  h->setComments( "Graphical user interface for Conditional Neighbourhood Algorithm (Inversion/Optimization)." );
  h->addGroup("Dinver","dinver");
  h->addOption("-plugin-list","Print the list of a available plugins");
  h->addOption("-i <PLUGIN_TAG>","Select inversion plugin with tag PLUGIN_TAG, skip first dialog box. See option '-plugin-list', to get the "
                                 "list of available plugins and their tags.");
  h->addOption("-env <FILE>","Load Dinver environment file .dinver. "
                             "With option '-run', only the current target and parameterization are "
                             "considered, the run description is ignored. It replaces former options "
                             "'-target' and '-param'.");
  h->addOption("-optimization","Starts one inversion run with target and parameterization specified by options '-param' and '-target'. "
                               "No graphical user interface is started. See '-h optimization' for all options.");
  h->addOption("-importance-sampling <REPORT>","Starts one resampling run with parameterization specified by options '-param'. REPORT is "
                                               "an inversion report produced by option '-optimization' or by graphical interface. "
                                               "No graphical user interface is started. See '-h importanceSampling' for all options.");
  h->addGroup("Optimization","optimization");
  h->addOption("-target <TARGET>","Set targets from file TARGET. It can be a .target or a .dinver file. A .dinver file contains both parameters and "
                                  "targets. Provide it to both options '-param' and '-target'.");
  h->addOption("-param <PARAM>","Set parameters from file PARAM. It can be a .param or a .dinver file. A .dinver file contains both parameters and "
                                  "targets. Provide it to both options '-param' and '-target'.");
  h->addOption("-ns0 <NS0>","Number of initial models (default=50).");
  h->addOption("-ns <NS>","Number of models generated (default=2500).");
  h->addOption("-nr <NR>","Number of best cells (default=50).");
  h->addOption("-max-nr <NR>","Expected number of best models (default=no limit). This is usefull when running inversion with a target "
                              "that include a minimum misfit concept. In this case a minimum value can be effectively reached and shared "
                              "by many distinct models. There is no reason to discard them because they all fit the data in the same way. "
                              "Hence, the vector of best models continuously increase when new models having the minimum misft are generated. "
                              "A high number of iteration must be setup to avoid stopping before reaching the maximum NR.");
  h->addOption("-max-misfit <MISFIT>","Only saves the models with a misfit less than MISFIT (default=no limit).");
  h->addOption("-o <REPORT>","Ouput report (default='run.report'). To suppress output to .report file, set option '-o' to \"\".");
  h->addOption("-f","Force overwrite if output report already exists.");
  h->addOption("-resume","If the output report file already exists, it is imported into the parameter space before starting "
                         "the inversion. Better to set NS0 to zero in this case. This way, it is possible to continue an "
                         "existing inversion adding new iterations.");
  h->addGroup("Importance sampling","importanceSampling");
  h->addOption("-dof <DOF>","Set degrees of freedom for conversion from misfit to a posteriori probability function.");
  h->addOption("-param <PARAM>","Set parameters from file PARAM. It can be a .param or a .dinver file. A .dinver file contains both parameters and "
                                  "targets. Provide it to both options '-param' and '-target'.");
  h->addOption("-ns <NS>","Number of models generated (default=5000).");
  h->addOption("-seed <SEED>","Set random seed to SEED. This option is for debug only.");
  h->addOption("-o <REPORT>","Ouput report (default='run.report'). To suppress output to .report file, set option '-o' to \"\".");
  h->addOption("-f","Force overwrite if output report already exists.");
  h->addOption("-resume","If the output report file already exists, add generated models without warning.");
  h->addGroup( "Debug","debug" );
  h->addOption("-debug-stream","Debug mode (redirect all logs to stdout)");
  h->addOption("-clear-plugins","Reset the list of a available plugins");
  return h;
}
