/***************************************************************************
**
**  This file is part of gpmaplayer.
**
**  gpmaplayer 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.
**
**  gpmaplayer 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: 2016-03-25
**  Copyright: 2016-2019
**    Marc Wathelet (ISTerre, Grenoble, France)
**
***************************************************************************/

#include <SciFigs.h>
#include <QGpGuiMath.h>

#include "ImageSource.h"
#include "ColumnImageMerger.h"
#include "RowImageMerger.h"

#define IMAGE_SIZE 1280
#define IMAGE_FOOTER 50
#define IMAGE_OVERLAP 300
#define IMAGE_OVERLAP_RANGE 150

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

  Full description of class still missing
*/

/*!
  Description of constructor still missing
*/
ImageSource::ImageSource(QObject * parent)
  : QObject(parent)
{
  TRACE;
  if(HttpAccess::isAvailable()) {
    _access=new HttpAccess("http://www.geopsy.org/access_testing.txt", this);
    connect(_access, SIGNAL(ready()), this, SLOT(setStep()));
    connect(_access, SIGNAL(finished(bool)), this, SLOT(imageDownloaded(bool)));
  } else {
    _access=0;
  }
  connect(this, SIGNAL(imageReady(const QImage&)), this, SLOT(processImage(const QImage&)), Qt::QueuedConnection);
  setZoom(16);
  _outputBaseName="map";
}

/*!
  Description of destructor still missing
*/
ImageSource::~ImageSource()
{
  TRACE;
  qDeleteAll(_columns);
}

void ImageSource::setZoom(int z)
{
  TRACE;
  _zoom=z;
  _pixelShift=0.005/pow(2.0,_zoom-14);
  _pixelShift=Number::round(_pixelShift, _pixelShift);
}

void ImageSource::setRect(Rect r)
{
  TRACE;
  _rect=r;
  App::log(tr("Map rectangle from [%1] to [%2]\n")
                   .arg(r.topLeft().toString(6,'f'))
                   .arg(r.bottomRight().toString(6,'f')));
}

void ImageSource::start()
{
  TRACE;
  _mode=MergeColumns;
  for(double lon=_rect.x1(); lon<=_rect.x2(); lon+=_step.x()) {
    _columns.append(new ColumnImageMerger(lon, _rect.y1(), _rect.y2(), _step.y()));
  }
  _currentIndex=0;
  request(_columns.at(_currentIndex)->center());
}

void ImageSource::request(const Point2D& center)
{
  TRACE;
  QFileInfo fi(imageFileName(center));
  if(fi.exists()) {
    App::log(tr("Loading file '%1'\n").arg(fi.fileName()) );
    QImage im;
    if(im.load(fi.fileName())) {
      emit imageReady(im);
      return;
    }
  }
  App::log(tr("Downloading file '%1'\n").arg(fi.fileName()) );
  // https://developers.google.com/maps/documentation/static-maps/intro#quick_example
  // maptype: roadmap, satellite, hybrid and terrain
  _access->get(QString("https://maps.googleapis.com/maps/api/staticmap?center=%1,%2&zoom=%3&size=640x640&maptype=satellite&scale=2")
               .arg(center.y())
               .arg(center.x())
               .arg(_zoom));
}

void ImageSource::imageDownloaded(bool)
{
  TRACE;
  QImage im;
  im.loadFromData(_access->receivedData(), "PNG");
  // Remove google banner at the bottom
  im=im.copy(0, 0, im.width(), im.height()-IMAGE_FOOTER)
       .convertToFormat(QImage::Format_RGB32);
  processImage(im);
}

void ImageSource::processImage(const QImage& im)
{
  switch(_mode) {
  case SetStepMaster:
    setStepMaster(im);
    break;
  case SetStepSlaveX:
    setStepSlaveX(im);
    break;
  case SetStepSlaveY:
    setStepSlaveY(im);
    break;
  case MergeColumns:
    mergeColumns(im);
    break;
  }
}

void ImageSource::mergeColumns(const QImage& im)
{
  TRACE;
  ColumnImageMerger * cm=_columns.at(_currentIndex);
  QSaveFile f(imageFileName(cm->center()));
  if(im.save(&f, "PNG")) {
    f.commit();
  }
  //cm->setImage(im, IMAGE_SIZE-IMAGE_FOOTER-IMAGE_OVERLAP-IMAGE_OVERLAP_RANGE,
  //                 IMAGE_SIZE-IMAGE_FOOTER-IMAGE_OVERLAP+IMAGE_OVERLAP_RANGE);
  cm->setImage(im, 400, 1000);
  if(cm->atEnd()) {
    _currentIndex++;
    if(_currentIndex<_columns.count()) {
      request(_columns.at(_currentIndex)->center());
    } else {
      mergeRows();
    }
  } else {
    request(cm->center());
  }
}

void ImageSource::mergeRows()
{
  TRACE;
  // Finish merging all columns, now merge them together
  RowImageMerger m;
  for(int i=_columns.count()-1; i>=0; i--) {
    m.setImage(_columns.at(i)->image(), 600, 1000);
  }
  App::log(tr("Global image %1x%2 pixels\n").arg(m.image().width()).arg(m.image().height()) );
  m.image().save(_outputBaseName+"-geo.png", "PNG");

  if(_columns.count()==1 && _columns.first()->count()==1) {
    App::log(tr("Only one image, cannot compute scale. Enlarge the radius or increase the zoom.\n") );
    ::exit(2);
  }

  // Compute scale
  Point2D realNW=_columns.first()->center(_columns.first()->count()-1);
  Point2D realNE=_columns.last()->center(_columns.last()->count()-1);
  Point2D realSW=_columns.first()->center(0);
  Point2D realSE=_columns.last()->center(0);
  realNW.geographicalToUtm(_utmZone);
  realNE.geographicalToUtm(_utmZone);
  realSW.geographicalToUtm(_utmZone);
  realSE.geographicalToUtm(_utmZone);
  App::log(tr("North west reference %1 UTM\n").arg(realNW.toString(2, 'f')) );
  App::log(tr("North east reference %1 UTM\n").arg(realNE.toString(2, 'f')) );
  App::log(tr("South west reference %1 UTM\n").arg(realSW.toString(2, 'f')) );
  App::log(tr("South east reference %1 UTM\n").arg(realSE.toString(2, 'f')) );

  // Unfortunately, UTM coordinates are not aligned to latitude and longitude axis
  // According to position in UTM cell, there may be a rotation of the axis
  // Get the best estimate of the local rotation
  App::log(tr("NW to NE %1 degrees\n").arg(Angle::radiansToDegrees(realNW.azimuthTo(realNE))) );
  App::log(tr("SW to SE %1 degrees\n").arg(Angle::radiansToDegrees(realSW.azimuthTo(realSE))) );
  App::log(tr("NW to SW %1 degrees\n").arg(Angle::radiansToDegrees(realNW.azimuthTo(realSW))) );
  App::log(tr("NE to SE %1 degrees\n").arg(Angle::radiansToDegrees(realNE.azimuthTo(realSE))) );
  // Avoid instabilities between 0 and 360, set angle between -180 to 180 instead of 0 to 360 for angle along X
  double angle, averageAngle=0.0;
  angle=realNW.azimuthTo(realNE);
  if(angle>M_PI) {
    angle-=2*M_PI;
  }
  averageAngle+=angle;
  angle=realSW.azimuthTo(realSE);
  if(angle>M_PI) {
    angle-=2*M_PI;
  }
  averageAngle+=angle;
  averageAngle+=realNW.azimuthTo(realSW)-1.5*M_PI;
  averageAngle+=realNE.azimuthTo(realSE)-1.5*M_PI;
  averageAngle*=0.25;
  App::log(tr("Local rotation %1 degrees (mathematical sense)\n").arg(averageAngle*180/M_PI) );
  // Transform for image, pure rotation
  QTransform rot;
  rot.rotateRadians(-averageAngle);
  QImage im=m.image().transformed(rot, Qt::SmoothTransformation);
  // Transform for points
  // Additionnal translations to rotate around image center and because image size increased while rotating
  rot.reset();
  rot.translate((im.width()-m.image().width())/2, (im.height()-m.image().height())/2);
  rot.translate(m.image().width()/2, m.image().height()/2);
  rot.rotateRadians(-averageAngle);
  rot.translate(-m.image().width()/2, -m.image().height()/2);
  QPointF pixNW(IMAGE_SIZE/2, IMAGE_SIZE/2);
  QPointF pixNE(m.image().width()-IMAGE_SIZE/2, IMAGE_SIZE/2);
  QPointF pixSW(IMAGE_SIZE/2, m.image().height()-(IMAGE_SIZE/2-IMAGE_FOOTER));
  QPointF pixSE(m.image().width()-IMAGE_SIZE/2, m.image().height()-(IMAGE_SIZE/2-IMAGE_FOOTER));
  App::log(tr("North west reference %1 pixels\n").arg(Point2D(pixNW).toString(2, 'f')) );
  App::log(tr("North east reference %1 pixels\n").arg(Point2D(pixNE).toString(2, 'f')) );
  App::log(tr("South west reference %1 pixels\n").arg(Point2D(pixSW).toString(2, 'f')) );
  App::log(tr("South east reference %1 pixels\n").arg(Point2D(pixSE).toString(2, 'f')) );
  pixNW=rot.map(pixNW);
  pixNE=rot.map(pixNE);
  pixSW=rot.map(pixSW);
  pixSE=rot.map(pixSE);
  App::log(tr("North west reference %1 pixels\n").arg(Point2D(pixNW).toString(2, 'f')) );
  App::log(tr("North east reference %1 pixels\n").arg(Point2D(pixNE).toString(2, 'f')) );
  App::log(tr("South west reference %1 pixels\n").arg(Point2D(pixSW).toString(2, 'f')) );
  App::log(tr("South east reference %1 pixels\n").arg(Point2D(pixSE).toString(2, 'f')) );

  QList<ImageLayer::ReferencePoint> refList;
  ImageLayer::ReferencePoint ref;
  ref.setImage(pixNW.toPoint());
  ref.setReal(realNW);
  ref.setName("NW");
  refList.append(ref);
  ref.setImage(pixNE.toPoint());
  ref.setReal(realNE);
  ref.setName("NE");
  refList.append(ref);
  ref.setImage(pixSW.toPoint());
  ref.setReal(realSW);
  ref.setName("SW");
  refList.append(ref);
  ref.setImage(pixSE.toPoint());
  ref.setReal(realSE);
  ref.setName("SE");
  refList.append(ref);
  Point2D origin, scale;
  ImageLayer::scaling(refList, origin, scale);
  App::log(tr("Origin: %1,%2 ; Scale %3,%4\n")
                      .arg(origin.x(), 0, 'f')
                      .arg(origin.y(), 0, 'f')
                      .arg(scale.x(), 0, 'f')
                      .arg(scale.y(), 0, 'f'));

  // Save global image
  QString outputImageFile=_outputBaseName+"-utm.png";
  im.save(outputImageFile, "PNG");

  // Build layer
  AxisWindow * w=new AxisWindow;
  ImageLayer * l=new ImageLayer(w);
  QFileInfo fi(outputImageFile);
  l->loadImage(fi.absoluteFilePath());
  // Compute origin in UTM coordinates
  l->setOrigin(origin);
  l->setScale(scale);
  // Save as .layer
  XMLSciFigs s;
  s.saveFile(_outputBaseName+".layer", w->graphContent(), XMLSciFigs::Layer);
  delete w;

  App::log(tr("File '%1' saved").arg(_outputBaseName+".layer\n") );
  ImageMerger::printAverageSteps();
  CoreApplication::quit();
}

/*!
  Return a rectangle that enclose the circle centered at \a loc and of \a radius.
  \a loc and returned rectangle are in geographical coordinates (long,lat). \a radius
  is in meters.
*/
Rect ImageSource::rect(const Point2D& loc, double radius)
{
  TRACE;
  Point2D size(radius, radius);
  Point2D topLeft=loc;
  UtmZone utmZone(loc);
  topLeft.geographicalToUtm(utmZone);
  Point2D bottomRight=topLeft;
  topLeft-=size;
  bottomRight+=size;
  topLeft.utmToGeographical(utmZone);
  bottomRight.utmToGeographical(utmZone);
  // round numbers to reduce download access
  Point2D delta=bottomRight-topLeft;
  double mdelta=delta.x()<delta.y() ? delta.x() : delta.y();
  topLeft.round(mdelta);
  bottomRight.round(mdelta);
  return Rect(topLeft, bottomRight);
}

QString ImageSource::imageFileName(const Point2D& c)
{
  TRACE;
  return QString("map_%1_%2_%3.png").arg(_zoom).arg(c.x(), 10, 'f', 5, '_').arg(c.y(), 10, 'f', 5, '_');
}

void ImageSource::setStep()
{
  TRACE;
  App::log(tr("Network access ready\n") );
  App::log(tr("Computing step...\n") );
  switch(_zoom) {
  case 14:
    _step=Point2D(0.032, 0.024);  // OK
    break;
  case 15:
    _step=Point2D(0.016, 0.012);  // Not tested
    break;
  case 16:
    _step=Point2D(0.008, 0.006);  // OK
    break;
  case 17:
    _step=Point2D(0.004, 0.003);  // Not tested
    break;
  case 18:
    _step=Point2D(0.002, 0.0015); // OK
    break;
  case 19:
    _step=Point2D(0.001, 0.0007);  // Not tested
    break;
  case 20:
    _step=Point2D(0.0005, 0.0003); // Getting strange signals
    break;
  case 21:
    _step=Point2D(0.00025, 0.00015);  // Not tested
    break;
  }

  start();
  //_mode=SetStepMaster;
  //request(_rect.topLeft());
}

void ImageSource::setStepMaster(const QImage& im)
{
  TRACE;
  App::log(tr("Received image centered at %1, %2\n")
                   .arg(_rect.topLeft().x(), 9, 'f', 5, ' ')
                   .arg(_rect.topLeft().y(), 9, 'f', 5, ' '));
  im.save(imageFileName(_rect.topLeft()), "PNG");
  _mode=SetStepSlaveX;
  request(_rect.topLeft()+Point2D(0.001, 0.0));
}

void ImageSource::setStepSlaveX(const QImage& im)
{
  TRACE;
  App::log(tr("Received image centered at %1, %2\n")
                   .arg(_rect.topLeft().x()+_pixelShift, 10, 'f', 5, ' ')
                   .arg(_rect.topLeft().y(), 10, 'f', 5, ' '));
  im.save(imageFileName(_rect.topLeft()+Point2D(_pixelShift, 0.0)), "PNG");
  _mode=SetStepSlaveY;
  request(_rect.topLeft()+Point2D(0.0, 0.001));
}

void ImageSource::setStepSlaveY(const QImage& im)
{
  TRACE;
  App::log(tr("Received image centered at %1, %2\n")
                   .arg(_rect.topLeft().x(), 10, 'f', 5, ' ')
                   .arg(_rect.topLeft().y()+_pixelShift, 10, 'f', 5, ' '));
  im.save(imageFileName(_rect.topLeft()+Point2D(0.0, _pixelShift)), "PNG");
  computeStep();
  start();
}

void ImageSource::computeStep()
{
  TRACE;
  QImage master, slaveX, slaveY;
  master.load(imageFileName(_rect.topLeft()));
  slaveX.load(imageFileName(_rect.topLeft()+Point2D(_pixelShift, 0.0)));
  slaveY.load(imageFileName(_rect.topLeft()+Point2D(0.0, _pixelShift)));
  bool ok;
  int sx=abs(RowImageMerger::getPixelShift(master, slaveX, 0, IMAGE_SIZE, ok));
  if(!ok) {
    App::log(tr("Error adjusting X step\n"));
    ::exit(2);
  }
  int sy=abs(ColumnImageMerger::getPixelShift(slaveY, master, 0, IMAGE_SIZE-IMAGE_FOOTER, ok));
  if(!ok) {
    App::log(tr("Error adjusting Y step\n"));
    ::exit(2);
  }
  App::log(tr("Resolution=[%1 %2] degree/pixel\n").arg(_pixelShift/sx).arg(_pixelShift/sy) );
  _step=Point2D((IMAGE_SIZE-IMAGE_OVERLAP)*_pixelShift/sx, (IMAGE_SIZE-IMAGE_FOOTER-IMAGE_OVERLAP)*_pixelShift/sy);
  App::log(tr("Step=%1\n").arg(_step.toString()) );
  _step.setX(Number::round(_step.x(), _step.x()));
  _step.setY(Number::round(_step.y(), _step.y()));
  App::log(tr("Rounded step=%1\n").arg(_step.toString()) );
}
