// Copyright 2016 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include "DolphinQt/Config/FilesystemWidget.h"

#include <QApplication>
#include <QCoreApplication>
#include <QFileInfo>
#include <QHeaderView>
#include <QMenu>
#include <QStandardItemModel>
#include <QStyleFactory>
#include <QTreeView>
#include <QVBoxLayout>

#include <future>

#include "Common/StringUtil.h"

#include "DiscIO/DiscExtractor.h"
#include "DiscIO/DiscUtils.h"
#include "DiscIO/Filesystem.h"
#include "DiscIO/Volume.h"

#include "DolphinQt/QtUtils/DolphinFileDialog.h"
#include "DolphinQt/QtUtils/ModalMessageBox.h"
#include "DolphinQt/QtUtils/ParallelProgressDialog.h"
#include "DolphinQt/Resources.h"

#include "UICommon/UICommon.h"

constexpr int ENTRY_PARTITION = Qt::UserRole;
constexpr int ENTRY_NAME = Qt::UserRole + 1;
constexpr int ENTRY_TYPE = Qt::UserRole + 2;

enum class EntryType
{
  Disc = -2,
  Partition = -1,
  File = 0,
  Dir = 1
};
Q_DECLARE_METATYPE(EntryType);

FilesystemWidget::FilesystemWidget(std::shared_ptr<DiscIO::Volume> volume)
    : m_volume(std::move(volume))
{
  CreateWidgets();
  ConnectWidgets();
  PopulateView();
}

FilesystemWidget::~FilesystemWidget() = default;

void FilesystemWidget::CreateWidgets()
{
  auto* layout = new QVBoxLayout;

  m_tree_model = new QStandardItemModel(0, 2);
  m_tree_model->setHorizontalHeaderLabels({tr("Name"), tr("Size")});

  m_tree_view = new QTreeView(this);
  m_tree_view->setModel(m_tree_model);
  m_tree_view->setContextMenuPolicy(Qt::CustomContextMenu);

  auto* header = m_tree_view->header();

  header->setSectionResizeMode(0, QHeaderView::Stretch);
  header->setSectionResizeMode(1, QHeaderView::ResizeToContents);
  header->setStretchLastSection(false);

// Windows: Set style to (old) windows, which draws branch lines
#ifdef _WIN32
  if (QApplication::style()->objectName() == QStringLiteral("windowsvista"))
    m_tree_view->setStyle(QStyleFactory::create(QStringLiteral("windows")));
#endif

  layout->addWidget(m_tree_view);

  setLayout(layout);
}

void FilesystemWidget::ConnectWidgets()
{
  connect(m_tree_view, &QTreeView::customContextMenuRequested, this,
          &FilesystemWidget::ShowContextMenu);
}

void FilesystemWidget::PopulateView()
{
  // Cache these two icons, the tree will use them a lot.
  m_folder_icon = Resources::GetScaledIcon("isoproperties_folder");
  m_file_icon = Resources::GetScaledIcon("isoproperties_file");

  auto* disc = new QStandardItem(tr("Disc"));
  disc->setEditable(false);
  disc->setIcon(Resources::GetScaledIcon("isoproperties_disc"));
  disc->setData(QVariant::fromValue(EntryType::Disc), ENTRY_TYPE);
  m_tree_model->appendRow(disc);
  m_tree_view->expand(disc->index());

  const auto& partitions = m_volume->GetPartitions();

  for (size_t i = 0; i < partitions.size(); i++)
  {
    auto* item = new QStandardItem;
    item->setEditable(false);

    item->setIcon(Resources::GetScaledIcon("isoproperties_disc"));
    item->setData(static_cast<qlonglong>(i), ENTRY_PARTITION);
    item->setData(QVariant::fromValue(EntryType::Partition), ENTRY_TYPE);

    PopulateDirectory(static_cast<int>(i), item, partitions[i]);

    disc->appendRow(item);

    if (m_volume->GetGamePartition() == partitions[i])
      m_tree_view->expand(item->index());
  }

  if (partitions.empty())
    PopulateDirectory(-1, disc, DiscIO::PARTITION_NONE);
}

void FilesystemWidget::PopulateDirectory(int partition_id, QStandardItem* root,
                                         const DiscIO::Partition& partition)
{
  auto partition_type = m_volume->GetPartitionType(partition);
  auto game_id = m_volume->GetGameID(partition);
  auto title_id = m_volume->GetTitleID(partition);

  QString text = root->text();

  if (!text.isEmpty())
    text += QStringLiteral(" - ");

  if (partition_type)
  {
    QString partition_type_str;
    switch (partition_type.value())
    {
    case DiscIO::PARTITION_DATA:
      partition_type_str = tr("Data Partition (%1)").arg(partition_type.value());
      break;
    case DiscIO::PARTITION_UPDATE:
      partition_type_str = tr("Update Partition (%1)").arg(partition_type.value());
      break;
    case DiscIO::PARTITION_CHANNEL:
      partition_type_str = tr("Channel Partition (%1)").arg(partition_type.value());
      break;
    case DiscIO::PARTITION_INSTALL:
      partition_type_str = tr("Install Partition (%1)").arg(partition_type.value());
      break;
    default:
      partition_type_str =
          tr("Other Partition (%1)").arg(partition_type.value(), 8, 16, QLatin1Char('0'));
      break;
    }
    text += partition_type_str + QStringLiteral(" - ");
  }

  text += QString::fromStdString(game_id);

  if (title_id)
  {
    text += QStringLiteral(" - %1 (").arg(title_id.value(), 16, 16, QLatin1Char('0'));
    for (u32 i = 0; i < 4; i++)
    {
      char c = static_cast<char>(title_id.value() >> 8 * (3 - i));
      if (IsPrintableCharacter(c))
        text += QLatin1Char(c);
      else
        text += QLatin1Char('.');
    }
    text += QLatin1Char(')');
  }

  root->setText(text);

  const DiscIO::FileSystem* const file_system = m_volume->GetFileSystem(partition);
  if (file_system)
    PopulateDirectory(partition_id, root, file_system->GetRoot());
}

void FilesystemWidget::PopulateDirectory(int partition_id, QStandardItem* root,
                                         const DiscIO::FileInfo& directory)
{
  for (const auto& info : directory)
  {
    auto* item = new QStandardItem(QString::fromStdString(info.GetName()));
    item->setEditable(false);
    item->setIcon(info.IsDirectory() ? m_folder_icon : m_file_icon);

    if (info.IsDirectory())
      PopulateDirectory(partition_id, item, info);

    item->setData(partition_id, ENTRY_PARTITION);
    item->setData(QString::fromStdString(info.GetPath()), ENTRY_NAME);
    item->setData(QVariant::fromValue(info.IsDirectory() ? EntryType::Dir : EntryType::File),
                  ENTRY_TYPE);

    const auto size = info.GetTotalSize();

    auto* size_item = new QStandardItem(QString::fromStdString(UICommon::FormatSize(size)));
    size_item->setTextAlignment(Qt::AlignRight);
    size_item->setEditable(false);

    root->appendRow({item, size_item});
  }
}

QString FilesystemWidget::SelectFolder()
{
  return DolphinFileDialog::getExistingDirectory(this,
                                                 QObject::tr("Choose the folder to extract to"));
}

void FilesystemWidget::ShowContextMenu(const QPoint&)
{
  auto* selection = m_tree_view->selectionModel();
  if (!selection->hasSelection())
    return;

  auto* item = m_tree_model->itemFromIndex(selection->selectedIndexes()[0]);

  QMenu* menu = new QMenu(this);

  EntryType type = item->data(ENTRY_TYPE).value<EntryType>();

  DiscIO::Partition partition = type == EntryType::Disc ?
                                    DiscIO::PARTITION_NONE :
                                    GetPartitionFromID(item->data(ENTRY_PARTITION).toInt());
  QString path = item->data(ENTRY_NAME).toString();

  const bool is_filesystem_root = (type == EntryType::Disc && m_volume->GetPartitions().empty()) ||
                                  type == EntryType::Partition;

  if (type == EntryType::Dir || is_filesystem_root)
  {
    menu->addAction(tr("Extract Files..."), this, [this, partition, path] {
      auto folder = SelectFolder();

      if (!folder.isEmpty())
        ExtractDirectory(partition, path, folder);
    });
  }

  if (is_filesystem_root)
  {
    menu->addAction(tr("Extract System Data..."), this, [this, partition] {
      auto folder = SelectFolder();

      if (folder.isEmpty())
        return;

      if (ExtractSystemData(partition, folder))
        ModalMessageBox::information(this, tr("Success"),
                                     tr("Successfully extracted system data."));
      else
        ModalMessageBox::critical(this, tr("Error"), tr("Failed to extract system data."));
    });
  }

  switch (type)
  {
  case EntryType::Disc:
    menu->addAction(tr("Extract Entire Disc..."), this, [this, path] {
      auto folder = SelectFolder();

      if (folder.isEmpty())
        return;

      if (m_volume->GetPartitions().empty())
      {
        ExtractPartition(DiscIO::PARTITION_NONE, folder);
      }
      else
      {
        for (DiscIO::Partition& p : m_volume->GetPartitions())
        {
          if (const std::optional<u32> partition_type = m_volume->GetPartitionType(p))
          {
            const std::string partition_name = DiscIO::NameForPartitionType(*partition_type, true);
            ExtractPartition(p, folder + QChar(u'/') + QString::fromStdString(partition_name));
          }
        }
      }
    });
    break;
  case EntryType::Partition:
    menu->addAction(tr("Extract Entire Partition..."), this, [this, partition] {
      auto folder = SelectFolder();
      if (!folder.isEmpty())
        ExtractPartition(partition, folder);
    });
    break;
  case EntryType::File:
    menu->addAction(tr("Extract File..."), this, [this, partition, path] {
      auto dest =
          DolphinFileDialog::getSaveFileName(this, tr("Save File to"), QFileInfo(path).fileName());

      if (!dest.isEmpty())
        ExtractFile(partition, path, dest);
    });
    break;
  case EntryType::Dir:
    // Handled above the switch statement
    break;
  }

  menu->exec(QCursor::pos());
}

DiscIO::Partition FilesystemWidget::GetPartitionFromID(int id)
{
  return id == -1 ? DiscIO::PARTITION_NONE : m_volume->GetPartitions()[id];
}

void FilesystemWidget::ExtractPartition(const DiscIO::Partition& partition, const QString& out)
{
  ExtractDirectory(partition, QString{}, out + QStringLiteral("/files"));
  ExtractSystemData(partition, out);
}

bool FilesystemWidget::ExtractSystemData(const DiscIO::Partition& partition, const QString& out)
{
  return DiscIO::ExportSystemData(*m_volume, partition, out.toStdString());
}

void FilesystemWidget::ExtractDirectory(const DiscIO::Partition& partition, const QString& path,
                                        const QString& out)
{
  const DiscIO::FileSystem* filesystem = m_volume->GetFileSystem(partition);
  if (!filesystem)
    return;

  std::unique_ptr<DiscIO::FileInfo> info = filesystem->FindFileInfo(path.toStdString());
  u32 size = info->GetTotalChildren();

  ParallelProgressDialog dialog(this);
  dialog.GetRaw()->setMinimum(0);
  dialog.GetRaw()->setMaximum(size);
  dialog.GetRaw()->setWindowTitle(tr("Progress"));

  const bool all = path.isEmpty();

  std::future<void> future = std::async(std::launch::async, [&] {
    int progress = 0;

    DiscIO::ExportDirectory(
        *m_volume, partition, *info, true, path.toStdString(), out.toStdString(),
        [all, &dialog, &progress](const std::string& current) {
          dialog.SetLabelText(
              (all ? QObject::tr("Extracting All Files...") :
                     QObject::tr("Extracting Directory..."))
                  .append(QStringLiteral(" %1").arg(QString::fromStdString(current))));
          dialog.SetValue(++progress);

          QCoreApplication::processEvents();
          return dialog.WasCanceled();
        });

    dialog.Reset();
  });

  dialog.GetRaw()->exec();
  future.get();
}

void FilesystemWidget::ExtractFile(const DiscIO::Partition& partition, const QString& path,
                                   const QString& out)
{
  const DiscIO::FileSystem* filesystem = m_volume->GetFileSystem(partition);
  if (!filesystem)
    return;

  bool success = DiscIO::ExportFile(
      *m_volume, partition, filesystem->FindFileInfo(path.toStdString()).get(), out.toStdString());

  if (success)
    ModalMessageBox::information(this, tr("Success"), tr("Successfully extracted file."));
  else
    ModalMessageBox::critical(this, tr("Error"), tr("Failed to extract file."));
}