/** -*- mode: c++ ; c-basic-offset: 2 -*-
 *
 *  @file Updater.cpp
 *
 *  Copyright 2017 Sebastien Fourey
 *
 *  This file is part of G'MIC-Qt, a generic plug-in for raster graphics
 *  editors, offering hundreds of filters thanks to the underlying G'MIC
 *  image processing framework.
 *
 *  gmic_qt 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.
 *
 *  gmic_qt 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 gmic_qt.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
#include "Updater.h"
#include <QDebug>
#include <QNetworkRequest>
#include <QTextStream>
#include <QUrl>
#include <iostream>
#include "Common.h"
#include "GmicStdlib.h"
#include "Logger.h"
#include "Misc.h"
#include "Utils.h"
#ifndef gmic_core
#include "CImg.h"
#endif
#include "gmic.h"

namespace GmicQt
{
std::unique_ptr<Updater> Updater::_instance = std::unique_ptr<Updater>(nullptr);
OutputMessageMode Updater::_outputMessageMode = OutputMessageMode::Quiet;

Updater::Updater(QObject * parent) : QObject(parent)
{
  _networkAccessManager = nullptr;
  _someNetworkUpdatesAchieved = false;
}

Updater * Updater::getInstance()
{
  if (!_instance) {
    _instance = std::unique_ptr<Updater>(new Updater(nullptr));
  }
  return _instance.get();
}

Updater::~Updater() = default;

void Updater::updateSources(bool useNetwork)
{
  _sources.clear();
  _sourceIsStdLib.clear();
  // Build sources map
  QString prefix = commandFromOutputMessageMode(_outputMessageMode);
  if (!prefix.isEmpty()) {
    prefix.push_back(QChar(' '));
  }
  cimg_library::CImgList<gmic_pixel_type> gptSources;
  cimg_library::CImgList<char> names;
  QString command = QString("%1gui_filter_sources %2").arg(prefix).arg(useNetwork);
  try {
    gmic(command.toLocal8Bit().constData(), gptSources, names, nullptr, true);
  } catch (...) {
    Logger::error(QString("Command '%1' failed.").arg(command));
    gptSources.assign();
  }
  cimg_library::CImgList<char> sources;
  gptSources.move_to(sources);
  cimglist_for(sources, l)
  {
    cimg_library::CImg<char> & str = sources[l];
    str.unroll('x');
    bool isStdlib = (str.back() == 1);
    if (isStdlib) {
      str.back() = 0;
    } else {
      str.columns(0, str.width());
    }
    QString source = QString::fromUtf8(str);
    _sources << source;
    _sourceIsStdLib[source] = isStdlib;
  }

  // NOTE : For testing purpose
  //  _sources.clear();
  //  _sourceIsStdLib.clear();
  //  //  _sources.push_back("http://localhost:2222/update300.gmic");
  //  //  _sourceIsStdLib["http://localhost:2222/update300.gmic"] = true;
  //  _sources.push_back("https://gmic.eu/update271.gmic");
  //  _sourceIsStdLib["https://gmic.eu/update271.gmic"] = true;
}

void Updater::startUpdate(int ageLimit, int timeout, bool useNetwork)
{
  TIMING;
  updateSources(useNetwork);
  _errorMessages.clear();
  _networkAccessManager = new QNetworkAccessManager(this);
  connect(_networkAccessManager, &QNetworkAccessManager::finished, this, &Updater::onNetworkReplyFinished);
  _someNetworkUpdatesAchieved = false;
  if (useNetwork) {
    QDateTime limit = QDateTime::currentDateTime().addSecs(-3600 * (qint64)ageLimit);
    for (const QString & str : _sources) {
      if (str.startsWith("http://") || str.startsWith("https://")) {
        QString filename = localFilename(str);
        QFileInfo info(filename);
        if (!info.exists() || (info.lastModified() < limit)) {
          TRACE << "Downloading" << str << "to" << filename;
          QUrl url(str);
          QNetworkRequest request(url);
          request.setHeader(QNetworkRequest::UserAgentHeader, pluginFullName());
          // PRIVACY NOTICE (to be displayed in one of the "About" filters of the plugin
          //
          // PRIVACY NOTICE
          // This plugin may download up-to-date filter definitions from the gmic.eu server.
          // It is the case when first launched after a fresh installation, and periodically
          // with a frequency which can be set in the settings dialog.
          // The user should be aware that the following information may be retrieved
          // from the server logs: IP address of the client; date and time of the request;
          // as well as a short string, supplied through the HTTP protocol "User Agent" header
          // field, which describes the full plugin version as shown in the window title
          // (e.g. "G'MIC-Qt for GIMP 2.8 - Linux 64 bits - 2.2.1_pre#180301").
          //
          // Note that this information may solely be used for purely anonymous
          // statistical purposes.
          _pendingReplies.insert(_networkAccessManager->get(request));
        }
      }
    }
  }
  if (_pendingReplies.isEmpty()) {
    QTimer::singleShot(0, this, &Updater::onUpdateNotNecessary); // While GUI is Idle
    _networkAccessManager->deleteLater();
  } else {
    QTimer::singleShot(timeout * 1000, this, &Updater::cancelAllPendingDownloads);
  }
  TIMING;
}

QList<QString> Updater::remotesThatNeedUpdate(int ageLimit) const
{
  QDateTime limit = QDateTime::currentDateTime().addSecs(-3600 * ageLimit);
  QList<QString> list;
  for (const QString & str : _sources) {
    if (str.startsWith("http://") || str.startsWith("https://")) {
      QString filename = localFilename(str);
      QFileInfo info(filename);
      if (!info.exists() || info.lastModified() < limit) {
        list << str;
      }
    }
  }
  return list;
}

bool Updater::someUpdatesNeeded(int ageLimit) const
{
  QDateTime limit = QDateTime::currentDateTime().addSecs(-3600 * ageLimit);
  for (const QString & str : _sources) {
    if (str.startsWith("http://") || str.startsWith("https://")) {
      QString filename = localFilename(str);
      QFileInfo info(filename);
      if (!info.exists() || info.lastModified() < limit) {
        return true;
      }
    }
  }
  return false;
}

QList<QString> Updater::errorMessages()
{
  return _errorMessages;
}

bool Updater::allDownloadsOk() const
{
  return _errorMessages.isEmpty();
}

void Updater::processReply(QNetworkReply * reply)
{
  QString url = reply->request().url().toString();
  if (!reply->bytesAvailable()) {
    return;
  }
  QByteArray array = reply->readAll();
  if (array.isNull()) {
    _errorMessages << QString(tr("Error downloading %1 (empty file?)")).arg(url);
    return;
  }
  if (!array.startsWith("#@gmic")) {
    TRACE << QString("Decompressing reply from") << url;
    QByteArray tmp = cimgzDecompress(array);
    array = tmp;
  }
  if (array.isNull() || !array.startsWith("#@gmic")) {
    _errorMessages << QString(tr("Could not read/decompress %1")).arg(url);
    return;
  }
  QString filename = localFilename(url);
  if (!safelyWrite(array, filename)) {
    _errorMessages << QString(tr("Error writing file %1")).arg(filename);
    return;
  }
  _someNetworkUpdatesAchieved = true;
}

void Updater::onNetworkReplyFinished(QNetworkReply * reply)
{
  TIMING;
  QNetworkReply::NetworkError error = reply->error();
  if (error == QNetworkReply::NoError) {
    processReply(reply);
  } else {
    QString str;
    QDebug d(&str);
    d << error;
    str = str.trimmed();
    _errorMessages << QString(tr("Error downloading %1<br/>Error %2: %3")).arg(reply->request().url().toString()).arg(static_cast<int>(error)).arg(str);
    Logger::error("Update failed");
    Logger::note(QString("Error string: %1").arg(reply->errorString()));
    Logger::note("******* Full reply contents ******\n");
    Logger::note(reply->readAll());
    Logger::note(QString("******** HTTP Status: %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()));
    // We either create an empty local file or 'touch' the existing one to prevent a systematic update on next startups
    // Instead, usual delay will occur before next try
    touchFile(localFilename(reply->url().toString()));
  }
  _pendingReplies.remove(reply);
  if (_pendingReplies.isEmpty()) {
    if (_errorMessages.isEmpty()) {
      emit updateIsDone((int)UpdateStatus::Successful);
    } else {
      emit updateIsDone((int)UpdateStatus::SomeFailed);
    }
    _networkAccessManager->deleteLater();
    _networkAccessManager = nullptr;
  }
  reply->deleteLater();
}

void Updater::notifyAllDownloadsOK()
{
  _errorMessages.clear();
  emit updateIsDone((int)UpdateStatus::Successful);
}

void Updater::cancelAllPendingDownloads()
{
  TIMING;
  // Make a copy because aborting will call onNetworkReplyFinished, and
  // thus modify the _pendingReplies set.
  QSet<QNetworkReply *> replies = _pendingReplies;
  for (QNetworkReply * reply : replies) {
    _errorMessages << QString(tr("Download timeout: %1")).arg(reply->request().url().toString());
    reply->abort();
  }
}

void Updater::onUpdateNotNecessary()
{
  emit updateIsDone((int)UpdateStatus::NotNecessary);
}

QByteArray Updater::cimgzDecompress(const QByteArray & array)
{
  QTemporaryFile tmpZ(QDir::tempPath() + QDir::separator() + "gmic_qt_update_XXXXXX_cimgz");
  if (!tmpZ.open()) {
    Logger::warning("Updater::cimgzDecompress(): Error creating " + tmpZ.fileName());
    return QByteArray();
  }
  if (!writeAll(array, tmpZ)) {
    Logger::warning("Updater::cimgzDecompress(): Error writing temporary " + tmpZ.fileName());
    return QByteArray();
  }
  tmpZ.close();
  cimg_library::CImg<unsigned char> buffer;
  try {
    buffer.load_cimg(tmpZ.fileName().toUtf8().constData());
  } catch (...) {
    Logger::warning("Updater::cimgzDecompress(): CImg<>::load_cimg error for file " + tmpZ.fileName());
    return QByteArray();
  }
  return QByteArray((char *)buffer.data(), (int)buffer.size());
}

QByteArray Updater::cimgzDecompressFile(const QString & filename)
{
  cimg_library::CImg<unsigned char> buffer;
  try {
    buffer.load_cimg(filename.toLocal8Bit().constData());
  } catch (...) {
    Logger::warning("Updater::cimgzDecompressFile(): CImg<>::load_cimg error for file " + filename);
    return QByteArray();
  }
  return QByteArray((char *)buffer.data(), (int)buffer.size());
}

QString Updater::localFilename(QString url)
{
  if (url.startsWith("http://") || url.startsWith("https://")) {
    QUrl u(url);
    return QString("%1%2").arg(gmicConfigPath(true)).arg(u.fileName());
  }
  return url;
}

bool Updater::isStdlib(const QString & source) const
{
  QMap<QString, bool>::const_iterator it = _sourceIsStdLib.find(source);
  if (it != _sourceIsStdLib.end()) {
    return it.value();
  }
  return false;
}

QList<QString> Updater::sources() const
{
  return _sources;
}

QByteArray Updater::buildFullStdlib() const
{
  QByteArray result;
  if (_sources.isEmpty()) {
    gmic_image<char> stdlib_h = gmic::decompress_stdlib();
    QByteArray tmp = QByteArray::fromRawData(stdlib_h, (int)stdlib_h.size());
    tmp[tmp.size() - 1] = '\n';
    result.append(tmp);
    return result;
  }
  for (const QString & source : _sources) {
    QString filename = localFilename(source);
    QFile file(filename);
    if (file.open(QFile::ReadOnly)) {
      QByteArray array;
      if (isStdlib(source) && !file.peek(10).startsWith("#@gmic")) {
        // Try to uncompress
        file.close();
        TRACE << "Appending compressed file:" << filename;
        array = cimgzDecompressFile(filename);
        if (array.size() && !array.startsWith("#@gmic")) {
          array.clear();
        }
        if (!array.size()) {
          gmic_image<char> stdlib_h = gmic::decompress_stdlib();
          QByteArray tmp = QByteArray::fromRawData(stdlib_h, (int)stdlib_h.size());
          tmp[tmp.size() - 1] = '\n';
          array.append(tmp);
        }
      } else {
        TRACE << "Appending:" << filename;
        array = file.readAll();
      }
      result.append(array);
      result.append('\n');
    } else if (isStdlib(source)) {
      gmic_image<char> stdlib_h = gmic::decompress_stdlib();
      QByteArray tmp = QByteArray::fromRawData(stdlib_h, (int)stdlib_h.size());
      tmp[tmp.size() - 1] = '\n';
      result.append(tmp);
    }
    const QString toTopLevel = QString("#@gui ") + QString("_").repeated(80) + QString("\n");
    result.append(toTopLevel.toUtf8());
  }
  return result;
}

bool Updater::someNetworkUpdateAchieved() const
{
  return _someNetworkUpdatesAchieved;
}

void Updater::setOutputMessageMode(OutputMessageMode mode)
{
  _outputMessageMode = mode;
}

} // namespace GmicQt
