//=========================================================================
//  Copyright (c) Kitware, Inc.
//  All rights reserved.
//  See LICENSE.txt for details.
//
//  This software is distributed WITHOUT ANY WARRANTY; without even
//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
//  PURPOSE.  See the above copyright notice for more information.
//=========================================================================

#include "smtk/newt/qtDownloadFolderRequester.h"

#include "smtk/newt/qtNewtInterface.h"

#include <QByteArray>
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QNetworkReply>
#include <QRegularExpression>
#include <QSet>

namespace
{
// For tracking instance state
enum State
{
  Idle = 0,
  Running,
  Error,
  Canceled
};

} // namespace

namespace newt
{

// --------------------------------------------------------------------
class qtDownloadFolderRequester::Internal
{
public:
  qtNewtInterface* m_newt = nullptr;
  int m_downloadFileCount = 0;
  QSet<qtDownloadFolderRequester*> m_requesterSet;

  QString m_localFolder;
  QString m_machine;
  QString m_remoteFolder;
  bool m_recursive;
  QString m_filePattern; // for file filtering with QRegularExpression

  State m_state = State::Idle;
};

// --------------------------------------------------------------------
qtDownloadFolderRequester::qtDownloadFolderRequester(QObject* parent)
  : QObject(parent)
{
  m_internal = new Internal();
}

qtDownloadFolderRequester::~qtDownloadFolderRequester()
{
  delete m_internal;
}

void qtDownloadFolderRequester::startDownload(
  const QString& localFolder,
  const QString& machine,
  const QString& remoteFolder,
  bool recursive,
  const QString& fileFilter)
{
  if (m_internal->m_state == State::Running)
  {
    emit this->errorMessage("Cannot start folder download while already busy downloading.");
    return;
  }

  QString filePattern;
  if (!fileFilter.isEmpty())
  {
    filePattern = QRegularExpression::wildcardToRegularExpression(fileFilter);
    // qDebug() << __FILE__ << __LINE__ << recursive << filePattern;
  }

  // Call internal method
  this->startInternal(localFolder, machine, remoteFolder, recursive, filePattern);
}

bool qtDownloadFolderRequester::isBusy() const
{
  return m_internal->m_state != State::Running;
}

void qtDownloadFolderRequester::startInternal(
  const QString& localFolder,
  const QString& machine,
  const QString& remoteFolder,
  bool recursive,
  const QString& filePattern)
{
  // Make sure user is logged in
  m_internal->m_newt = qtNewtInterface::instance();
  if (!m_internal->m_newt->isLoggedIn())
  {
    emit this->errorMessage("Cannot download folder(s) because user not logged into NERSC.");
    return;
  }

  // Create localFolder if needed
  QDir qdir;
  if (!qdir.mkpath(localFolder))
  {
    m_internal->m_state = State::Error;
    QString msg = QString("Failed to create download directory: %1").arg(localFolder);
    emit this->errorMessage(msg);
    return;
  }

  m_internal->m_state = State::Running;

  // Save parameters in case we need to traverse subfolders
  m_internal->m_localFolder = localFolder;
  m_internal->m_machine = machine;
  m_internal->m_remoteFolder = remoteFolder;
  m_internal->m_recursive = recursive;
  m_internal->m_filePattern = filePattern;

  m_internal->m_downloadFileCount = 0;
  m_internal->m_requesterSet.clear();

  QString msg = QString("Checking folder %1").arg(remoteFolder);
  emit this->progressMessage(msg);

  QNetworkReply* reply = m_internal->m_newt->requestDirectoryList(machine, remoteFolder);
  QObject::connect(
    reply, &QNetworkReply::finished, this, &qtDownloadFolderRequester::onListFolderReply);
}

void qtDownloadFolderRequester::onListFolderReply()
{
  QNetworkReply* reply = qobject_cast<QNetworkReply*>(this->sender());

  // Skip if we are in error state
  if (m_internal->m_state == State::Error)
  {
    reply->deleteLater();
    return;
  }

  // Check for errors in this reply
  if (reply->error())
  {
    QByteArray bytes = reply->readAll();
    QString msg(bytes);
    emit this->errorMessage(msg);
    reply->deleteLater();
    m_internal->m_state = State::Error;
    return;
  }

  // Initialize regular expression for file filtering
  QRegularExpression regex;
  if (!m_internal->m_filePattern.isEmpty())
  {
    regex.setPattern(m_internal->m_filePattern);
  }

  // Iterate over folder contents
  QByteArray bytes = reply->readAll();
  QJsonDocument jsonDocument = QJsonDocument::fromJson(bytes.constData());
  const QJsonArray& jsonArray = jsonDocument.array();
  for (const QJsonValue& jsonValue : jsonArray)
  {
    QJsonObject jsonObject = jsonValue.toObject();
    QString name = jsonObject.value("name").toString();
    // Skip hidden files plus directory references (. and ..)
    if (name[0] == ".")
    {
      continue;
    }
    QString perms = jsonObject.value("perms").toString();
    if ((perms[0] == 'd') && m_internal->m_recursive)
    {
      // Request directory
      QString localPath = QString("%1/%2").arg(m_internal->m_localFolder, name);
      QString remotePath = QString("%1/%2").arg(m_internal->m_remoteFolder, name);
      qtDownloadFolderRequester* subRequester = new qtDownloadFolderRequester;
      m_internal->m_requesterSet.insert(subRequester);
      QObject::connect(
        subRequester,
        &qtDownloadFolderRequester::progressMessage,
        this,
        &qtDownloadFolderRequester::progressMessage);
      QObject::connect(
        subRequester,
        &qtDownloadFolderRequester::errorMessage,
        this,
        &qtDownloadFolderRequester::errorMessage);
      QObject::connect(
        subRequester, &qtDownloadFolderRequester::downloadComplete, [this, subRequester]() {
          m_internal->m_requesterSet.remove(subRequester);
          delete subRequester;
          this->checkIfComplete();
        });
      subRequester->startInternal(
        localPath,
        m_internal->m_machine,
        remotePath,
        m_internal->m_recursive,
        m_internal->m_filePattern);
    }
    else if (perms[0] == '-')
    {
      // Check file pattern
      if (m_internal->m_filePattern.isEmpty() || regex.match(name).hasMatch())
      {
        QString localPath = QString("%1/%2").arg(m_internal->m_localFolder, name);
        QString remotePath = QString("%1/%2").arg(m_internal->m_remoteFolder, name);
        this->downloadFile(localPath, remotePath);
      }
    }
    else if (perms[0] == 'l')
    {
      QString msg = QString("Skipping symbolic link download: %1").arg(name);
      // emit this->progressMessage(msg);
      qDebug() << msg;
    }
  } // for (jsonValue)

  reply->deleteLater();
  this->checkIfComplete(); // in case there were no files/folders to download
}

void qtDownloadFolderRequester::downloadFile(const QString& localPath, const QString& remotePath)
{
  QString msg = QString("Requesting download %1").arg(remotePath);
  emit this->progressMessage(msg);

  // Copy the input string references
  QString localFilePath = QString(localPath);
  QString remoteFilePath = QString(remotePath);

  // Request the download
  ++m_internal->m_downloadFileCount;
  QNetworkReply* reply = m_internal->m_newt->requestFileDownload(m_internal->m_machine, remotePath);
  QObject::connect(reply, &QNetworkReply::finished, [this, reply, localFilePath, remoteFilePath]() {
    QString msg;
    if (reply->error() != QNetworkReply::NoError)
    {
      msg = QString("ERROR Downloading file %1: %2").arg(remoteFilePath).arg(reply->error());
      emit this->errorMessage(msg);
      reply->deleteLater();
      return;
    }

    QFile file(localFilePath);
    if (!file.open(QIODevice::WriteOnly)) // truncates any existing file
    {
      msg = QString("Unable to open local file for writing at %1").arg(localFilePath);
      emit this->errorMessage(msg);
      reply->deleteLater();
      return;
    }
    file.write(reply->readAll());

    reply->deleteLater();
    --m_internal->m_downloadFileCount;
    this->checkIfComplete();
  });
}

void qtDownloadFolderRequester::checkIfComplete()
{
  bool complete = (m_internal->m_downloadFileCount == 0) && m_internal->m_requesterSet.empty();
  if (complete)
  {
    m_internal->m_state = State::Idle;
    emit this->downloadComplete();
  }
}

} // namespace newt
