//=========================================================================
//  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 "qtNewtFileBrowserWidget.h"
#include "ui_qtNewtFileBrowserWidget.h"

#include "smtk/newt/qtNewtInterface.h"

#include <QAction>
#include <QByteArray>
#include <QClipboard>
#include <QContextMenuEvent>
#include <QCursor>
#include <QDebug>
#include <QDialog>
#include <QGuiApplication>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QMenu>
#include <QMessageBox>
#include <QNetworkReply>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QStandardItem>
#include <QStandardPaths>
#include <QVBoxLayout>
#include <Qt>
#include <QtGlobal>

#include <algorithm> // sort

namespace
{
// For now, hard code machine to "cori"
const QString MACHINE("cori");

const int NUM_COLUMNS = 4;
const int NAME_COLUMN = 0;
const int SIZE_COLUMN = 1;
const int TYPE_COLUMN = 2;
const int DATE_COLUMN = 3;

// Internal classes used in parsing ls results
class FileSystemItem
{
public:
  enum Type
  {
    FILE = 1,
    FOLDER,
    LINK,
    UNKNOWN,
  };

  QString m_name;
  QString m_size;
  FileSystemItem::Type m_type;
  QString m_date;

  FileSystemItem()
    : m_type(FileSystemItem::UNKNOWN)
  {
  }

  bool isHidden() const { return m_name[0] == '.'; }

  QString typeString() const
  {
    switch (m_type)
    {
      case FileSystemItem::FILE:
        return "File";

      case FileSystemItem::FOLDER:
        return "Folder";

      case FileSystemItem::LINK:
        return "Link";

      case FileSystemItem::UNKNOWN:
        return "?";
    }

    return "?";
  }

  static bool lessThan(const FileSystemItem& left, const FileSystemItem& right)
  {
    QString sLeft = left.m_type == FileSystemItem::FOLDER ? "0." : "1.";
    sLeft.append(left.m_name);

    QString sRight = right.m_type == FileSystemItem::FOLDER ? "0." : "1.";
    sRight.append(right.m_name);

    return sLeft < sRight;
  }
};

} // namespace

namespace newt
{

qtNewtFileBrowserWidget::qtNewtFileBrowserWidget(QWidget* parentWidget)
  : QWidget(parentWidget)
  , ui(new Ui::qtNewtFileBrowserWidget)
  , m_fileIcon(":/icons/newt/file.png")
  , m_folderIcon(":/icons/newt/folder.png")
  , m_model(0, NUM_COLUMNS, this)
  , m_newt(qtNewtInterface::instance())
{
  this->ui->setupUi(this);
  this->ui->m_statusLabel->setText("Click Home or Scratch button");
  this->ui->m_userLabel->setText("?");
  this->ui->m_tableView->setModel(&m_model);
  this->ui->m_progressBar->setValue(0);
  this->ui->m_progressBar->hide();

  if (m_newt->isLoggedIn())
  {
    this->ui->m_userLabel->setText(m_newt->userName());
  }
  // Connect to signals from newt interface
  QObject::connect(
    m_newt, &qtNewtInterface::loginComplete, this, &qtNewtFileBrowserWidget::onLogin);

  // Connect to signals from the UI
  QObject::connect(this->ui->m_copyButton, &QPushButton::clicked, [this]() {
    QString path = this->ui->m_folderLine->text();
    auto clipboard = QGuiApplication::clipboard();
    clipboard->setText(path);
    emit this->pathCopied(path);
  });
  QObject::connect(
    this->ui->m_homeButton, &QPushButton::clicked, this, &qtNewtFileBrowserWidget::onHomeClicked);
  QObject::connect(
    this->ui->m_scratchButton,
    &QPushButton::clicked,
    this,
    &qtNewtFileBrowserWidget::onScratchClicked);
  QObject::connect(
    this->ui->m_tableView, &QTableView::activated, this, &qtNewtFileBrowserWidget::onItemActivated);
}

qtNewtFileBrowserWidget::~qtNewtFileBrowserWidget()
{
  if (!m_files.empty())
  {
    qWarning() << __FILE__ << __LINE__ << "m_files.count()" << m_files.count();
  }
}

void qtNewtFileBrowserWidget::onItemActivated(const QModelIndex& index)
{
  // Only respond if name clicked
  int col = index.column();
  if (col != NAME_COLUMN)
  {
    return;
  }

  int row = index.row();
  QStandardItem* nameItem = m_model.item(row, NAME_COLUMN);
  QString name = nameItem->text();
  if (name == "..")
  {
    this->goUpDirectory();
    return;
  }

  int intValue = nameItem->data().toInt();
  FileSystemItem::Type fsType = static_cast<FileSystemItem::Type>(intValue);
  QString sep = m_currentPath.back() == QChar('/') ? "" : "/";
  if (fsType == FileSystemItem::FOLDER)
  {
    QString nextPath = QString("%1%2%3").arg(m_currentPath, sep, name);
    this->gotoPath(nextPath);
  }
}

void qtNewtFileBrowserWidget::onListFolderReply()
{
  this->setCursor(Qt::ArrowCursor);
  QNetworkReply* reply = qobject_cast<QNetworkReply*>(this->sender());
  if (reply->error())
  {
    this->handleNetworkError(reply);
  }

  QByteArray bytes = reply->readAll();
  QJsonDocument jsonDocument = QJsonDocument::fromJson(bytes.constData());
  const QJsonArray& jsonArray = jsonDocument.array();

  QList<FileSystemItem> fsList;
  for (const QJsonValue& jsonValue : jsonArray)
  {
    FileSystemItem fsItem;

    QJsonObject jsonObject = jsonValue.toObject();
    fsItem.m_name = jsonObject.value("name").toString();
    fsItem.m_size = jsonObject.value("size").toString();
    fsItem.m_date = jsonObject.value("date").toString();

    QString perms = jsonObject.value("perms").toString();
    if (perms[0] == 'd')
    {
      fsItem.m_type = FileSystemItem::FOLDER;
    }
    else if (perms[0] == 'l')
    {
      fsItem.m_type = FileSystemItem::LINK;
    }
    else
    {
      fsItem.m_type = FileSystemItem::FILE;
    }

    if (!fsItem.isHidden())
    {
      fsList.push_back(fsItem);
    }
  }
  std::sort(fsList.begin(), fsList.end(), FileSystemItem::lessThan);

  // Rebuild model
  m_model.clear();

  QStringList labels = { "Name", "Size", "Type", "Date Modified" };
  m_model.setHorizontalHeaderLabels(labels);

  // Generate QStandardItem instances from FileSystemItem list
  m_model.setRowCount(fsList.size());
  for (int i = 0; i < fsList.size(); ++i)
  {
    FileSystemItem fsItem = fsList[i];

    QStandardItem* nameItem = new QStandardItem(fsItem.m_name);
    if (fsItem.m_type == FileSystemItem::FILE)
    {
      nameItem->setIcon(m_fileIcon);
    }
    else if (fsItem.m_type == FileSystemItem::FOLDER)
    {
      nameItem->setIcon(m_folderIcon);
    }
    m_model.setItem(i, NAME_COLUMN, nameItem);

    m_model.setItem(i, SIZE_COLUMN, new QStandardItem(fsItem.m_size));
    m_model.setItem(i, TYPE_COLUMN, new QStandardItem(fsItem.typeString()));
    m_model.setItem(i, DATE_COLUMN, new QStandardItem(fsItem.m_date));

    // Set flags
    for (int col = 0; col < NUM_COLUMNS; ++col)
    {
      m_model.item(i, col)->setFlags(Qt::ItemIsEnabled);
    }

    // Attach type as data
    m_model.item(i, 0)->setData(fsItem.m_type);
  } // for (i)

  // If not at root path, insert ".." item
  if ((m_requestPath != m_homePath) && (m_requestPath != m_scratchPath))
  {
    m_model.insertRow(0, new QStandardItem(".."));
    m_model.item(0, 0)->setFlags(Qt::ItemIsEnabled);
  }

  this->ui->m_tableView->resizeColumnsToContents();
  this->ui->m_tableView->horizontalHeader()->setVisible(true);
  this->ui->m_tableView->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);

  m_currentPath = m_requestPath;
  this->ui->m_folderLine->setText(m_currentPath);

  QString status = QString("%1 items").arg(m_model.rowCount());
  this->ui->m_statusLabel->setText(status);
  m_requestPath.clear();

  reply->deleteLater();
  emit this->endGotoPath(m_currentPath);
}

void qtNewtFileBrowserWidget::onLogin(const QString& username)
{
  this->ui->m_userLabel->setText(username);
}

void qtNewtFileBrowserWidget::onNewtError(QString text, QNetworkReply* reply)
{
  this->setCursor(Qt::ArrowCursor);
  qWarning() << text;

  QMessageBox msgBox(this->parentWidget());
  msgBox.setText("Network Error");
  msgBox.setDetailedText(text);
  msgBox.setStandardButtons(QMessageBox::Close);
  msgBox.setStyleSheet("QTextEdit{min-width:600 px; min-height: 400px;}");
  msgBox.exec();

  reply->deleteLater();
}

void qtNewtFileBrowserWidget::onHomeClicked()
{
  if (!m_homePath.isEmpty())
  {
    this->gotoPath(m_homePath);
    return;
  }

  this->setCursor(Qt::BusyCursor);
  QNetworkReply* reply = m_newt->requestHomePath(MACHINE);
  QObject::connect(reply, &QNetworkReply::finished, [this, reply]() {
    if (reply->error())
    {
      this->handleNetworkError(reply);
      return;
    }

    QString text;
    if (!this->getCommandReply(reply, text))
    {
      return;
    }

#ifndef NDEBUG
    qDebug() << "Home path is" << text;
#endif
    this->m_homePath = text;
    this->gotoPath(text);
    reply->deleteLater();
  });
}

void qtNewtFileBrowserWidget::onScratchClicked()
{
  if (!m_scratchPath.isEmpty())
  {
    this->gotoPath(m_scratchPath);
    return;
  }

  this->setCursor(Qt::BusyCursor);
  QNetworkReply* reply = m_newt->requestScratchPath(MACHINE);
  QObject::connect(reply, &QNetworkReply::finished, [this, reply]() {
    if (reply->error())
    {
      this->handleNetworkError(reply);
      return;
    }

    QString text;
    if (!this->getCommandReply(reply, text))
    {
      qWarning() << "Request for scratch folder location failed";
      return;
    }

#ifndef NDEBUG
    qDebug() << "Scratch path is" << text;
#endif
    this->m_scratchPath = text;
    this->gotoPath(text);
    reply->deleteLater();
  });
}

void qtNewtFileBrowserWidget::contextMenuEvent(QContextMenuEvent* event)
{
  // If download in progress, don't display the context menu
  if (this->ui->m_progressBar->isVisible())
  {
    return;
  }

  // Get selected row
  auto pos = this->ui->m_tableView->viewport()->mapFromGlobal(event->globalPos());
  int row = this->ui->m_tableView->rowAt(pos.y());

  if (row < 0)
  {
    return;
  }

  QMenu* menu = new QMenu(this);

  QStandardItem* nameItem = m_model.item(row, NAME_COLUMN);
  QString name = nameItem->text();
  if (name == "..")
  {
    QAction* goUpAction = new QAction("Go Up", menu);
    QObject::connect(goUpAction, &QAction::triggered, [this]() { this->goUpDirectory(); });
    menu->addAction(goUpAction);
    menu->exec(event->globalPos());
    return;
  }

  // Action to copy the path to the clipboard
  QAction* copyPathAction = new QAction("Copy Path to Clipboard", this);
  menu->addAction(copyPathAction);
  QObject::connect(copyPathAction, &QAction::triggered, [this, name]() {
    QString path = QString("%1/%2").arg(this->m_currentPath, name);
    auto clipboard = QGuiApplication::clipboard();
    clipboard->setText(path);
    emit this->pathCopied(path);
  });

  int intValue = nameItem->data().toInt();
  FileSystemItem::Type fsType = static_cast<FileSystemItem::Type>(intValue);
  if (fsType == FileSystemItem::FILE)
  {
    // Action to download the file
    QAction* downloadFileAction = new QAction("Download File", this);
    QObject::connect(
      downloadFileAction, &QAction::triggered, [this, name]() { this->downloadFile(name); });
    menu->addAction(downloadFileAction);

    // Action to view the file contents in a text browser
    QAction* viewFileAction = new QAction("View as Text", this);
    QObject::connect(viewFileAction, &QAction::triggered, [this, name]() { this->viewFile(name); });
    menu->addAction(viewFileAction);

    menu->exec(event->globalPos());
    return;
  }
  else if (fsType == FileSystemItem::FOLDER)
  {
    // Action to navigate to the folder
    QAction* openAction = new QAction("Open Directory", menu);
    QObject::connect(openAction, &QAction::triggered, [this, name]() {
      QString nextPath = QString("%1/%2").arg(m_currentPath, name);
      this->gotoPath(nextPath);
    });
    menu->addAction(openAction);
    menu->exec(event->globalPos());
    return;
  }
}

void qtNewtFileBrowserWidget::downloadFile(const QString& remoteFileName)
{
  // Create QFile for writing to download directory
  QString downloadDirectory = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
  QString localPath = downloadDirectory + "/" + remoteFileName;
  QFile* file = new QFile(localPath);
  if (!file->open(QIODevice::WriteOnly)) // truncates any existing file
  {
    delete file;
    QString msg = QString("Unable to open local file for writing at %1").arg(localPath);
    QMessageBox::critical(this, "Error Opening Local File", msg);
  }

  QString remotePath = QString("%1/%2").arg(m_currentPath, remoteFileName);
  QNetworkReply* reply = m_newt->requestFileDownload(MACHINE, remotePath);

  QObject::connect(
    reply, &QNetworkReply::readyRead, [this, reply, file]() { file->write(reply->readAll()); });

  QObject::connect(
    reply, &QNetworkReply::downloadProgress, [this](qint64 bytesReceived, qint64 bytesTotal) {
      this->ui->m_progressBar->setMaximum(static_cast<int>(bytesTotal));
      this->ui->m_progressBar->setValue(static_cast<int>(bytesReceived));
    });

  QObject::connect(reply, &QNetworkReply::finished, [this, reply, localPath]() {
    this->ui->m_progressBar->hide();
    QString msg;
    if (reply->error() == 0)
    {
      msg = QString("Downloaded file %1").arg(localPath);
      QMessageBox::information(this, "Download Complete", msg);
    }
    else
    {
      msg = QString("ERROR Downloading to file %1: %2").arg(localPath).arg(reply->error());
      QMessageBox::critical(this, "Download Error", msg);
    }
    reply->deleteLater();
  });

  this->ui->m_progressBar->setRange(0, 100);
  this->ui->m_progressBar->setValue(0);
  this->ui->m_progressBar->show();
  qDebug() << "Downloading" << remotePath;
}

bool qtNewtFileBrowserWidget::getCommandReply(QNetworkReply* reply, QString& text)
{
  QString content;
  QString error;
  this->m_newt->getCommandReply(reply, content, error);

  if (error.isEmpty())
  {
    text = content;
    reply->deleteLater();
    return true;
  }

  this->onNewtError(error, reply);
  return false;
}

void qtNewtFileBrowserWidget::gotoPath(const QString& path)
{
#ifndef NDEBUG
  qDebug() << "filebrowser::gotoPath" << path;
#endif

  emit this->beginGotoPath(path);
  m_requestPath = path;
  this->setCursor(Qt::BusyCursor);
  QNetworkReply* reply = m_newt->requestDirectoryList(MACHINE, path);
  QObject::connect(
    reply, &QNetworkReply::finished, this, &qtNewtFileBrowserWidget::onListFolderReply);
}

void qtNewtFileBrowserWidget::goUpDirectory()
{
  QString path = m_currentPath;
  // Strip off trailing slash
  if (path.back() == QChar('/'))
  {
    path = path.left(path.size() - 1);
  }
  // Strip off last directory
  int pos = path.lastIndexOf(QChar('/'));
  QString parentPath = path.left(pos);

  this->gotoPath(parentPath);
}

void qtNewtFileBrowserWidget::handleNetworkError(QNetworkReply* reply)
{
  QByteArray bytes = reply->readAll();
  QString errorMessage(bytes);
  this->onNewtError(errorMessage, reply);
}

void qtNewtFileBrowserWidget::viewFile(const QString& name)
{
  QString path = QString("%1/%2").arg(m_currentPath, name);
  QNetworkReply* reply = m_newt->requestFileDownload(MACHINE, path);
  QObject::connect(reply, &QNetworkReply::finished, [this, reply, name]() {
    this->ui->m_progressBar->hide();
    if (reply->error())
    {
      this->handleNetworkError(reply);
      return;
    }
    QByteArray bytes = reply->readAll();
    QString text = QString::fromUtf8(bytes);

    QDialog dialog(this);
    QVBoxLayout* layout = new QVBoxLayout(&dialog);
    QPlainTextEdit* browser = new QPlainTextEdit(&dialog);
    browser->setReadOnly(true);
    browser->setPlainText(text);

    layout->addWidget(browser);
    dialog.setLayout(layout);
    dialog.setWindowTitle(name);
    dialog.setMinimumSize(640, 640);
    dialog.exec();
  });

  this->ui->m_progressBar->setRange(0, 0);
  this->ui->m_progressBar->show();
}

void qtNewtFileBrowserWidget::onRequestNavigate(const QString& path)
{
  this->gotoPath(path);
}

} // namespace newt
