/*
    SPDX-FileCopyrightText: 2008-2010 Volker Lanz <vl@fidra.de>
    SPDX-FileCopyrightText: 2014-2020 Andrius Štikonas <andrius@stikonas.eu>
    SPDX-FileCopyrightText: 2014 Yuri Chornoivan <yurchor@ukr.net>
    SPDX-FileCopyrightText: 2018 Abhijeet Sharma <sharma.abhijeet2096@gmail.com>

    SPDX-License-Identifier: GPL-3.0-or-later
*/

#include "gui/partpropsdialog.h"
#include "gui/partpropswidget.h"

#include <core/partition.h>
#include <core/device.h>

#include <fs/filesystemfactory.h>

#include <util/capacity.h>
#include <util/helpers.h>
#include "util/guihelpers.h"

#include <utility>

#include <QComboBox>
#include <QFontDatabase>
#include <QtGlobal>
#include <QLineEdit>
#include <QLocale>
#include <QPalette>
#include <QPushButton>

#include <KConfigGroup>
#include <KLocalizedString>
#include <KMessageBox>
#include <KSharedConfig>

namespace {
    const int MAX_GPT_PARTITION_LABLE_LENGTH = 36;
}

/** Creates a new PartPropsDialog
    @param parent pointer to the parent widget
    @param d the Device the Partition is on
    @param p the Partition to show properties for
*/
PartPropsDialog::PartPropsDialog(QWidget* parent, Device& d, Partition& p) :
    QDialog(parent),
    m_Device(d),
    m_Partition(p),
    m_WarnFileSystemChange(false),
    m_DialogWidget(new PartPropsWidget(this)),
    m_ReadOnly(partition().isMounted() || partition().state() == Partition::State::Copy || partition().state() == Partition::State::Restore || d.partitionTable()->isReadOnly()),
    m_ForceRecreate(false)
{
    mainLayout = new QVBoxLayout(this);
    setLayout(mainLayout);
    mainLayout->addWidget(&dialogWidget());

    setWindowTitle(xi18nc("@title:window", "Partition properties: <filename>%1</filename>", partition().deviceNode()));

    setupDialog();
    setupConnections();

    KConfigGroup kcg(KSharedConfig::openConfig(), QStringLiteral("partPropsDialog"));
    restoreGeometry(kcg.readEntry<QByteArray>("Geometry", QByteArray()));
}

/** Destroys a PartPropsDialog */
PartPropsDialog::~PartPropsDialog()
{
    KConfigGroup kcg(KSharedConfig::openConfig(), QStringLiteral("partPropsDialog"));
    kcg.writeEntry("Geometry", saveGeometry());
}

QString PartPropsDialog::newPartitionLabel() const
{
    return dialogWidget().partitionLabel().text();
}

/** @return the new label */
QString PartPropsDialog::newLabel() const
{
    return dialogWidget().label().text();
}

/** @return the new Partition flags */
PartitionTable::Flags PartPropsDialog::newFlags() const
{
    PartitionTable::Flags flags;

    for (int i = 0; i < dialogWidget().listFlags().count(); i++)
        if (dialogWidget().listFlags().item(i)->checkState() == Qt::Checked)
            flags |= static_cast<PartitionTable::Flag>(dialogWidget().listFlags().item(i)->data(Qt::UserRole).toInt());

    return flags;
}

/** @return the new FileSystem type */
FileSystem::Type PartPropsDialog::newFileSystemType() const
{
    return FileSystem::typeForName(dialogWidget().fileSystem().currentText());
}

void PartPropsDialog::setupDialog()
{
    dialogButtonBox = new QDialogButtonBox;
    okButton = dialogButtonBox->addButton(QDialogButtonBox::Ok);
    cancelButton = dialogButtonBox->addButton(QDialogButtonBox::Cancel);
    mainLayout->addWidget(dialogButtonBox);
    okButton->setEnabled(false);
    cancelButton->setFocus();
    cancelButton->setDefault(true);
    connect(dialogButtonBox, &QDialogButtonBox::accepted, this, &PartPropsDialog::accept);
    connect(dialogButtonBox, &QDialogButtonBox::rejected, this, &PartPropsDialog::reject);

    dialogWidget().partWidget().init(&partition());

    const QString mp = partition().mountPoint().isEmpty()
                       ? xi18nc("@item mountpoint", "(none found)")
                       : partition().mountPoint();
    dialogWidget().mountPoint().setText(mp);

    const QString devPath = partition().deviceNode().isEmpty() || partition().deviceNode() == QStringLiteral("unallocated")
                            ? xi18nc("@item device path", "(not found)")
                            : partition().deviceNode();
    dialogWidget().devicePath().setText(devPath);

    dialogWidget().role().setText(partition().roles().toString());

    QString statusText = xi18nc("@label partition state", "idle");
    if (partition().isMounted()) {
        if (partition().roles().has(PartitionRole::Extended))
            statusText = xi18nc("@label partition state", "At least one logical partition is mounted.");
        else if (!partition().mountPoint().isEmpty())
            statusText = xi18nc("@label partition state", "mounted on <filename>%1</filename>", mp);
        else
            statusText = xi18nc("@label partition state", "mounted");
    }

    dialogWidget().status().setText(statusText);
    dialogWidget().uuid().setText(partition().fileSystem().uuid().isEmpty() ? xi18nc("@item uuid", "(none)") : partition().fileSystem().uuid());

    if(device().partitionTable()->type() == PartitionTable::gpt){
        QString PartitionUUID = partition().uuid().isEmpty() ? xi18nc("@item uuid", "(none)") : partition().uuid();

        dialogWidget().partitionLabel().setMaxLength(MAX_GPT_PARTITION_LABLE_LENGTH);
        dialogWidget().partitionLabel().setText(partition().label());
        dialogWidget().partitionUuid().setText(PartitionUUID);

        // On GPT there are no logical or extended partitions.
        // Therefore, it's unnecessary to show partition type on GPT.
        dialogWidget().m_LabelTextRole->hide();
        dialogWidget().role().hide();
    }
    else{
        dialogWidget().partitionLabel().hide();
        dialogWidget().partitionTextLabel().hide();
        dialogWidget().partitionUuid().hide();
        dialogWidget().partitionTextUuid().hide();
    }

    setupFileSystemComboBox();

    // don't do this before the file system combo box has been set up!
    dialogWidget().label().setText(newLabel().isEmpty() ? partition().fileSystem().label() : newLabel());
    dialogWidget().capacity().setText(Capacity::formatByteSize(partition().capacity()));

    if (Capacity(partition(), Capacity::Type::Available).isValid()) {
        const qint64 availPercent = (partition().fileSystem().length() - partition().fileSystem().sectorsUsed()) * 100 / partition().fileSystem().length();

        const QString availString = i18n("%1% — %2", availPercent, Capacity::formatByteSize(partition().available()));
        const QString usedString = i18n("%1% — %2", (100 - availPercent), Capacity::formatByteSize(partition().used()));

        dialogWidget().available().setText(availString);
        dialogWidget().used().setText(usedString);
    } else {
        dialogWidget().available().setText(Capacity::invalidString());
        dialogWidget().used().setText(Capacity::invalidString());
    }

    dialogWidget().firstSector().setText(QLocale().toString(partition().firstSector()));
    dialogWidget().lastSector().setText(QLocale().toString(partition().lastSector()));
    dialogWidget().numSectors().setText(QLocale().toString(partition().length()));

    setupFlagsList();

    updateHideAndShow();

    setMinimumSize(dialogWidget().size());
    resize(dialogWidget().size());
}

void PartPropsDialog::setupFlagsList()
{
    int f = 1;
    QString s;
    while (!(s = PartitionTable::flagName(static_cast<PartitionTable::Flag>(f))).isEmpty()) {
        if (partition().availableFlags() & f) {
            QListWidgetItem* item = new QListWidgetItem(s);
            dialogWidget().listFlags().addItem(item);
            item->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled);
            item->setData(Qt::UserRole, f);
            item->setCheckState((partition().activeFlags() & f) ? Qt::Checked : Qt::Unchecked);
        }

        f <<= 1;
    }
}

void PartPropsDialog::updateHideAndShow()
{
    // create a temporary fs for some checks
    const FileSystem* fs = FileSystemFactory::create(newFileSystemType(), -1, -1, -1, -1, QString());

    if (fs == nullptr || fs->supportSetLabel() == FileSystem::cmdSupportNone) {
        dialogWidget().label().setReadOnly(true);
        dialogWidget().noSetLabel().setVisible(true);
        dialogWidget().noSetLabel().setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));

        QPalette palette = dialogWidget().noSetLabel().palette();
        QColor f = palette.color(QPalette::WindowText);
        f.setAlpha(128);
        palette.setColor(QPalette::WindowText, f);
        dialogWidget().noSetLabel().setPalette(palette);
    } else {
        dialogWidget().label().setReadOnly(isReadOnly() && !partition().fileSystem().supportSetLabelOnline());
        dialogWidget().noSetLabel().setVisible(false);
    }

    // when do we show the uuid?
    const bool showUuid =
        partition().state() != Partition::State::New &&                           // not for new partitions
        !(fs == nullptr || fs->supportGetUUID() == FileSystem::cmdSupportNone);   // not if the FS doesn't support it

    dialogWidget().showUuid(showUuid);

    delete fs;

    // when do we show available and used capacity?
    const bool showAvailableAndUsed =
        partition().state() != Partition::State::New &&             // not for new partitions
        !partition().roles().has(PartitionRole::Extended) &&        // neither for extended
        !partition().roles().has(PartitionRole::Unallocated) &&     // or for unallocated
        newFileSystemType() != FileSystem::Type::Unformatted;       // and not for unformatted file systems

    dialogWidget().showAvailable(showAvailableAndUsed);
    dialogWidget().showUsed(showAvailableAndUsed);

    // when do we show the file system combo box?
    const bool showFileSystem =
        !partition().roles().has(PartitionRole::Extended) &&        // not for extended, they have no file system
        !partition().roles().has(PartitionRole::Unallocated) &&     // and not for unallocated: no choice there
                                                                    // do not show file system comboBox for open luks volumes.
        !(partition().roles().has(PartitionRole::Luks) && partition().fileSystem().type() != FileSystem::Type::Luks);
    dialogWidget().showFileSystem(showFileSystem);

    // when do we show the recreate file system check box?
    const bool showCheckRecreate =
        showFileSystem &&                                                       // only if we also show the file system
        partition().fileSystem().supportCreate() != FileSystem::cmdSupportNone &&  // and support creating this file system
        partition().fileSystem().type() != FileSystem::Type::Unknown &&         // and not for unknown file systems
        partition().state() != Partition::State::New &&                         // or new partitions
        !partition().roles().has(PartitionRole::Luks);                          // or encrypted filesystems

    dialogWidget().showCheckRecreate(showCheckRecreate);

    // when do we show the list of partition flags?
    const bool showListFlags =
        partition().state() != Partition::State::New &&                         // not for new partitions
        !partition().roles().has(PartitionRole::Unallocated);                   // and not for unallocated space

    dialogWidget().showListFlags(showListFlags);

    dialogWidget().checkRecreate().setEnabled(!isReadOnly());
    dialogWidget().listFlags().setEnabled(!isReadOnly());
    dialogWidget().fileSystem().setEnabled(!isReadOnly() && !forceRecreate());
}

void PartPropsDialog::setupConnections()
{
    auto setDirty = [this]() {
        this->setDirty();
    };

    connect(&dialogWidget().partitionLabel(), &QLineEdit::textEdited, this, setDirty);
    connect(&dialogWidget().label(), &QLineEdit::textEdited, this, setDirty);
    connect(&dialogWidget().fileSystem(), &QComboBox::currentIndexChanged, this, &PartPropsDialog::onFilesystemChanged);
#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
    connect(&dialogWidget().checkRecreate(), &QCheckBox::stateChanged, this, &PartPropsDialog::onRecreate);
#else
    connect(&dialogWidget().checkRecreate(), &QCheckBox::checkStateChanged, this, &PartPropsDialog::onRecreate);
#endif

    // We want to enable the OK-button whenever the user checks or unchecks a flag in the flag list.
    // But it seems Qt doesn't offer a foolproof way to detect if this has happened: The currentRow/ItemChanged
    // signal only means the _current_ item has changed, but not necessarily that it was checked/unchecked. And
    // itemClicked alone isn't enough either. We choose to rather enable the OK-button too often than too
    // seldom.
    connect(&dialogWidget().listFlags(), &QListWidget::itemClicked, this, &PartPropsDialog::setDirty);
    connect(&dialogWidget().listFlags(), &QListWidget::currentRowChanged, this, setDirty);
}

void PartPropsDialog::setDirty(void*)
{
    okButton->setEnabled(true);
    okButton->setDefault(true);
}

void PartPropsDialog::setupFileSystemComboBox()
{
    dialogWidget().fileSystem().clear();
    QString selected;
    QStringList fsNames;

    for(const auto &fs : FileSystemFactory::map())
    {
        // If the partition isn't encrypted, skip the luks FS
        if (fs->type() == FileSystem::Type::Luks && partition().fileSystem().type() != FileSystem::Type::Luks)
            continue;
        if (fs->type() == FileSystem::Type::Luks2 && partition().fileSystem().type() != FileSystem::Type::Luks2)
            continue;
        if (partition().fileSystem().type() == fs->type() || (fs->supportCreate() != FileSystem::cmdSupportNone &&
                            partition().capacity() >= fs->minCapacity() && partition().capacity() <= fs->maxCapacity())) {
            QString name = fs->name();

            if (partition().fileSystem().type() == fs->type())
                selected = name;

            // If the partition isn't extended, skip the extended FS
            if (fs->type() == FileSystem::Type::Extended && !partition().roles().has(PartitionRole::Extended))
                continue;

            // The user cannot change the filesystem back to "unformatted" once a filesystem has been created.
            if (fs->type() == FileSystem::Type::Unformatted) {
                // .. but if the file system is unknown to us, show the unformatted option as the currently selected one
                if (partition().fileSystem().type() == FileSystem::Type::Unknown) {
                    name = FileSystem::nameForType(FileSystem::Type::Unformatted);
                    selected = name;
                } else if (partition().fileSystem().type() != FileSystem::Type::Unformatted && partition().state() != Partition::State::New)
                    continue;
            }

            fsNames.append(name);
        }
    }

    std::sort(fsNames.begin(), fsNames.end(), caseInsensitiveLessThan);

    for (const auto &fsName : std::as_const(fsNames))
        dialogWidget().fileSystem().addItem(createFileSystemColor(FileSystem::typeForName(fsName), 8), fsName);

    dialogWidget().fileSystem().setCurrentIndex(dialogWidget().fileSystem().findText(selected));

    const FileSystem* fs = FileSystemFactory::create(FileSystem::typeForName(dialogWidget().fileSystem().currentText()), -1, -1, -1, -1, QString());
    dialogWidget().m_EditLabel->setMaxLength(fs->maxLabelLength());
    dialogWidget().m_EditLabel->setValidator(fs->labelValidator(dialogWidget().m_EditLabel));
}

void PartPropsDialog::updatePartitionFileSystem()
{
    FileSystem* fs = FileSystemFactory::create(newFileSystemType(), partition().firstSector(), partition().lastSector(), partition().sectorSize());
    partition().deleteFileSystem();
    partition().setFileSystem(fs);
    dialogWidget().partWidget().update();
}

void PartPropsDialog::onFilesystemChanged(int)
{
    if (partition().state() == Partition::State::New || warnFileSystemChange() || KMessageBox::warningContinueCancel(this,
            xi18nc("@info", "<para><warning>You are about to lose all data on partition <filename>%1</filename>.</warning></para>"
                   "<para>Changing the file system on a partition already on disk will erase all its contents. If you continue now and apply the resulting operation in the main window, all data on <filename>%1</filename> will unrecoverably be lost.</para>", partition().deviceNode()),
            xi18nc("@title:window", "Really Recreate <filename>%1</filename> with File System %2?", partition().deviceNode(), dialogWidget().fileSystem().currentText()),
            KGuiItem(xi18nc("@action:button", "Change the File System"), QStringLiteral("arrow-right")),
            KGuiItem(xi18nc("@action:button", "Do Not Change the File System"), QStringLiteral("dialog-cancel")), QStringLiteral("reallyChangeFileSystem")) == KMessageBox::Continue) {
        setDirty();
        updateHideAndShow();
        setWarnFileSystemChange();
        updatePartitionFileSystem();

        const FileSystem* fs = FileSystemFactory::create(FileSystem::typeForName(dialogWidget().fileSystem().currentText()), -1, -1, -1, -1, QString());
        dialogWidget().m_EditLabel->setMaxLength(fs->maxLabelLength());
        dialogWidget().m_EditLabel->setValidator(fs->labelValidator(dialogWidget().m_EditLabel));
    } else {
        dialogWidget().fileSystem().disconnect(this);
        setupFileSystemComboBox();
        connect(&dialogWidget().fileSystem(), &QComboBox::currentIndexChanged, this, &PartPropsDialog::onFilesystemChanged);
    }
}

void PartPropsDialog::onRecreate(int state)
{
    if (state == Qt::Checked && (warnFileSystemChange() || KMessageBox::warningContinueCancel(this,
                                 xi18nc("@info", "<para><warning>You are about to lose all data on partition <filename>%1</filename>.</warning></para>"
                                        "<para>Recreating a file system will erase all its contents. If you continue now and apply the resulting operation in the main window, all data on <filesystem>%1</filesystem> will unrecoverably be lost.</para>", partition().deviceNode()),
                                 xi18nc("@title:window", "Really Recreate File System on <filename>%1</filename>?", partition().deviceNode()),
                                 KGuiItem(xi18nc("@action:button", "Recreate the File System"), QStringLiteral("arrow-right")),
                                 KGuiItem(xi18nc("@action:button", "Do Not Recreate the File System"), QStringLiteral("dialog-cancel")), QStringLiteral("reallyRecreateFileSystem")) == KMessageBox::Continue)) {
        setDirty();
        setWarnFileSystemChange();
        setForceRecreate(true);
        dialogWidget().fileSystem().setCurrentIndex(dialogWidget().fileSystem().findText(partition().fileSystem().name()));
        dialogWidget().fileSystem().setEnabled(false);
        updateHideAndShow();
        updatePartitionFileSystem();
    } else {
        setForceRecreate(false);
        dialogWidget().checkRecreate().setCheckState(Qt::Unchecked);
        dialogWidget().fileSystem().setEnabled(true);
        updateHideAndShow();
    }
}
