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

#include "ArrayResponse.h"
#include "WaveNumAnimate.h"

namespace ArrayGui {

ArrayResponse::ArrayResponse(QWidget *parent, Qt::WindowFlags f)
  : GraphicSheetMenu(parent, f),
    waveNum(this),
    _palette(nullptr)
{
  TRACE;
  setAttribute(Qt::WA_DeleteOnClose);
  _palette.generateColorScale(20, ColorPalette::McNamesClip, true);
  _palette.setObjectName("Palette");

  Settings::getSize(this, "ArrayResponse");
  _childLayout->addWidget(&waveNum);
  connect(waveNum.kminSpin, SIGNAL(valueChanged(double)),
           this, SLOT(setKmin( double)));
  connect(waveNum.kmaxSpin, SIGNAL(valueChanged(double)),
           this, SLOT(setKmax( double)));
  connect(waveNum.azimuthSpin, SIGNAL(valueChanged(double)),
           this, SLOT(setAzimuth( double) ));

  _kmin=0.0;
  _kmaxSolver=nullptr;
  _kmaxTouched=false;
  _kmaxThreshold=0.25;

  _gridLayer=nullptr;
  _circleLayer=nullptr;
  _directionLayer=nullptr;
  _crossGrayLayer=nullptr;
  _midHeightLayer=nullptr;
  _crossBlackLayer=nullptr;
  _dispLimitLayer=nullptr;
  _dispLambdaLayer=nullptr;
  _lambdaLegend=nullptr;
}

ArrayResponse::~ArrayResponse()
{
  TRACE;
  if(_kmaxSolver) {
    _kmaxSolver->terminate();
    delete _kmaxSolver;
  }
}

void ArrayResponse::setArray(const QVector<Point2D>& p)
{
  TRACE;
  if(_kmaxSolver) {
    _kmaxSolver->terminate();
    _kmaxSolver->deleteLater();
    _kmaxSolver=nullptr;
  }

  _kmaxTouched=false;

  _gridLayer->lockDelayPainting(); // required before touching the stations
  _stations=p;
  _gridLayer->setFunction(new TheoreticalFK(_stations));
  shiftStations();
  _gridLayer->unlock();

  _crossBlackLayer->lockDelayPainting();
  _crossBlackLayer->line(0)->clear();
  _crossBlackLayer->line(1)->clear();
  _crossBlackLayer->unlock();
  _crossGrayLayer->lockDelayPainting();
  _crossGrayLayer->clear();
  _crossGrayLayer->unlock();

  KminSolver kminSolver(_stations);
  bool ok=true;
  _kmin=kminSolver.calculate(ok);
  if(ok && _kmin>0.0) {
    _kmaxSolver=new KmaxSolver(_stations, _kmin, _kmaxThreshold);
    connect(&_kmaxTimer, SIGNAL(timeout()), this, SLOT(setTemporaryKmax()));
    connect(_kmaxSolver, SIGNAL(finished()), this, SLOT(setComputedKmax()));
    _kmaxSolver->calculate();
    _kmaxTimer.start(1000);
  } else {
    _comments->setText(tr("Error computing kmin (linear array?)"));
    _comments->update();
  }
  setAzimuth(waveNum.azimuthSpin->value());
  waveNum.kminSpin->setValue(_kmin);
  setKmin(_kmin);

  if(_dispLambdaLayer) {
    _dispLambdaLayer->setArraySize(arraySize());
  }
}

double ArrayResponse::arraySize() const
{
  TRACE;
  double maxD=0.0;
  int n=_stations.count();
  for(int i=0; i<n; i++) {
    for(int j=i+1; j<n; j++) {
      double d=_stations.at(i).distanceTo(_stations.at(j));
      if(d>maxD) {
        maxD=d;
      }
    }
  }
  return maxD;
}
void ArrayResponse::setTemporaryKmax()
{
  TRACE;
  if(_kmaxSolver) { // Only while kmax solver is still running
    bool ok;
    double kmax=_kmaxSolver->kmax(ok);
    initKmax(kmax);
    if(!_kmaxTouched) {
      emit kmaxChanged(kmax); // update dispersion plot
    }
    static const QString cmt=tr("kmin: %1 rad/m\n"
                                "kmax: at least %2 rad/m (still searching)\n");
    _comments->setText(cmt.arg(_kmin).arg(kmax));
    _comments->update();
  }
}

void ArrayResponse::setComputedKmax()
{
  TRACE;
  // Upon termination, _kmaxSolver might be replaced by another solver
  // and this function called by an old event still in the stack
  // Make sure that it still refers to the current solver.
  KmaxSolver * solver=qobject_cast<KmaxSolver *>(sender());
  if(solver && _kmaxSolver==solver) {
    _kmaxTimer.stop();
    bool ok;
    double kmax=_kmaxSolver->kmax(ok);
    if(ok) {
      initKmax(kmax);
      scanAzimuth();

      static const QString cmt=tr("kmin: %1 rad/m\n"
                                  "kmax: %2 rad/m\n"
                                  "ground level: %3\n"
                                  "kmax-kmin: %4 rad/m\n"
                                  "kaverage: %5 rad/m");
      KminSolver kminSolver(_stations);
      double groundLevel=kminSolver.groundLevel(_kmin,kmax);
      _comments->setText(cmt.arg(_kmin).arg(kmax).arg(groundLevel).arg(kmax-_kmin).arg(0.5*(kmax+_kmin)));
      _comments->update();

      if(!_kmaxTouched) {
        waveNum.kmaxSpin->setValue(kmax);
        setKmax(kmax);
      }
    } else {
      static const QString cmt=tr("kmin: %1 rad/m\n"
                                  "kmax: error\n");
      _comments->setText(cmt.arg(_kmin));
      _comments->update();
    }

    _kmaxSolver->deleteLater();
    _kmaxSolver=nullptr;
  }
}

void ArrayResponse::initKmax(double kmax)
{
  Rect r;
  r.setLimits(-kmax, -kmax, kmax, kmax);
  r.enlarge(1.0, LinearScale, LinearScale);

  AxisWindow * w;
  w=_gridLayer->graph();
  w->xAxis()->setRange(r.x1(), r.x2());
  w->yAxis()->setRange(r.y1(), r.y2());
  w->deepUpdate();

  w=_crossGrayLayer->graph();
  double crossLimit=2.0*sqrt(2)*kmax;
  w->xAxis()->setRange(-crossLimit, crossLimit);
  w->deepUpdate();
}

void ArrayResponse::setFrequencySampling(const SamplingParameters& fparam)
{
  if(_dispLimitLayer) {
    _dispLimitLayer->setFrequencySampling(fparam);
    _dispLimitLayer->graph()->xAxis()->setRange(fparam.minimum() * 0.95, fparam.maximum() * 1.05);
    _dispLambdaLayer->setFrequencySampling(fparam);
    _dispLambdaLayer->graph()->xAxis()->setRange(fparam.minimum() * 0.95, fparam.maximum() * 1.05);
  }
}

void ArrayResponse::createObjects(bool showDispersion)
{
  TRACE;
  AxisWindow * w;

  // Color palette
  _palette.setPrintXAnchor(9.5);
  _palette.setPrintYAnchor(0.5);
  _palette.setOrientation(Axis::West);
  _palette.setPrintWidth(1.7);
  _palette.setPrintHeight(8.0);
  _palette.setTitle(tr("Array transfer function"));
  _sheet.addObject(&_palette);

  // Wavenumber map
  w=sheet()->addGraph();
  w->xAxis()->setTitle(tr("Wave number X (rad/m)"));
  w->xAxis()->setTitleInversedScale(tr("Wave length X/(2*pi) (m/rad)"));
  w->yAxis()->setTitle(tr("Wave number Y (rad/m)"));
  w->yAxis()->setTitleInversedScale(tr("Wave length Y/(2*pi) (m/rad)"));
  w->xAxis()->setSizeType(Axis::AxisSize);
  w->xAxis()->setSizeInfo(7);
  w->setPrintXAnchor(0.5);
  w->yAxis()->setSizeType(Axis::AxisSize);
  w->yAxis()->setSizeInfo(7);
  w->setPrintYAnchor(0.5);
  w->updateExternalGeometry();
  w->updateGeometry();
  _gridLayer=new LiveGridLayer(w);
  _gridLayer->setObjectName("theoretical FK map");
  _gridLayer->setSampling(3);
  _gridLayer->setColorMap(_palette.colorMap());
  _gridLayer->setAutoAdjust(false);
  connect(_gridLayer, SIGNAL(colorMapChanged(ColorMap)), &_palette, SLOT(setColorMap(const ColorMap&)));
  connect(&_palette, SIGNAL(changed(ColorMap)), _gridLayer, SLOT(setColorMap(const ColorMap&)));
  _circleLayer=new CircleViewer(w);
  _circleLayer->setObjectName( "FK limits" );
  _circleLayer->resize(2);
  _directionLayer=new LineLayer(w);
  _directionLayer->setObjectName( "Azimuth" );
  _directionLayer->setEditable(false);
  PlotLine2D * line;
  line=new PlotLine2D;
  line->setPen(Pen( Qt::black, 0.6, Qt::SolidLine));
  line->setSymbol(Symbol());
  _directionLayer->setReferenceLine(line);
  static_cast<PlotLine2D *>(_directionLayer->addLine())->curve().resize(2);
  static_cast<PlotLine2D *>(_directionLayer->addLine())->curve().resize(2);
  _palette.setVLinear(0.0, 1.0);

  // Wavenumber map cross section
  w=sheet()->addGraph();
  w->xAxis()->setTitle(tr("Wave number (rad/m)"));
  w->xAxis()->setTitleInversedScale(tr("Wave length/(2*pi) (m/rad)"));
  w->yAxis()->setTitle(tr("Array Transfer function"));
  w->yAxis()->setTitleInversedScale(tr("1/Array Transfer function"));
  w->setGeometry(0.5, 9.0, 9.0, 6.0);
  _crossGrayLayer=new XUniqueYColorLines(w);
  _crossGrayLayer->setObjectName( "Cross background" );
  _crossGrayLayer->setLineWeight(0.025);
  _crossBlackLayer=new LineLayer(w);
  _crossBlackLayer->setObjectName( "Cross current" );
  line=new PlotLine2D;
  line->setPen(Pen( Qt::black, 0.6, Qt::SolidLine) );
  line->setSymbol(Symbol());
  _crossBlackLayer->setReferenceLine(line);
  _crossBlackLayer->addLine();
  _crossBlackLayer->addLine();
  _midHeightLayer=new LineLayer(w);
  _midHeightLayer->setEditable(false);
  line=new PlotLine2D;
  line->setPen(Pen( Qt::black, 0.4, Qt::SolidLine) );
  line->setSymbol(Symbol());
  _midHeightLayer->setReferenceLine(line);
  Curve<Point2D>& limitLine=static_cast<PlotLine2D *>(_midHeightLayer->addLine())->curve();
  limitLine.append(Point2D(-std::numeric_limits<double>::infinity(),0.5));
  limitLine.append(Point2D(std::numeric_limits<double>::infinity(),0.5));
  w->yAxis()->setRange(0.0, 1.0);

  if(showDispersion) {
    // Frequency-Velocity plot
    w=sheet()->addGraph();
    w->xAxis()->setFrequency();
    w->yAxis()->setTitle(tr("Slowness (s/m)"));
    w->yAxis()->setTitleInversedScale(tr("Velocity (m/s)"));
    w->setGeometry(10.0, 9.0, 9.0, 6.0);
    _dispLimitLayer=new DispersionLimitLayer(w);
    _dispLimitLayer->setObjectName("FK dispersion limits");
    _dispLimitLayer->addArrayLimits();
    connect(this, SIGNAL(kminChanged(double)), _dispLimitLayer, SLOT(setArrayKmin(double)));
    connect(this, SIGNAL(kmaxChanged(double)), _dispLimitLayer, SLOT(setArrayKmax(double)));
    w->yAxis()->setRange(0.0, 0.01);

    // Legend for lambda ratios
    _lambdaLegend=new LegendWidget;
    _lambdaLegend->setPrintXAnchor(5.0);
    _lambdaLegend->setPrintYAnchor(15.5);
    _lambdaLegend->setTitle(tr("Lambda/ArraySize"));
    Legend l;
    l.generateColorScale(3, ColorPalette::Sardine);
    for(int i=0; i<3; i++) {
      l.setText(i, QString::number(i+1));
    }
    _lambdaLegend->legend()=l;
    _lambdaLegend->setObjectName("LambdaArraySizeLegend");
    _sheet.addObject(_lambdaLegend);
    _lambdaLegend->setAdjustBox(true);

    // Frequency-Velocity plot for lambda ratios
    w=sheet()->addGraph();
    w->xAxis()->setFrequency();
    w->yAxis()->setTitle(tr("Slowness (s/m)"));
    w->yAxis()->setTitleInversedScale(tr("Velocity (m/s)"));
    w->setGeometry(10.0, 15.5, 9.0, 6.0);
    _dispLambdaLayer=new DispersionLimitLayer(w);
    _dispLambdaLayer->setObjectName("Lambda/ArraySize ratio");
    connect(_lambdaLegend, SIGNAL(changed(Legend)), _dispLambdaLayer, SLOT(setLambdaLegend(const Legend&)));
    _dispLambdaLayer->setLambdaLegend(_lambdaLegend->legend());
    connect(this, SIGNAL(kminChanged(double)), _dispLimitLayer, SLOT(setArrayKmin(double)));
    w->yAxis()->setRange(0.0, 0.01);
  } else {
    _dispLimitLayer=nullptr;
    _dispLambdaLayer=nullptr;
  }

  // Comments
  _comments=sheet()->addText();
  _comments->setGeometry(11.5, 0.5, 5.0, 5.0);
}

void ArrayResponse::resizeEvent (QResizeEvent * )
{
  TRACE;
  Settings::setSize(this, "ArrayResponse" );
}

/*!
  Shift station to have the array center at (0,0)
*/
void ArrayResponse::shiftStations()
{
  TRACE;
  uniquePoints();
  double x0=0, y0=0;
  int n=_stations.count();
  // Get min and max distances
  for(int i=0;i < n;i++ ) {
    x0 += _stations[ i ].x();
    y0 += _stations[ i ].y();
  }
  // Calculate relative coordinates to avoid big number computations
  x0 /= n;
  y0 /= n;
  for(int i=0;i < n;i++ ) {
    _stations[ i ].setX(_stations[ i ].x() - x0);
    _stations[ i ].setY(_stations[ i ].y() - y0);
  }
}

void ArrayResponse::uniquePoints()
{
  TRACE;
  int i, j, is=0, n=_stations.count();
  Point * pList=new Point[n];
  for(i=0;i < n;i++ ) {
    pList[ i ]=_stations[ i ];
  }
  for(i=0;i < n;i++ ) {
    for(j=i + 1;j < n;j++ ) {
      if(pList[ i ]==pList[ j ] )
        break;
    }
    if(j==n) {
      _stations[ is ]=pList[ i ];
      is++;
    } else {
      _stations.pop_back();
    }
  }
  delete [] pList;
}

/*!
  Calculate kmax from minimum distance between stations
*/
double ArrayResponse::theoreticalKmax() const
{
  TRACE;
  KminSolver s(_stations);
  double dmin, dmax;
  s.distanceRange(dmin, dmax);
  // k=2*M_PI/lambda, due to Nyquist, d<lambda/2
  return M_PI/dmin;
}

/*!
  Grad kmax from axis limits of grid plot
*/
double ArrayResponse::gridKmax() const
{
  TRACE;
  AxisWindow * w=_gridLayer->graph();
  double kmax=w->xAxis()->maximum();
  double k=-w->xAxis()->minimum();
  if(k>kmax) kmax=k;
  k=-w->yAxis()->maximum();
  if(k>kmax) kmax=k;
  k=-w->yAxis()->minimum();
  if(k>kmax) kmax=k;
  return kmax;
}

double ArrayResponse::gridKmin() const
{
  TRACE;
  GraphContent * gc=_gridLayer->graphContent();
  return fabs(gc->options().xs2r(_gridLayer->sampling())-gc->options().xs2r(0));
}

void ArrayResponse::setKmin(double kmin)
{
  TRACE;
  _circleLayer->set(0, 0, 0, 0.5*kmin, 0.5*kmin, 0.0, Qt::black);
  _circleLayer->deepUpdate();
  emit kminChanged(kmin);
  setAzimuth(waveNum.azimuthSpin->value());
}

double ArrayResponse::kmin() const
{
  return waveNum.kminSpin->value();
}

void ArrayResponse::setKmax(double kmax)
{
  TRACE;
  _kmaxTouched=true;
  _circleLayer->set(1, 0, 0, kmax, kmax, 0.0, Qt::black);
  _circleLayer->deepUpdate();
  emit kmaxChanged(kmax);
  setAzimuth(waveNum.azimuthSpin->value());
}

double ArrayResponse::kmax() const
{
  return waveNum.kmaxSpin->value();
}

void ArrayResponse::setAzimuth(double azimuth)
{
  TRACE;
  LayerLocker lla(_directionLayer);
  Curve<Point2D>& azLine1=static_cast<PlotLine2D *>(_directionLayer->line(0) )->curve();
  Curve<Point2D>& azLine2=static_cast<PlotLine2D *>(_directionLayer->line(1) )->curve();

  double kminHalf=0.5 * waveNum.kminSpin->value();
  double kmax=waveNum.kmaxSpin->value();

  KminSolver kSolver(_stations);
  kSolver.setAzimuth(azimuth);
  azLine1[ 0 ].setX(kminHalf * kSolver.angle().cos());
  azLine1[ 0 ].setY(kminHalf * kSolver.angle().sin());
  azLine1[ 1 ].setX(kmax * kSolver.angle().cos());
  azLine1[ 1 ].setY(kmax * kSolver.angle().sin());
  azLine2[ 0 ].setX( -kminHalf * kSolver.angle().cos());
  azLine2[ 0 ].setY( -kminHalf * kSolver.angle().sin());
  azLine2[ 1 ].setX( -kmax * kSolver.angle().cos());
  azLine2[ 1 ].setY( -kmax * kSolver.angle().sin());

  // A cross section across grid is slower than a complete recomputation==>as for scan, create a specific layer
  Curve<Point2D>& crossCurve1=static_cast<PlotLine2D *>(_crossBlackLayer->line(0) )->curve();
  Curve<Point2D>& crossCurve2=static_cast<PlotLine2D *>(_crossBlackLayer->line(1) )->curve();

  LayerLocker llc(_crossBlackLayer);
  crossCurve1.clear();
  crossCurve2.clear();

  double dk=0.005*(kmax-kminHalf);
  for(double k=-kmax; k < -kminHalf; k+=dk) {
    crossCurve1.append(Point(k, kSolver.value(k)));
  }
  crossCurve1.append(Point(-kminHalf, kSolver.value( -kminHalf) ));
  for(double k=kminHalf; k < kmax; k+=dk) {
    crossCurve2.append(Point(k, kSolver.value(k)));
  }
  crossCurve2.append(Point(kmax, kSolver.value(kmax) ));

  _crossBlackLayer->resetLineProperties();
  _crossBlackLayer->deepUpdate();
  _directionLayer->deepUpdate();
}

/*!
  Called only once at initialization

  \todo calculate the scan in another thread==> or better define a special layer
*/
void ArrayResponse::scanAzimuth()
{
  TRACE;
  double kmax=gridKmax();
  int n=(int)round(3.0*gridKmax()/gridKmin());
  int n21=2*n+1;
  KminSolver fkmap(_stations);
  _crossGrayLayer->setPointCount(180, n21);
  // Set common x
  double * k=_crossGrayLayer->x();
  double dk=sqrt(2) * kmax/(double) n;
  for(int i=0; i<n21; i++ ) {
    k[i]=(double) (i-n) * dk;
  }
  // Set y and color
  double * y=_crossGrayLayer->y();
  Color * colors=new Color[180];
  double dAng=M_PI/180; // every one degree
  for(int iAng=0; iAng<180; iAng++) {
    double ang=(double)iAng*dAng;
    fkmap.setRadians(ang);
    for(int i=0; i < n21; i++ ) {
      *(y++)=fkmap.value(k[i] );
    }
    colors[iAng]=Qt::gray;
  }
  _crossGrayLayer->setColors(colors);
}

} // namespace ArrayGui
