/***************************************************************************
**
**  This file is part of geopsydamping.
**
**  geopsydamping 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.
**
**  geopsydamping 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-04-15
**  Copyright: 2004-2019
**    Marc Wathelet
**    Marc Wathelet (ULg, Liège, Belgium)
**    Marc Wathelet (LGIT, Grenoble, France)
**
***************************************************************************/

#include <GeopsyGui.h>

#include "DampingResults.h"
#include "Parameters.h"

Curve<Point2D> DampingResults::_aveCurve;
int DampingResults::_nSampFit=0;
double DampingResults::_fitW=0;

DampingResults::DampingResults(QWidget *parent) :
    GraphicSheetMenu(parent)
{
  TRACE;
  setObjectName("DampingResults");

  QAction * a;

  menuTools=addMenu(tr("&Tools"));

  a=new QAction(tr("&Save results"), this);
  a->setStatusTip(tr("Save results of spectral analysis in a text file with process information (.log file)"));
  connect(a, SIGNAL(triggered()), this, SLOT(save()));
  menuTools->addAction(a);
}

DampingResults::~DampingResults()
{
  TRACE;
  delete [] _comments;
  delete [] _averageCurveLayers;
  //delete [] _stddevCurveLayers;
  delete [] _fitCurveLayers;
}

void DampingResults::createObjects(SubSignalPool * subPool)
{
  TRACE;
  sheet()->setStatusBar(GeopsyGuiEngine::instance()->statusBar());
  double y=0.5;

  // Resize containers to fit the number of signals
  int n=subPool->count();
  _comments=new TextEdit* [n];
  _averageCurveLayers=new LineLayer* [n];
  //_stddevCurveLayers=new LineLayer* [n];
  _fitCurveLayers=new LineLayer* [n];
  _results.resize(n);

  for(int ig=0; ig<n; ig++) {
    AxisWindow * w=sheet()->addGraph();
    w->setGeometry(0.5, y, 15.0, 6.0);
    Signal * sig=subPool->at(ig);
    w->xAxis()->setTitle("Time (s)");
    w->xAxis()->setNumberPrecision(2);
    w->xAxis()->setTitleInversedScale("1/Time (1/s)");
    w->xAxis()->setNumberPrecisionInversedScale(2);
    w->yAxis()->setTitle(sig->nameComponent());
    w->yAxis()->setNumberType('e');
    w->yAxis()->setNumberPrecision(1);
    w->yAxis()->setTitleInversedScale(sig->nameComponent());
    w->yAxis()->setNumberPrecisionInversedScale(1);

    PlotLine2D * line;
    LineLayer * layer;

    layer=new LineLayer(w);
    line=new PlotLine2D;
    line->setPen(Pen(Qt::black, 0.6, Qt::SolidLine));
    layer->setReferenceLine(line);
    layer->setObjectName("average");
    _averageCurveLayers[ig]=layer;

    /*layer=new LineLayer(w);
    line=new PlotLine2D;
    line->setPen(Pen(Qt::black, 0.6, Qt::DotLine));
    layer->setReferenceLine(line);
    layer->setObjectName("stddev");
    _stddevCurveLayers[ig]=layer;*/

    layer=new LineLayer(w);
    line=new PlotLine2D;
    line->setPen(Pen(Qt::red, 0.3, Qt::SolidLine));
    layer->setReferenceLine(line);
    layer->setObjectName("fit");
    _fitCurveLayers[ig]=layer;

    _comments[ig]=sheet()->addText();
    _comments[ig]->setGeometry(16.0, y, 4.0, 5.0);
    _results[ig].name=sig->nameComponent();
    y+=6.0;
  }
}

bool DampingResults::compute(int ig, const Signal *sig, const Parameters& param)
{
  TRACE;
  _log=param.toString();
  _averageCurveLayers[ig]->clear();
  //_stddevCurveLayers[ig]->clear();
  _fitCurveLayers[ig]->clear();
  // Cut the time window to process
  TimeRange tw=param.timeRange().absoluteRange(sig);
  double dt=sig->samplingPeriod();
  double sampFreq=sig->samplingFrequency();
  int nSampSig=tw.lengthSamples(sampFreq);
  int nSampWin=qCeil(param.windowLength()*sampFreq);
  _nSampFit=qCeil(param.fitLength()*sampFreq);
  if(nSampWin>nSampSig) nSampWin=nSampSig;
  if(_nSampFit>nSampWin) {
    Message::warning(MSG_ID, tr("Damping toolbox"),
                     tr("The fitting length cannot be greater than the window length. "
                        "Continuing with a fitting length reduced to window length"), Message::ok(), true);
    _nSampFit=nSampWin;
  }
  // Get result vectors ready
  _aveCurve.clear();
  // Standard deviations are meannigless, hence commented
  //Curve<Point2D> minCurve, maxCurve;
  Curve<Point2D> fitCurve;
  _aveCurve.resize(nSampWin);
  //minCurve.resize(nSampWin);
  //maxCurve.resize(nSampWin);
  fitCurve.resize(_nSampFit);
  // Set x coordinates for curves, and init y
  double t=0;
  int iw;
  for(iw=0;iw < _nSampFit;iw++, t+=dt) {
    _aveCurve[iw].setX(t);
    _aveCurve[iw].setY(0);
    //minCurve[iw].setX(t);
    //minCurve[iw].setY(0);
    //maxCurve[iw].setX(t);
    //maxCurve[iw].setY(0);
    fitCurve[iw].setX(t);
    fitCurve[iw].setY(0);
  }
  for( ;iw < nSampWin;iw++, t+=dt) {
    _aveCurve[iw].setX(t);
    _aveCurve[iw].setY(0);
    //minCurve[iw].setX(t);
    //minCurve[iw].setY(0);
    //maxCurve[iw].setX(t);
    //maxCurve[iw].setY(0);
  }
  int nWin=0;
  double tau, val; // time shift relative to signal sampling

  const DoubleSignal * sigfft=sig->saveType(DoubleSignal::Waveform);
  DoubleSignal * tSig=new DoubleSignal(nSampSig);
  tSig->initValues(0.0, 0, nSampSig);
  tSig->setSamplingPeriod(dt);
  tSig->copySamplesFrom(sigfft, sig->startTime().secondsTo(tw.start()), 0.0, tw.lengthSeconds());
  tSig->subtractValue();
  sig->restoreType(sigfft);
  if(param.isFilter()) {
    tSig->filter(param.filter());
  }
  CONST_LOCK_SAMPLES(double, samp, tSig)
    // Stack windows (sum for ave, sum2 for min)
    int ns=nSampSig - nSampWin - 1;
    for(int is=0;is < ns;is++ ) {
      if(samp[is]<=0.0 && samp[is+1]>=0.0) {
        nWin++;
        if(samp[is]==0.0)
          tau=0;
        else
          tau=samp[ is ]/(samp[ is ] - samp[ is + 1 ] );
        for(iw=1;iw < nSampWin;iw++ ) {
          val=samp[ is + iw ] + tau * (samp[ is + iw + 1 ] - samp[ is + iw ] );
          _aveCurve[iw].setY(_aveCurve.at(iw).y()+val);
          //minCurve[iw].setY(minCurve.at(iw).y()+val*val);
        }
      }
    }
  UNLOCK_SAMPLES(tSig);
  delete tSig;
  // Compute average and std deviation
  for(iw=1;iw < nSampWin;iw++ ) {
    val=_aveCurve.at(iw).y()/(double)nWin;
    _aveCurve[iw].setY(val);
    //tau=sqrt(minCurve[iw].y()/(double) nWin - val * val);
    //minCurve[iw].setY(val - tau);
    //maxCurve[iw].setY(val + tau);
  }
  // Fit function A/w*e^(-Zwt)*sin(wt), A, w, Z are the unknowns
  // Use the downhill simplex algorithm to find the minimum
  // Init the algorithm with good estimates. Estimate the amplitude of signal
  // average at the first absolute maximum
  double z=0.01, a=0;
  int iw0=0, nw0=0;
  int nw=_nSampFit-1;
  for(iw=1; iw<nw; iw++) {
    if(_aveCurve[iw].y()<0 && _aveCurve[iw+1].y()>0) {
      nw0++;
      iw0=iw;
    }
  }
  if(iw0==0) {
    Message::warning(MSG_ID, tr("Damping"),
                     tr("For signal %1, the main period is larger than the fitting length. "
                        "Check the fitting length, the signal and the time range.")
                     .arg(sig->nameComponent()), Message::cancel());
    return false;
  }
  _fitW=2*nw0*M_PI/(iw0*dt);
  // a=aveCurve->at(1).y/dt;
  for(iw=1; iw<_nSampFit; iw++) {
    if(fabs(_aveCurve.at(iw).y())>a) {
      a=fabs(_aveCurve.at(iw).y());
      iw0=iw;
    }
  }
  a*=_fitW/exp(-z*_fitW*iw0*dt);
  // Init an initial tetrahedron
  double * p[ 3 ], pv[ 6 ], y[ 3 ];

  p[ 0 ]=pv;
  p[ 1 ]=pv + 2;
  p[ 2 ]=pv + 4;
  //  p[0][0]=a;
  //  p[0][1]=w;
  //  p[0][2]=z;
  //  y[0]=misfitFunk(p[0]);
  //  p[1][0]=a*1.01;
  //  p[1][1]=w*1.1;
  //  p[1][2]=z*1.5;
  //  y[1]=misfitFunk(p[1]);
  //  p[2][0]=a*0.99;
  //  p[2][1]=w*0.9;
  //  p[2][2]=z*0.5;
  //  y[2]=misfitFunk(p[2]);
  //  p[3][0]=a*1.005;
  //  p[3][1]=w*1.05;
  //  p[3][2]=z*1.2;
  //  y[3]=misfitFunk(p[3]);
  //  p[0][0]=a*0.9;
  //  p[0][1]=w;
  //  p[0][2]=z*0.5;
  //  y[0]=misfitFunk(p[0]);
  //  p[1][0]=a*1.1;
  //  p[1][1]=p[0][1];
  //  p[1][2]=p[0][2];
  //  y[1]=misfitFunk(p[1]);
  //  p[2][0]=a;
  //  p[2][1]=w;
  //  p[2][2]=p[0][2];
  //  y[2]=misfitFunk(p[2]);
  //  p[3][0]=a;
  //  p[3][1]=w;
  //  p[3][2]=z*1.5;
  //  y[3]=misfitFunk(p[3]);
  p[ 0 ][ 0 ]=a * 0.9;
  p[ 0 ][ 1 ]=z * 0.9;
  y[ 0 ]=misfitFunk(p[ 0 ] );
  p[ 1 ][ 0 ]=a * 0.9;
  p[ 1 ][ 1 ]=z * 1.1;
  y[ 1 ]=misfitFunk(p[ 1 ] );
  p[ 2 ][ 0 ]=a * 1.1;
  p[ 2 ][ 1 ]=z;
  y[ 2 ]=misfitFunk(p[ 2 ] );
  int nfunk;
  amoeba(p, y, 2, 2.5e-8, misfitFunk, &nfunk);
  App::log(tr( "%1 function evaluations were necessary to converge\n").arg(nfunk));
  // Get the mean a,w and z
  a=0.25 * (p[ 0 ][ 0 ] + p[ 1 ][ 0 ] + p[ 2 ][ 0 ] );
  z=0.25 * (p[ 0 ][ 1 ] + p[ 1 ][ 1 ] + p[ 2 ][ 1 ] );
  // get the fitCurve
  for(iw=1;iw < _nSampFit;iw++ ) {
    t=fitCurve.at(iw).x();
    fitCurve[iw].setY(a/_fitW * exp( -z * _fitW * t) * sin(_fitW * t) );
  }
  // Set comments
  _comments[ ig ] ->setText(tr("f=%1 Hz\nz=%2%\nN win=%3")
                             .arg(_fitW/(2 * M_PI), 0, 'f', 2)
                             .arg(z * 100.0, 0, 'f', 4)
                             .arg(nWin));
  _comments[ ig ] ->update();
  // Add curves to graph
  static_cast<PlotLine2D *>(_averageCurveLayers[ig]->addLine())->setCurve(_aveCurve);
  //static_cast<PlotLine2D *>(_stddevCurveLayers[ig]->addLine())->setCurve(minCurve);
  //static_cast<PlotLine2D *>(_stddevCurveLayers[ig]->addLine())->setCurve(maxCurve);
  static_cast<PlotLine2D *>(_fitCurveLayers[ig]->addLine())->setCurve(fitCurve);
  // Set limits correctly
  GraphContent * gc=_averageCurveLayers[ig]->graphContent();
  AxisWindow * w=gc->graph();
  Rect r=gc->boundingRect();
  w->xAxis()->setRange(r.x1(), r.x2());
  double maxY=-r.y1();
  if(r.y2()>maxY) maxY=r.y2();
  maxY*=1.05;
  w->yAxis()->setRange(-maxY, maxY);
  w->deepUpdate();
  // Keep results
  _results[ig].frequency=_fitW/(2*M_PI);
  _results[ig].damping=z*100.0;
  _results[ig].nWindows=nWin;
  return true;
}

/*!
  Damping sinusoide to fit to calculated data, returns a misfit
  Used with call back in amoeba
*/
double DampingResults::misfitFunk(double x[] )
{
  TRACE;
#define A x[0]
#define w _fitW
#define Z x[1]
  double misfit=0.0, t, y0;
  if(A < 0) return std::numeric_limits<double>::infinity();
  for(int i=1; i<_nSampFit; i++) {
    t=_aveCurve[i].x();
    y0=_aveCurve[i].y();
    y0=y0 - A/w * exp( -Z * w * t) * sin(w * t);
    misfit += y0 * y0;
  }
  misfit /= (double) _nSampFit;
  return misfit;
}

void DampingResults::save(QString fileName)
{
  TRACE;
  if(fileName.isEmpty()) {
    fileName=Message::getSaveFileName(tr("Save results"), tr("Damping results (*.damping)"));
  }
  if(fileName.isEmpty()) {
    return;
  }
  QString message;
  QFileInfo fi(fileName);
  if(fi.exists()) {
     message += tr( "File %1 already exists.\n" ).arg(fi.filePath());
  }
  QDir d(fi.absolutePath());
  QFileInfo filog(d.absoluteFilePath(fi.baseName() + ".log" ));
  if(filog.exists()) {
    message += tr( "File %1 already exists.\n" ).arg(filog.filePath());
  }
  if(!message.isEmpty()) {
    if(Message::warning(MSG_ID, tr( "Saving result files" ),
                              tr( "%1\nDo you want to replace it(them)?" ).arg(message),
                              Message::no(), Message::yes(), true)==Message::Answer0) return;
  }
  // Save a log file with parameters
  QFile flog(filog.filePath());
  if( !flog.open(QIODevice::WriteOnly) ) {
    Message::warning(MSG_ID, tr( "Saving log file" ),
                          tr( "Unable to open file %1 for writing" ).arg(filog.filePath()), Message::cancel(), true);
    return;
  }
  QTextStream slog(&flog);
  slog << "### Parameters ###\n"
       << _log
       << "### End Parameters ###" << endl;
  // Save a result file with frequency, damping, number of windows
  QFile fres(fileName);
  if( !fres.open(QIODevice::WriteOnly) ) {
    Message::warning(MSG_ID, tr( "Saving result file" ),
                          tr( "Unable to open file %1 for writing" ).arg(fileName), Message::cancel(), true);
    return;
  }
  QTextStream sres(&fres);
  sres << "# Name | Frequency | Damping | Number of windows" << endl;
  for(QVector<Result>::iterator it=_results.begin();it!=_results.end(); it++) {
    sres << it->name << "\t"
         << QString::number(it->frequency) << "\t"
         << QString::number(it->damping) << "\t"
         << QString::number(it->nWindows) << endl;
  }
}
