/***************************************************************************
**
**  This file is part of gpdcmisfit.
**
**  gpdcmisfit 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.
**
**  gpdcmisfit 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: 2007-12-11
**  Copyright: 2007-2019
**    Marc Wathelet
**    Marc Wathelet (LGIT, Grenoble, France)
**
***************************************************************************/

#include <DinverCore.h>
#include <DinverDCCore.h>
#include <QGpCoreTools.h>
#include <QGpCoreWave.h>

#include "gpdcmisfitVersion.h"
#include "gpdcmisfitInstallPath.h"

PACKAGE_INFO("gpdcmisfit", GPDCMISFIT)

ApplicationHelp * help();
int reportMode();
int surfaceModelMode();
int refractionModelMode();

enum AppMode {Report, SurfaceModel, RefractionModel};
AppMode mode=Report;
int akaikeDof=1;

TargetList * targets=0;          // Targets to consider in misfit computation
                                 // Constructor of TargetList calls TRACE
                                 // This cannot be used before creation of CoreApplication
ReportWriter * outputReport=0;
int iModel=0;
double maxInputMisfit=std::numeric_limits<double>::infinity();
double maxOutputMisfit=std::numeric_limits<double>::infinity();
int maxOutputCount=0;
int bestOutputCount=0;
QStringList inputFileList;
QList<DCModelInfo *> models;
ReportWriter::Action reportAction=ReportWriter::Ask;

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

  targets=new TargetList;
  // Options
  QString targetFile;            // Target file to consider in misfit computation

  int i, j=1;
  // Check arguments for Target file only
  for(i=1; i<argc; i++) {
    QByteArray arg=argv[i];
    if(arg[0]=='-') {
      if(arg=="-target") {
        CoreApplication::checkOptionArg(i, argc, argv);
        targetFile=argv[i];
      } else {
        argv[j++]=argv[i];
      }
    } else {
      argv[j++]=argv[i];
    }
  }
  if(j < argc) {
    argv[j]=nullptr;
    argc=j;
  }

  if(targetFile.isEmpty()) {
    App::log(tr("gpdcmisfit: option -target is mandatory\n") );
    delete targets;
    return 2;
  }
  XMLVirtualPlugin plugin(targets, "DispersionCurve");
  XMLDinverHeader hdr(&plugin);
  if(hdr.xml_restoreFile(targetFile)!=XMLClass::NoError) {
    App::log(tr("gpdcmisfit: error parsing target file\n") );
    delete targets;
    return 2;
  }

  // By default no misfit component is selected
  targets->dispersionTarget().setSelected(false);
  targets->autocorrTarget().setSelected(false);
  targets->ellipticityCurveTarget().setSelected(false);
  targets->ellipticityPeakTarget().setSelected(false);
  targets->refractionVpTarget().setSelected(false);
  targets->refractionVsTarget().setSelected(false);
  targets->magnetoTelluricTarget().setSelected(false);
  MisfitType misfitType=L2_Normalized;
  bool forceMisfitType=false;

  // Options
  QString reportFile;            // Report file to output results
  // Check arguments
  j=1;
  for(i=1; i<argc; i++) {
    QByteArray arg=argv[i];
    if(arg[0]=='-') {
      if(arg=="-report") {
        mode=Report;
      } else if(arg=="-surf-model") {
        mode=SurfaceModel;
      } else if(arg=="-refra-model") {
        mode=RefractionModel;
      } else if(arg=="-l1") {
        misfitType=L1_Normalized;
        forceMisfitType=true;
      } else if(arg=="-l2") {
        misfitType=L2_Normalized;
        forceMisfitType=true;
      } else if(arg=="-akaike") {
        CoreApplication::checkOptionArg(i, argc, argv);
        misfitType=Akaike;
        akaikeDof=atoi(argv[i]);
        forceMisfitType=true;
      } else if(arg=="-akaike-few") {
        CoreApplication::checkOptionArg(i, argc, argv);
        misfitType=AkaikeFewSamples;
        akaikeDof=atoi(argv[i]);
        forceMisfitType=true;
      } else if(arg=="-o") {
        CoreApplication::checkOptionArg(i, argc, argv);
        reportFile=argv[i];
      } else if(arg=="-m-in") {
        CoreApplication::checkOptionArg(i, argc, argv);
        maxInputMisfit=atof(argv[i])*(1.0+1e-15);
      } else if(arg=="-m-out") {
        CoreApplication::checkOptionArg(i, argc, argv);
        maxOutputMisfit=atof(argv[i])*(1.0+1e-15);
      } else if(arg=="-best-out") {
        CoreApplication::checkOptionArg(i, argc, argv);
        bestOutputCount=atoi(argv[i] );
      } else if(arg=="-n-out") {
        CoreApplication::checkOptionArg(i, argc, argv);
        maxOutputCount=atoi(argv[i]);
      } else if(arg=="-all") {
        CoreApplication::checkOptionArg(i, argc, argv);
        targets->dispersionTarget().setSelected(true);
        targets->autocorrTarget().setSelected(true);
        targets->ellipticityCurveTarget().setSelected(true);
        targets->ellipticityPeakTarget().setSelected(true);
        targets->refractionVpTarget().setSelected(true);
        targets->refractionVsTarget().setSelected(true);
        double m=atof(argv[i]);
        targets->dispersionTarget().setMinimumMisfit(m);
        targets->autocorrTarget().setMinimumMisfit(m);
        targets->ellipticityCurveTarget().setMinimumMisfit(m);
        targets->ellipticityPeakTarget().setMinimumMisfit(m);
        targets->refractionVpTarget().setMinimumMisfit(m);
        targets->refractionVsTarget().setMinimumMisfit(m);
      } else if(arg=="-disp") {
        CoreApplication::checkOptionArg(i, argc, argv);
        targets->dispersionTarget().setSelected(true);
        targets->dispersionTarget().setMinimumMisfit(atof(argv[i]));
      } else if(arg=="-autocorr") {
        CoreApplication::checkOptionArg(i, argc, argv);
        targets->autocorrTarget().setSelected(true);
        targets->autocorrTarget().setMinimumMisfit(atof(argv[i]));
      } else if(arg=="-ellcurve") {
        CoreApplication::checkOptionArg(i, argc, argv);
        targets->ellipticityCurveTarget().setSelected(true);
        targets->ellipticityCurveTarget().setMinimumMisfit(atof(argv[i]));
      } else if(arg=="-ellpeak") {
        CoreApplication::checkOptionArg(i, argc, argv);
        targets->ellipticityPeakTarget().setSelected(true);
        targets->ellipticityPeakTarget().setMinimumMisfit(atof(argv[i]));
      } else if(arg=="-refravp") {
        CoreApplication::checkOptionArg(i, argc, argv);
        targets->refractionVpTarget().setSelected(true);
        targets->refractionVpTarget().setMinimumMisfit(atof(argv[i]));
      } else if(arg=="-refravs") {
        CoreApplication::checkOptionArg(i, argc, argv);
        targets->refractionVsTarget().setSelected(true);
        targets->refractionVsTarget().setMinimumMisfit(atof(argv[i]));
      } else if(arg=="-mt") {
        CoreApplication::checkOptionArg(i, argc, argv);
        targets->magnetoTelluricTarget().setSelected(true);
        targets->magnetoTelluricTarget().setMinimumMisfit(atof(argv[i]));
      } else if(arg=="-f") {
        reportAction=ReportWriter::Overwrite;
      } else if(arg=="-resume") {
        reportAction=ReportWriter::Append;
      } else {
        App::log(tr("gpdcmisfit: bad option %1, see --help\n").arg(argv[i]) );
        delete targets;
        return 2;
      }
    } else {
      argv[j++]=argv[i];
    }
  }
  if(j < argc) {
    argv[j]=nullptr;
    argc=j;
  }

  if(!targets->dispersionTarget().selected() &&
     !targets->autocorrTarget().selected() &&
     !targets->ellipticityCurveTarget().selected() &&
     !targets->ellipticityPeakTarget().selected() &&
     !targets->refractionVpTarget().selected() &&
     !targets->refractionVsTarget().selected() &&
     !targets->magnetoTelluricTarget().selected()) {
    App::log(tr("gpdcmisfit: no sub-target selected, see --help\n") );
    delete targets;
    return 2;
  }

  for(int i =1;i<argc;i++) {
    inputFileList.append(argv[i]);
  }
  if(forceMisfitType) {
    targets->setMisfitType(misfitType);
  }
  targets->validateTargets();

  if(!reportFile.isEmpty()) {
    if(!ReportWriter::initReport(reportFile, tr("Open report for writing"), reportAction)) {
      return 2;
    }
    outputReport=new ReportWriter(reportFile);
    if(!outputReport->open()) {
      App::log(tr("gpdcmisfit: cannot open report file for writing\n") );
      delete outputReport;
      return 2;
    }
  }

  int exitCode=0;
  switch(mode) {
  case Report:
    exitCode=reportMode();
    break;
  case SurfaceModel:
    exitCode=surfaceModelMode();
    break;
  case RefractionModel:
    exitCode=refractionModelMode();
    break;
  }

  if(outputReport) {
    QTextStream(stdout) << QString("# Saved %1 misfits in %2").arg(iModel).arg(reportFile) << endl;
  }

  delete targets;
  delete outputReport;
  return exitCode;
}

int reportMode()
{
  QTextStream sOut(stdout);
  //report->addModel(totalMisfit/totalWeight, checksum, nDimensions, _varParamList);
  sOut << "# Index in report | New misfit | Old misfit\n";
  QList<ReportReader *> reports;
  for(QStringList::iterator it=inputFileList.begin(); it!=inputFileList.end(); it++) {
    ReportReader * report=new ReportReader(*it);
    if(!report->open()) {
      qDeleteAll(models);
      foreach(ReportReader * report, reports) {
        ReportReader::removeReference(report);
      }
      delete report;
      return 2;
    }
    report->addReference();
    reports << report;
    report->synchronize();
    int nReportModels=report->nModels();
    sOut << "# Report=" << *it << "\n";
    sOut << "# N Models=" << nReportModels << "\n";
    QDataStream& s=report->stream();
    for(int iReportModel=0; iReportModel<nReportModels; iReportModel++) {
      double oldMisfit=report->misfit(iReportModel);
      if(oldMisfit<maxInputMisfit) {
        int nParams;
        uint cs;
        double val;
        qint64 blockOffset=s.device()->pos();
        s >> nParams;
        s >> cs;
        Parameter * params[nParams];
        for(int id=0 ; id<nParams; id++) {
          s >> val;
          params[id]=new Parameter;
          params[id]->setRealValue(val);
        }
        // Read layered model
        s.device()->seek(blockOffset);
        int reportDCVersion=report->userBlockVersion("DISP");
        if(reportDCVersion>=0) {
          DCReportBlock dcBlock(s);
          dcBlock.readProfiles(reportDCVersion);
          Seismic1DModel * sm=dcBlock.readSeismicModel();
          Resistivity1DModel * em=dcBlock.readElectricModel();
          // Compute new misfit
          double totalMisfit=0.0;
          double totalWeight=0.0;
          bool ok=true;
          if(sm) {
            targets->surfaceMisfit(totalMisfit, totalWeight, sm, akaikeDof, ok);
            if(dcBlock.pitch()) {
              Profile pitch;
              pitch.readReport(s);
              pitch.resample(sm->vpProfile().depths());
              targets->refractionMisfit(totalMisfit, totalWeight, sm, pitch.values(), akaikeDof, ok);
            }
          }
          if(em) {
            // No static shift: TODO provide a way to identify parameters values
            targets->magnetoTelluricMisfit(totalMisfit, totalWeight, em, 1.0, akaikeDof, ok);
          }
          if(!sm && !em) {
            // Really nothing to retrieve skip this model
            for(int id=0; id<nParams; id++) delete params[id];
            continue;
          }
          if(ok && totalWeight>0) {
            double misfit=totalMisfit/totalWeight;
            if(misfit<maxOutputMisfit) {
              if(bestOutputCount>0) {
                DCModelInfo * info=new DCModelInfo;
                info->setReport(report);
                info->setIndexInReport(iReportModel);
                info->setMisfit(misfit);
                info->addReference();
                models << info;
              } else {
                sOut << QString("%1 %2 %3").arg(iReportModel+1).arg(oldMisfit).arg(misfit) << endl;
                if(outputReport) {
                  // Save new misfit and the rest of the model entry (param, disp,...)
                  outputReport->addModel(misfit, cs, nParams, params);
                  s.device()->seek(blockOffset);
                  DCReportBlock::write(outputReport, report);
                }
                iModel++;
                if(iModel==maxOutputCount) {
                  for(int id=0 ; id<nParams; id++) {
                    delete params[id];
                  }
                  delete sm;
                  delete em;
                  break;
                }
              }
            }
            delete sm;
            delete em;
          }
          for(int id=0; id<nParams; id++) {
            delete params[id];
          }
        }
      }
    }
  }
  if(bestOutputCount>0) {
    std::sort(models.begin(), models.end(), DCModelInfo::misfitLessThan);
    int iStop=models.count() - maxOutputCount;
    if(iStop<0) iStop=0;
    for(int i=models.count()-1; i>=iStop; i-- ) {
      DCModelInfo * info=models.at(i);
      ReportReader * report=info->report();
      double oldMisfit=report->misfit(info->indexInReport());
      sOut << QString("%1 %2 %3\n").arg(info->indexInReport()+1).arg(oldMisfit).arg(info->misfit());
      if(outputReport) {
        QDataStream& s=report->stream();
        int nParams;
        uint cs;
        double val;
        qint64 blockOffset=s.device()->pos();
        s >> nParams;
        s >> cs;
        Parameter * params[nParams];
        for(int id=0 ; id<nParams; id++ ) {
          s >> val;
          params[id]=new Parameter;
          params[id]->setRealValue(val);
        }
        // Save new misfit and the rest of the model entry (param, disp,...)
        outputReport->addModel(info->misfit(), cs, nParams, params);
        for(int id=0 ; id<nParams; id++ ) delete params[id];
        s.device()->seek(blockOffset);
        DCReportBlock::write(outputReport, report);
      }
      iModel++;
    }
  }
  qDeleteAll(models);
  foreach(ReportReader * report, reports) ReportReader::removeReference(report);
  return 0;
}

int surfaceModelMode()
{
  QTextStream sOut(stdout);
  Seismic1DModel m;
  QFile * f;
  QStringList::iterator itFile=inputFileList.begin();
  if(itFile==inputFileList.end()) {
    f=new QFile;
    f->open(stdin,QIODevice::ReadOnly);
    CoreApplication::instance()->debugUserInterrupts(false);
  } else {
    f=0;
  }
  while(true) {
    if(f && f->atEnd()) {
      delete f;
      f=0;
      // Even if stdin was not in use, it does not hurt.
      CoreApplication::instance()->debugUserInterrupts(true);
    }
    if(!f) {
      if(itFile!=inputFileList.end()) {
        f=new QFile(*itFile);
        if(!f->open(QIODevice::ReadOnly)) {
          App::log(tr("Cannot open file %1 for reading.\n").arg(*itFile) );
          delete f;
          return 2;
        }
        itFile++;
      } else break;
    }
    QString comments;
    QTextStream s(f);
    if(!m.fromStream(s, &comments)) {
      break;
    }
    if(m.layerCount()>0) {
      double totalMisfit=0.0;
      double totalWeight=0.0;
      bool ok=true;
      targets->surfaceMisfit(totalMisfit, totalWeight, &m, akaikeDof, ok);
      if(ok && totalWeight>0.0) {
        double misfit=totalMisfit/totalWeight;
        if(misfit<maxOutputMisfit) {
          sOut << misfit << endl;
          if(outputReport) {
            outputReport->addModel(misfit, 0, 0, 0);
            Profile vp=m.vpProfile();
            Profile vs=m.vsProfile();
            Profile rho=m.rhoProfile();
            DCReportBlock::write(outputReport, *targets, &vp, &vs, &rho);
          }
          iModel++;
          if(iModel==maxOutputCount) {
            delete f;
            return 0;
          }
        }
      }
    } else {
      break;
    }
  }
  delete f;
  return 0;
}

int refractionModelMode()
{
  RefractionDippingModel m;
  QFile * f;
  QStringList::iterator itFile=inputFileList.begin();
  if(itFile==inputFileList.end()) {
    f=new QFile;
    f->open(stdin,QIODevice::ReadOnly);
    CoreApplication::instance()->debugUserInterrupts(false);
  } else {
    f=0;
  }
  while(true) {
    if(f && f->atEnd()) {
      delete f;
      f=0;
      // Even if stdin was not in use, it does not hurt.
      CoreApplication::instance()->debugUserInterrupts(true);
    }
    if(!f) {
      if(itFile!=inputFileList.end()) {
        f=new QFile(*itFile);
        if(!f->open(QIODevice::ReadOnly)) {
          App::log(tr("Cannot open file %1 for reading.\n").arg(*itFile) );
          delete f;
          return 2;
        }
        itFile++;
      } else break;
    }
    QString comments;
    QTextStream s(f);
    if(!m.fromStream(s, &comments)) {
      break;
    }
    if(m.layerCount()>0) {
      double totalMisfit=0.0;
      double totalWeight=0.0;
      bool ok=true;
      targets->refractionMisfit(totalMisfit, totalWeight, &m, akaikeDof,ok);
      if(ok && totalWeight>0) {
        s << totalMisfit/totalWeight << endl;
        m.toStream(s);
      }
    } else {
      break;
    }
  }
  delete f;
  return 0;
}

ApplicationHelp * help()
{
  ApplicationHelp * h=new ApplicationHelp;
  h->setOptionSummary( "[OPTIONS] [FILE]..." );
  h->setComments( "Computes misfits (full forward computation) for models contained in report files or from model files (see Mode selection). "
                  "By default the misfit is not computed for any component. You must enable the components explicitely "
                  "with options -disp, -autocorr, -ellcurve, -ellpeak, -refravp or -refravs. Use -all to enable all components.");
  h->addGroup("Mode selection","modeSelection");
  h->addOption("-report","In this mode, FILEs are report files. This is used to re-compute in a different way (default mode).");
  h->addOption("-surf-model","In this mode, FILEs are layered model files (see gpdc for details). Only surface wave misfit are "
                             "computed (dispersion, autocorr and ellipticity).");
  h->addOption("-refra-model","In this mode, FILEs are tilted layered model files (see gprefra for details). Only refraction "
                              "misfits are computed.");
  h->addOption("-mt-model","In this mode, FILEs are resistivity layered model files (see gpmt for details). Only magneto-telluric "
                              "misfits are computed. [not yet implemented]");
  h->addGroup("All modes","allModes");
  h->addOption("-target <FILE>","Set the target to used for misfit computation (mandatory).");
  h->addOption("-l1","Computes L1 normalized misfit.");
  h->addOption("-l2","Computes L2 normalized misfit (default misfit).");
  h->addOption("-akaike <ND>","Computes Akaike misfit. ND is the degrees of freedom.");
  h->addOption("-akaike-few <ND>","Computes Akaike misfit for small sample sets (compared to ND). ND is the degrees of freedom.");
  h->addGroup("Report mode","reportMode");
  h->addOption("-o <FILE>","Set the output report.");
  h->addOption("-f","Force overwrite if output report already exists (default=ask for it).");
  h->addOption("-resume","If the output report file already exists, append to it (default=ask for it).");
  h->addOption("-m-in <MAX>","Maximum original misfit to consider (default=std::numeric_limits<double>::infinity())");
  h->addOption("-m-out <MAX>","Maximum new calculated misfit in output (default=std::numeric_limits<double>::infinity())");
  h->addOption("-best-out <N>","Output the N best models (default=no limit). Only the best ones according"
                               "to new misfit computation are output. If there are many models with the same "
                               "minimum misfit and if N is small, the returned models are randomly selected.");
  h->addOption("-n-out <N>","Maximum number of models in output (default=no limit). Only the first ones that "
                            "fit with the condition set by option '-m-out'.");
  h->addOption("-all <MIN>","Compute misfit on all components of the target with a misfit clipped to MIN.");
  h->addOption("-disp <MIN>","Compute misfit on the dispersion curves with a misfit clipped to MIN.");
  h->addOption("-autocorr <MIN>","Compute misfit on the autocorrelation curves with a misfit clipped to MIN.");
  h->addOption("-ellcurve <MIN>","Compute misfit on the ellipticity curves with a misfit clipped to MIN.");
  h->addOption("-ellpeak <MIN>","Compute misfit on the ellipticity peaks with a misfit clipped to MIN.");
  h->addOption("-refravp <MIN>","Compute misfit on the refraction Vp curves with a misfit clipped to MIN.");
  h->addOption("-refravs <MIN>","Compute misfit on the refraction Vs curves with a misfit clipped to MIN.");
  h->addOption("-mt <MIN>","Compute misfit on the magneto-telluric curves with a misfit clipped to MIN.");
  h->addGroup("Surface model mode","surfModelMode");
  h->addOption("-all <MIN>","Compute misfit on all surface wave components of the target with a misfit clipped to MIN.");
  h->addOption("-disp <MIN>","Compute misfit on the dispersion curves with a misfit clipped to MIN.");
  h->addOption("-autocorr <MIN>","Compute misfit on the autocorrelation curves with a misfit clipped to MIN.");
  h->addOption("-ellcurve <MIN>","Compute misfit on the ellipticity curves with a misfit clipped to MIN.");
  h->addOption("-ellpeak <MIN>","Compute misfit on the ellipticity peaks with a misfit clipped to MIN.");
  h->addGroup("Refraction model mode","refraModelMode");
  h->addOption("-all <MIN>","Compute misfit on all refraction components of the target with a misfit clipped to MIN.");
  h->addOption("-refravp <MIN>","Compute misfit on the ellipticity peaks with a misfit clipped to MIN.");
  h->addOption("-refravs <MIN>","Compute misfit on the ellipticity peaks with a misfit clipped to MIN.");
  h->addExample("gpdcmisfit -report -target my.target -disp 1 -m-in 1 -m-out 1 -o clipped.report *.report ",
                "Reads all reports of current directory, computes clipped misfits and stores all models with a calculated "
                "misfit less than 1 in a new report. Original misfits greater than 1 all have a new misfit greater than 1, "
                "hence we filter models in input with option '-m-in'.");
  return h;
}
