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

#include <QGpCoreTools.h>
#include <QJSEngine>

#include "CurveReader.h"

/*!
  \class CurveReader CurveReader.h
  \brief Brief description of class still missing

  Full description of class still missing
*/

/*!
  Description of constructor still missing
*/
CurveReader::CurveReader()
    : ArgumentStdinReader()
{
  TRACE;
  _mode=None;
  _nResample=100;
  _minX=-std::numeric_limits<double>::infinity();
  _maxX=std::numeric_limits<double>::infinity();
  _dx=1;
  _xValue=0.0;
  _columnIndex=1;
  _samplingType=LinearScale;
  _misfitType=L2_Normalized;
  _misfitMin=0.0;
  _misfitDof=1;
  _logBase=10.0;
  _multiplyFactor=1.0;
  _splitMaxX=1.2;
  _splitMaxErr=1e-4;
  _splitMinCount=4;
}

bool CurveReader::setOptions(int& argc, char ** argv)
{
  TRACE;
  int i, j=1;
  for(i=1; i<argc; i++) {
    QByteArray arg=argv[i];
    if(arg[0]=='-') {
      if(arg=="-resample") {
        CoreApplication::checkOptionArg(i, argc, argv);
        _mode=Resample;
        _nResample=atoi(argv[i]);
      } else if(arg=="-resample-ext") {
        CoreApplication::checkOptionArg(i, argc, argv);
        _mode=ResampleExtrapole;
        _nResample=atoi(argv[i]);
      } else if(arg=="-cut") {
        _mode=Cut;
      } else if(arg=="-cut-int") {
        _mode=CutInterpole;
      } else if(arg=="-column") {
        CoreApplication::checkOptionArg(i, argc, argv);
        _columnIndex=atoi(argv[i]);
        if(_columnIndex<1) {
          App::log(tr("gpcurve: column index must be greater or equal to 1 (%1)\n").arg(argv[i]) );
          return false;
        }
      } else if(arg=="-swap") {
        _mode=Swap;
      } else if(arg=="-max-index") {
        _mode=MaxIndex;
      } else if(arg=="-max-value") {
        _mode=MaxValue;
      } else if(arg=="-local-max") {
        _mode=LocalMax;
      } else if(arg=="-local-max-index") {
        _mode=LocalMaxIndex;
      } else if(arg=="-local-max-value") {
        _mode=LocalMaxValue;
      } else if(arg=="-local-min") {
        _mode=LocalMin;
      } else if(arg=="-local-min-index") {
        _mode=LocalMinIndex;
      } else if(arg=="-local-min-value") {
        _mode=LocalMinValue;
      } else if(arg=="-min-index") {
        _mode=MinIndex;
      } else if(arg=="-min-value") {
        _mode=MinValue;
      } else if(arg=="-value") {
        CoreApplication::checkOptionArg(i, argc, argv);
        _mode=FunctionValue;
        _xValue=atof(argv[i]);
      } else if(arg=="-merge-replace") {
        CoreApplication::checkOptionArg(i, argc, argv);
        _mode=MergeReplace;
        _referenceFile=argv[i];
      } else if(arg=="-merge-interpolate") {
        CoreApplication::checkOptionArg(i, argc, argv);
        _mode=MergeInterpolate;
        _referenceFile=argv[i];
      } else if(arg=="-minmax") {
        _mode=MinMax;
      } else if(arg=="-misfit") {
        CoreApplication::checkOptionArg(i, argc, argv);
        _mode=Misfit;
        _referenceFile=argv[i];
      } else if(arg=="-regression") {
        _mode=Regression;
      } else if(arg=="-derivative") {
        _mode=Derivative;
      } else if(arg=="-curvature") {
        _mode=Curvature;
      } else if(arg=="-pow") {
        CoreApplication::checkOptionArg(i, argc, argv);
        _mode=Power;
        _logBase=atof(argv[i]);
      } else if(arg=="-log") {
        CoreApplication::checkOptionArg(i, argc, argv);
        _mode=Log;
        _logBase=atof(argv[i]);
      } else if(arg=="-multiply") {
        CoreApplication::checkOptionArg(i, argc, argv);
        _mode=Multiply;
        _multiplyFactor=atof(argv[i]);
      } else if(arg=="-function") {
        CoreApplication::checkOptionArg(i, argc, argv);
        _mode=ParseFunction;
        _functionText=argv[i];
      } else if(arg=="-misfit-type") {
        CoreApplication::checkOptionArg(i, argc, argv);
        _misfitType=RealStatisticalValue::misfitType(argv[i] );
      } else if(arg=="-misfit-min") {
        CoreApplication::checkOptionArg(i, argc, argv);
        _misfitMin=atof(argv[i] );
      } else if(arg=="-misfit-dof") {
        CoreApplication::checkOptionArg(i, argc, argv);
        _misfitDof=atoi(argv[i] );
      } else if(arg=="-min") {
        CoreApplication::checkOptionArg(i, argc, argv);
        _minX=atof(argv[i]);
      } else if(arg=="-max") {
        CoreApplication::checkOptionArg(i, argc, argv);
        _maxX=atof(argv[i]);
      } else if(arg=="-dx") {
        CoreApplication::checkOptionArg(i, argc, argv);
        _dx=atof(argv[i]);
      } else if(arg=="-angles") {
        _mode=Angles;
      } else if(arg=="-split") {
        _mode=Split;
      } else if(arg=="-max-x") {
        CoreApplication::checkOptionArg(i, argc, argv);
        _splitMaxX=atof(argv[i]);
      } else if(arg=="-max-err") {
        CoreApplication::checkOptionArg(i, argc, argv);
        _splitMaxErr=atof(argv[i]);
      } else if(arg=="-min-count") {
        CoreApplication::checkOptionArg(i, argc, argv);
        _splitMinCount=atoi(argv[i]);
      } else if(arg=="-extrapolate") {
        CoreApplication::checkOptionArg(i, argc, argv);
        _nResample=atoi(argv[i]);
        _mode=Extrapolate;
      } else if(arg=="-sampling") {
        CoreApplication::checkOptionArg(i, argc, argv);
        if(strcmp(argv[i],"inversed")==0) {
          _samplingType=InversedScale;
        } else if(strcmp(argv[i],"linear")==0) {
          _samplingType=LinearScale;
        } else if(strcmp(argv[i],"log")==0) {
          _samplingType=LogScale;
        } else {
          App::log(tr("gpcurve: bad sampling type (%1), see -help\n").arg(argv[i]) );
          return false;
        }
      } else {
        App::log(tr("gpcurve: bad option %1, see -help\n").arg(argv[i]) );
        return false;
      }
    } else {
      argv[j++]=argv[i];
    }
  }
  if(j < argc) {
    argv[j]=nullptr;
    argc=j;
  }
  return true;
}

bool CurveReader::parse(QTextStream& s)
{
  TRACE;
  QTextStream sOut(stdout);
  Curve<PointND> curve;
  QString comments;
  while(curve.fromStream(s, &comments)) {
    sOut << comments << flush;
    if(!exec(curve)) {
      return false;
    }
    comments.clear();
  }
  sOut << comments << flush;
  return exec(curve);
}

bool CurveReader::exec(Curve<PointND>& curve)
{
  if(curve.isEmpty()) return true;
  QTextStream sOut(stdout);
  if(curve.count()<2) {
    App::log(tr("At least 2 samples are required to define a curve\n") );
    return false;
  }
  switch(_mode) {
  case ResampleExtrapole:
  case Resample:
  case Cut:
  case CutInterpole:
    curve.sort();
    // const must be passed to Curve to avoid reset of sorted flag
    if(_minX==-std::numeric_limits<double>::infinity()) {
      _minX=const_cast<const Curve<PointND>&>(curve).first().x();
    }
    if(_maxX==std::numeric_limits<double>::infinity()) {
      _maxX=const_cast<const Curve<PointND>&>(curve).last().x();
    }
    break;
  default:
    break;
  }
  PointNDOptions pointOptions(_columnIndex-1);
  switch(_mode) {
  case None:
  case ParseFunction:
    break;
  case ResampleExtrapole:
    curve.resample(_nResample, _minX, _maxX, _samplingType);
    sOut << curve.toString();
    break;
  case Resample: {
      // _minX and _maxX can be set by the user to any value, not linked to actual range of curve
      // To avoid extrapolation we want to keep the original range of the curve
      double minOriginalX=const_cast<const Curve<PointND>&>(curve).first().x();
      double maxOriginalX=const_cast<const Curve<PointND>&>(curve).last().x();
      curve.resample(_nResample, _minX, _maxX, _samplingType);
      curve.cut(minOriginalX, maxOriginalX, LinearScale);
      sOut << curve.toString();
    }
    break;
  case Cut:
    curve.cut(_minX, _maxX, LinearScale);
    sOut << curve.toString();
    break;
  case CutInterpole:
    curve.cut(_minX, _maxX, Interpole);
    sOut << curve.toString();
    break;
  case Swap:
    curve.swapXY(&pointOptions);
    sOut << curve.toString();
    break;
  case MinIndex:
    sOut << curve.minimumY(0, &pointOptions) << "\n";
    break;
  case MinValue: {
      int i=curve.minimumY(0, &pointOptions);
      ASSERT(i>-1);
      sOut << curve.at(i).toString(20) << "\n";
    }
    break;
  case MaxIndex:
    sOut << curve.maximumY(0, &pointOptions) << "\n";
    break;
  case MaxValue: {
      int i=curve.maximumY(0, &pointOptions);
      ASSERT(i>-1);
      sOut << curve.at(i).toString(20) << "\n";
    }
    break;
  case LocalMin: {
      int i=curve.localMinimumY(0, &pointOptions);
      while(i>=0) {
        sOut << i << " "
             << curve.at(i).y(&pointOptions) << " "
             << 2.0*curve.at(i).y(&pointOptions)/(curve.at(i-1).y(&pointOptions)+curve.at(i+1).y(&pointOptions))<< ::endl;
        i=curve.localMinimumY(i, &pointOptions);
      }
    }
    break;
  case LocalMinIndex: {
      int i=curve.localMinimumY(0, &pointOptions);
      while(i>=0) {
        sOut << i << ::endl;
        i=curve.localMinimumY(i, &pointOptions);
      }
    }
    break;
  case LocalMinValue: {
      int i=curve.localMinimumY(0, &pointOptions);
      while(i>=0) {
        sOut << curve.at(i).y(&pointOptions) << ::endl;
        i=curve.localMinimumY(i, &pointOptions);
      }
    }
    break;
  case LocalMax: {
      int i=curve.localMaximumY(0, &pointOptions);
      while(i>=0) {
        sOut << i << " "
             << curve.at(i).y(&pointOptions) << " "
             << 2.0*curve.at(i).y(&pointOptions)/(curve.at(i-1).y(&pointOptions)+curve.at(i+1).y(&pointOptions))<< ::endl;
        i=curve.localMaximumY(i, &pointOptions);
      }
    }
    break;
  case LocalMaxIndex: {
      int i=curve.localMaximumY(0, &pointOptions);
      while(i>=0) {
        sOut << i << ::endl;
        i=curve.localMaximumY(i, &pointOptions);
      }
    }
    break;
  case LocalMaxValue: {
      int i=curve.localMaximumY(0, &pointOptions);
      while(i>=0) {
        sOut << curve.at(i).y(&pointOptions) << ::endl;
        i=curve.localMaximumY(i, &pointOptions);
      }
    }
    break;
  case FunctionValue:
    curve.sort();
    sOut << curve.at(_xValue).toString(20) << "\n";
    break;
  case MergeReplace: {
      loadReference();
      for(int i=0;i<curve.count();i++) {
        const PointND& p=curve.at(i);
        int index=_referenceCurve.indexAfter(p.x());
        if(index<_referenceCurve.count() && _referenceCurve.at(index).x()==p.x()) {
          sOut << _referenceCurve.at(index).toString(20) << "\n";
        } else {
          sOut << p.toString(20) << "\n";
        }
      }
    }
    break;
  case MergeInterpolate: {
      loadReference();
      for(int i=0;i<curve.count();i++) {
        const PointND& p=curve.at(i);
        sOut << p.toString(20) << " " << _referenceCurve.at(p.x()).toString(20) << "\n";
      }
    }
    break;
  case Split: {
      QList<Curve<PointND>> curves=curve.split(_splitMaxX, _samplingType, _splitMaxErr, _splitMinCount, &pointOptions);
      for(int i=0;i<curves.count();i++) {
        Curve<PointND>& c=curves[i];
        // Filling holes in the curve sampling by re-interpolation
        double min=c.at(c.minimumX()).x();
        double max=c.at(c.maximumX()).x();
        double step=c.minimumStep(_samplingType);
        int n;
        if(_samplingType & LogScale) {
          n=1+qRound(log(c.at(max).x()/c.at(min).x())/log(step));
        } else {
          n=1+qRound(c.at(max).x()-c.at(min).x()/step);
        }
        c.resample(n, min, max, _samplingType);
        sOut << "# split " << i << ": " << c.count() << " samples\n";
        sOut << c.toString(20);
      }
    }
    break;
  case MinMax: {
      if(_minCurve.count()==0) {
        int ns=curve.count();
        int nd=curve.first().count();
        _minCurve.resize(ns);
        _maxCurve.resize(ns);
        for(int is=0; is<ns; is++) {
          const PointND& p=curve.at(is);
          PointND& pmin=_minCurve[is];
          PointND& pmax=_maxCurve[is];
          pmin.setX(p.x());
          pmax.setX(p.x());
          for(int id=0; id<nd; id++) {
            pmin.setY(id, p.y(id));
            pmax.setY(id, p.y(id));
          }
        }
      } else {
        int ns=_minCurve.count();
        int nd=_minCurve.first().count();
        if(ns!=curve.count() || nd!=curve.first().count()) {
          App::log(tr("Curves must have exactly the same sampling.\n") );
          return false;
        }
        for(int is=0; is<ns; is++) {
          const PointND& p=curve.at(is);
          PointND& pmin=_minCurve[is];
          PointND& pmax=_maxCurve[is];
          for(int id=0; id<nd; id++) {
            double v=p.y(id);
            if(v<pmin.y(id)) pmin.setY(id, v);
            if(v>pmax.y(id)) pmax.setY(id, v);
          }
        }
      }
    }
    break;
  case Derivative:
    if(curve.count()<3) {
      App::log(tr("At least 3 samples per curve are required to calculate derivative\n") );
      return false;
    } else {
      curve.sort();
      sOut << curve.derivative(&pointOptions).toString();
    }
    break;
  case Curvature:
    if(curve.count()<3) {
      App::log(tr("At least 3 samples per curve are required to calculate curvature\n") );
      return false;
    } else {
      curve.sort();
      sOut << curve.curvature(&pointOptions).toString();
    }
    break;
  case Angles:
    if(curve.count()<2) {
      App::log(tr("At least 3 samples per curve are required to calculate curvature\n") );
      return false;
    } else {
      curve.sort();
      sOut << curve.angles(&pointOptions).toString();
    }
    break;
  case Regression:
    if(curve.count()<2) {
      App::log(tr("At least 2 samples per curve are required to calculate regression\n") );
      return false;
    } else {
      double a, b;
      if(curve.leastSquare(a, b, &pointOptions)) {
        sOut << QString("y= %1 *x+ %2\n").arg(a, 0, 'g', 16).arg(b, 0, 'g', 16);
      } else {
        sOut << QString("x= %1\n").arg(b, 0, 'g', 16);
      }
    }
    break;
  case Misfit: {
      loadReference();
      double sum=0.0;
      int nData=0, nValues =0;
      for(int i=0;i<curve.count();i++) {
        const PointND& p=curve.at(i);
        PointND r=_referenceCurve.at(p.x());
        RealStatisticalValue sv(r.y(0), r.y(1));
        RealValue v(p.y(0));
        sum += sv.misfit(nValues, nData, v, _misfitType, _misfitMin);
      }
      switch(_misfitType) {
      case L1:
      case L1_Normalized:
      case L1_LogNormalized:
      case L1_NormalizedBySigmaOnly:
        sOut << sum/curve.count() << "\n";
        break;
      case RMS:
        sOut << sqrt(sum/curve.count()) << "\n";
        break;
      case L2:
      case L2_Normalized:
      case L2_LogNormalized:
      case L2_NormalizedBySigmaOnly:
        sOut << sqrt(sum) << "\n";
        break;
      case Akaike:
        sOut << curve.count()*log(sqrt(sum/curve.count()))+2.0*_misfitDof << "\n";
        break;
      case AkaikeFewSamples:
        sOut << curve.count()*log(sqrt(sum/curve.count()))
               +2.0*_misfitDof+(2.0*_misfitDof*(_misfitDof+1.0))/(curve.count()-_misfitDof-1.0) << "\n";
        break;
      case Determinant:
        ASSERT(false);
      }
    }
    break;
  case Multiply:
    curve.yMultiply(_multiplyFactor, &pointOptions);
    sOut << curve.toString();
    break;
  case Log:
    curve.yLog10(&pointOptions);
    if(_logBase!=10.0) {
      curve.yMultiply(1.0/log10(_logBase), &pointOptions);
    }
    sOut << curve.toString();
    break;
  case Power:
    if(_logBase!=10.0) {
      curve.yMultiply(log10(_logBase), &pointOptions);
    }
    curve.yExp10(&pointOptions);
    sOut << curve.toString();
    break;
  case Extrapolate:
    curve.extrapolateFromStart(_nResample, false, _samplingType, &pointOptions);
    curve.extrapolateFromEnd(_nResample, false, _samplingType, &pointOptions);
    sOut << curve.toString();
    break;
  }
  sOut << flush;
  return true;
}

bool CurveReader::loadReference()
{
  if(_referenceFile.isEmpty()) {
    App::log(tr("gpcurve: missing reference file, see -help\n") );
    return false;
  } else {
    QFile f(_referenceFile);
    if( !f.open(QIODevice::ReadOnly)) {
      App::log(tr("gpcurve: cannot open reference file %1\n").arg(_referenceFile) );
      return false;
    }
    QTextStream s(&f);
    // Read file for first curve and set it as the reference curve
    QString buf;
    PointND p;
    while(!s.atEnd()) {
      buf=s.readLine();
      if(buf[0]=='\n' || buf[0]=='#') {
        if(!_referenceCurve.isEmpty()) break;
      } else {
        StringSection bufFields(buf);
        p.fromString(bufFields);
        _referenceCurve.append(p);
      }
    }
  }
  if(_referenceCurve.isEmpty()) {
    App::log(tr("gpcurve: reference curve is empty\n") );
    return false;
  }
  _referenceCurve.sort();
  return true;
}

void CurveReader::generateFunction() const
{
  TRACE;
  QTextStream sOut(stdout);
  QJSEngine engine;
  QJSValue global=engine.globalObject();
  QJSValue result;
  if(_functionText.contains("return")) {
    result=engine.evaluate("function f(x) {"+_functionText+";}");
  } else {
    result=engine.evaluate("function f(x) {return "+_functionText+";}");
  }
  if(result.isError()) {
    App::log(tr("Uncaught exception at line %1: %2\n")
                     .arg(result.property("lineNumber").toInt())
                     .arg(result.toString()));
    return;
  }
  QJSValue f=global.property("f");
  for(double x=_minX;x<=_maxX;x+=_dx) {
    QJSValueList args;
    args << x;
    sOut << QString("%1 %2\n").arg(x, 0, 'g', 16).arg(f.call(args).toNumber(), 0, 'g', 16);
  }
  sOut << flush;
}

bool CurveReader::terminate()
{
  TRACE;
  QTextStream sOut(stdout);
  switch(_mode) {
  case MinMax:
    sOut << "# Min\n"
         << _minCurve.toString()
         << "# Max\n"
         << _maxCurve.toString();
    break;
  default:
    break;
  }
  return true;
}
