/*
 * Fooyin
 * Copyright © 2024, Luke Taylor <luket@pm.me>
 *
 * Fooyin 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.
 *
 * Fooyin 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 Fooyin.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "fileopsdialog.h"

#include "fileopsmodel.h"
#include "fileopssettings.h"

#include <core/library/librarymanager.h>
#include <gui/guiconstants.h>
#include <gui/iconloader.h>
#include <gui/widgets/scriptlineedit.h>
#include <utils/settings/settingsmanager.h>
#include <utils/utils.h>

#include <QAction>
#include <QByteArray>
#include <QCheckBox>
#include <QComboBox>
#include <QDialogButtonBox>
#include <QDir>
#include <QFileDialog>
#include <QGridLayout>
#include <QGroupBox>
#include <QHeaderView>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QPushButton>
#include <QRadioButton>
#include <QTreeView>

using namespace Qt::StringLiterals;

constexpr auto CurrentPreset = "FileOps/CurrentPreset";

namespace Fooyin::FileOps {
class FileOpsDialogPrivate : public QObject
{
    Q_OBJECT

public:
    FileOpsDialogPrivate(FileOpsDialog* self, MusicLibrary* library, const TrackList& tracks, Operation operation,
                         std::shared_ptr<AudioLoader> audioLoader, SettingsManager* settings,
                         LibraryManager* libraryManager);

    void setup();

    void changeOperation(Operation operation);
    void updateOptionState() const;
    void updateButtonState() const;

    [[nodiscard]] FileOpPreset currentPreset() const;

    void savePreset();
    void loadPreset();
    void deletePreset();

    void saveCurrentPreset() const;
    void loadCurrentPreset() const;
    void loadPresets();
    void populatePresets();

    void simulateOp() const;
    void toggleRun();
    void modelUpdated();

    void populateDestinationOptions() const;
    void browseDestination() const;
    std::vector<FileOpPreset>::iterator findPreset(const QString& name);

    FileOpsDialog* m_self;
    SettingsManager* m_settings;
    LibraryManager* m_libraryManager;

    Operation m_operation;

    QRadioButton* m_copyOp;
    QRadioButton* m_moveOp;
    QRadioButton* m_renameOp;
    QRadioButton* m_extractOp;

    QComboBox* m_destination;
    ScriptLineEdit* m_filename;
    QCheckBox* m_entireSource;
    QCheckBox* m_removeEmpty;
    QCheckBox* m_removeArchive;

    QComboBox* m_presetBox;
    QPushButton* m_loadButton;
    QPushButton* m_saveButton;
    QPushButton* m_deleteButton;

    QTreeView* m_resultsTable;
    FileOpsModel* m_model;
    QLabel* m_status;
    QPushButton* m_runButton{nullptr};

    std::vector<FileOpPreset> m_presets;
    bool m_supportsFileOperations{false};
    bool m_supportsExtract{false};
    bool m_loading{false};
    bool m_running{false};
    bool m_presetsChanged{false};
};

FileOpsDialogPrivate::FileOpsDialogPrivate(FileOpsDialog* self, MusicLibrary* library, const TrackList& tracks,
                                           Operation operation, std::shared_ptr<AudioLoader> audioLoader,
                                           SettingsManager* settings, LibraryManager* libraryManager)
    : m_self{self}
    , m_settings{settings}
    , m_libraryManager{libraryManager}
    , m_operation{operation}
    , m_copyOp{new QRadioButton(FileOpsDialog::tr("Copy"), self)}
    , m_moveOp{new QRadioButton(FileOpsDialog::tr("Move"), self)}
    , m_renameOp{new QRadioButton(FileOpsDialog::tr("Rename"), self)}
    , m_extractOp{new QRadioButton(FileOpsDialog::tr("Extract"), self)}
    , m_destination{new QComboBox(self)}
    , m_filename{new ScriptLineEdit(u"%filename%"_s, tracks.front(), self)}
    , m_entireSource{new QCheckBox(self)}
    , m_removeEmpty{new QCheckBox(FileOpsDialog::tr("Remove empty source folders"), self)}
    , m_removeArchive{new QCheckBox(FileOpsDialog::tr("Delete archive after extraction"), self)}
    , m_presetBox{new QComboBox(self)}
    , m_loadButton{new QPushButton(FileOpsDialog::tr("&Load"), self)}
    , m_saveButton{new QPushButton(FileOpsDialog::tr("&Save"), self)}
    , m_deleteButton{new QPushButton(FileOpsDialog::tr("&Delete"), self)}
    , m_resultsTable{new QTreeView(self)}
    , m_model{new FileOpsModel(library, std::move(audioLoader), tracks, m_settings, self)}
    , m_status{new QLabel(self)}
    , m_supportsFileOperations{std::ranges::all_of(tracks, [](const Track& track) { return !track.isInArchive(); })}
    , m_supportsExtract{std::ranges::all_of(tracks, [](const Track& track) { return track.isInArchive(); })}
{ }

void FileOpsDialogPrivate::setup()
{
    auto* layout = new QGridLayout(m_self);

    auto* browseAction = new QAction({}, m_self);
    Gui::setThemeIcon(browseAction, Constants::Icons::Options);
    QObject::connect(browseAction, &QAction::triggered, this, &FileOpsDialogPrivate::browseDestination);
    m_destination->setEditable(true);
    auto* destLine = m_destination->lineEdit();
    destLine->addAction(browseAction, QLineEdit::TrailingPosition);

    m_resultsTable->setModel(m_model);
    m_resultsTable->header()->setStretchLastSection(true);

    const auto firstHint = m_resultsTable->header()->sectionSizeHint(0);
    m_resultsTable->header()->setSectionResizeMode(0, QHeaderView::Fixed);
    m_resultsTable->header()->resizeSection(0, static_cast<int>(firstHint * 1.6));
    m_resultsTable->header()->resizeSection(1, 200);
    m_resultsTable->header()->resizeSection(2, 300);

    m_copyOp->setChecked(m_operation == Operation::Copy);
    m_moveOp->setChecked(m_operation == Operation::Move);
    m_renameOp->setChecked(m_operation == Operation::Rename);
    m_extractOp->setChecked(m_operation == Operation::Extract);
    m_copyOp->setEnabled(m_supportsFileOperations);
    m_moveOp->setEnabled(m_supportsFileOperations);
    m_renameOp->setEnabled(m_supportsFileOperations);
    m_extractOp->setEnabled(m_supportsExtract);

    m_presetBox->setEditable(true);

    auto* opLabel = new QLabel(tr("Operation") + u":"_s);

    auto* destLabel     = new QLabel(tr("Destination") + u":"_s);
    auto* filenameLabel = new QLabel(tr("Filename") + u":"_s);

    auto* presetGroup  = new QGroupBox(tr("Presets"), m_self);
    auto* presetLayout = new QGridLayout(presetGroup);

    QObject::connect(m_loadButton, &QAbstractButton::clicked, this, &FileOpsDialogPrivate::loadPreset);
    QObject::connect(m_saveButton, &QAbstractButton::clicked, this, &FileOpsDialogPrivate::savePreset);
    QObject::connect(m_deleteButton, &QAbstractButton::clicked, this, &FileOpsDialogPrivate::deletePreset);

    presetLayout->addWidget(m_presetBox, 0, 0, 1, 3);
    presetLayout->addWidget(m_loadButton, 1, 0);
    presetLayout->addWidget(m_saveButton, 1, 1);
    presetLayout->addWidget(m_deleteButton, 1, 2);
    presetLayout->setColumnStretch(3, 1);

    auto* buttonBox = new QDialogButtonBox(QDialogButtonBox::Apply | QDialogButtonBox::Close, m_self);
    m_runButton     = buttonBox->button(QDialogButtonBox::Apply);
    m_runButton->setText(tr("&Run"));
    QObject::connect(m_runButton, &QAbstractButton::clicked, this, &FileOpsDialogPrivate::toggleRun);
    QObject::connect(buttonBox, &QDialogButtonBox::accepted, m_self, &QDialog::accept);
    QObject::connect(buttonBox, &QDialogButtonBox::rejected, m_self, &QDialog::reject);

    auto* opLayout = new QHBoxLayout();

    opLayout->addWidget(m_copyOp);
    opLayout->addWidget(m_moveOp);
    opLayout->addWidget(m_renameOp);
    opLayout->addWidget(m_extractOp);

    auto* optionsLayout = new QHBoxLayout();

    optionsLayout->addWidget(m_entireSource);
    optionsLayout->addWidget(m_removeEmpty);
    optionsLayout->addWidget(m_removeArchive);
    optionsLayout->addStretch();

    int row{0};
    layout->addWidget(opLabel, row, 0);
    layout->addLayout(opLayout, row, 1);
    layout->addWidget(presetGroup, row++, 4, 3, 1);
    layout->addWidget(destLabel, row, 0);
    layout->addWidget(m_destination, row++, 1, 1, 3);
    layout->addWidget(filenameLabel, row, 0);
    layout->addWidget(m_filename, row++, 1, 1, 3);
    layout->addLayout(optionsLayout, row++, 0, 1, 6);
    layout->addWidget(m_resultsTable, row++, 0, 1, 5);
    layout->addWidget(m_status, row, 0, 1, 5);
    layout->addWidget(buttonBox, row, 4, 1, 2);
    layout->setColumnStretch(3, 1);

    QObject::connect(m_copyOp, &QRadioButton::clicked, m_model, [this]() { changeOperation(Operation::Copy); });
    QObject::connect(m_moveOp, &QRadioButton::clicked, m_model, [this]() { changeOperation(Operation::Move); });
    QObject::connect(m_renameOp, &QRadioButton::clicked, m_model, [this]() { changeOperation(Operation::Rename); });
    QObject::connect(m_extractOp, &QRadioButton::clicked, m_model, [this]() { changeOperation(Operation::Extract); });
    QObject::connect(destLine, &QLineEdit::textChanged, this, &FileOpsDialogPrivate::simulateOp);
    QObject::connect(m_filename, &QLineEdit::textChanged, this, &FileOpsDialogPrivate::simulateOp);
    QObject::connect(m_entireSource, &QCheckBox::clicked, this, [this]() {
        updateOptionState();
        simulateOp();
    });
    QObject::connect(m_removeEmpty, &QCheckBox::clicked, this, &FileOpsDialogPrivate::simulateOp);
    QObject::connect(m_removeArchive, &QCheckBox::clicked, this, &FileOpsDialogPrivate::simulateOp);
    QObject::connect(m_presetBox, &QComboBox::currentTextChanged, this, &FileOpsDialogPrivate::updateButtonState);

    QObject::connect(m_model, &FileOpsModel::simulated, this, &FileOpsDialogPrivate::modelUpdated);
    QObject::connect(m_model, &QAbstractItemModel::rowsRemoved, this, &FileOpsDialogPrivate::modelUpdated);

    changeOperation(m_operation);
    loadPresets();
    populateDestinationOptions();
    loadCurrentPreset();
    updateButtonState();

    m_self->resize(720, 480);
}

void FileOpsDialogPrivate::changeOperation(Operation operation)
{
    m_operation = operation;
    m_destination->setDisabled(operation == Operation::Rename);
    m_removeEmpty->setEnabled(operation == Operation::Move);
    m_entireSource->setEnabled(operation == Operation::Copy || operation == Operation::Move
                               || operation == Operation::Extract);
    if(operation == Operation::Copy) {
        m_entireSource->setText(tr("Copy entire source folder contents"));
    }
    else if(operation == Operation::Move) {
        m_entireSource->setText(tr("Move entire source folder contents"));
    }
    else if(operation == Operation::Extract) {
        m_entireSource->setText(tr("Extract entire source archive contents"));
    }

    updateOptionState();
    populatePresets();
    simulateOp();
}

void FileOpsDialogPrivate::updateOptionState() const
{
    m_removeEmpty->setVisible(m_operation != Operation::Extract);
    m_removeArchive->setVisible(m_operation == Operation::Extract);
    m_removeArchive->setEnabled(m_entireSource->isVisible() && m_entireSource->isChecked());

    if(m_operation != Operation::Extract || !m_entireSource->isChecked()) {
        m_removeArchive->setChecked(false);
    }
}

void FileOpsDialogPrivate::updateButtonState() const
{
    m_loadButton->setEnabled(m_presetBox->findText(m_presetBox->currentText()) >= 0);
    m_saveButton->setEnabled(!m_presetBox->currentText().isEmpty());
    m_deleteButton->setEnabled(m_presetBox->findText(m_presetBox->currentText()) >= 0);
}

FileOpPreset FileOpsDialogPrivate::currentPreset() const
{
    FileOpPreset preset;
    preset.op = m_operation;

    preset.dest        = m_destination->currentText();
    preset.filename    = m_filename->text();
    preset.wholeDir    = m_entireSource->isChecked();
    preset.removeEmpty = m_removeEmpty->isChecked();
    preset.removeSourceArchive
        = m_operation == Operation::Extract && m_entireSource->isChecked() && m_removeArchive->isChecked();

    return preset;
}

void FileOpsDialogPrivate::savePreset()
{
    const QString name = m_presetBox->currentText();

    FileOpPreset preset = currentPreset();
    preset.name         = name;

    auto existingPreset = findPreset(name);
    if(existingPreset != m_presets.cend()) {
        QMessageBox msg{QMessageBox::Question, tr("Preset already exists"),
                        tr("A preset named \"%1\" already exists. Overwrite it?").arg(name),
                        QMessageBox::Yes | QMessageBox::No};
        if(msg.exec() == QMessageBox::Yes) {
            *existingPreset = preset;
            m_presetBox->setCurrentIndex(m_presetBox->findText(name));
        }
    }
    else {
        m_presets.push_back(preset);
        m_presetBox->addItem(preset.name);
        m_presetBox->setCurrentIndex(m_presetBox->count() - 1);
        updateButtonState();
    }

    m_presetsChanged = true;
}

void FileOpsDialogPrivate::loadPreset()
{
    if(m_presetBox->count() == 0) {
        return;
    }

    m_self->loadPreset(m_presetBox->currentText());
}

void FileOpsDialogPrivate::deletePreset()
{
    if(m_presetBox->count() == 0) {
        return;
    }

    const QString name = m_presetBox->currentText();

    auto preset = findPreset(name);
    if(preset != m_presets.cend()) {
        m_presets.erase(preset);
        m_presetBox->removeItem(m_presetBox->findText(name));
    }

    m_presetsChanged = true;
}

void FileOpsDialogPrivate::saveCurrentPreset() const
{
    QByteArray byteArray;
    QDataStream stream(&byteArray, QIODevice::WriteOnly);
    stream.setVersion(QDataStream::Qt_6_0);

    stream << currentPreset();

    byteArray = qCompress(byteArray, 9);

    m_settings->fileSet(CurrentPreset, byteArray);
}

void FileOpsDialogPrivate::loadCurrentPreset() const
{
    auto byteArray = m_settings->fileValue(CurrentPreset).toByteArray();

    if(!byteArray.isEmpty()) {
        byteArray = qUncompress(byteArray);

        QDataStream stream(&byteArray, QIODevice::ReadOnly);
        stream.setVersion(QDataStream::Qt_6_0);

        FileOpPreset preset;
        stream >> preset;

        m_destination->setCurrentIndex(-1);
        m_destination->setCurrentText(preset.dest);
        m_filename->setText(preset.filename);
        m_entireSource->setChecked(preset.wholeDir);
        m_removeEmpty->setChecked(preset.removeEmpty);
        m_removeArchive->setChecked(preset.removeSourceArchive);
        updateOptionState();

        simulateOp();
    }
}

void FileOpsDialogPrivate::loadPresets()
{
    m_presets = FileOps::getPresets();
    populatePresets();
}

void FileOpsDialogPrivate::populatePresets()
{
    m_presetBox->clear();

    for(const auto& preset : m_presets) {
        if(preset.op == m_operation) {
            m_presetBox->addItem(preset.name);
        }
    }
}

void FileOpsDialogPrivate::simulateOp() const
{
    if(m_loading) {
        return;
    }

    m_model->simulate(currentPreset());

    m_status->setText(FileOpsDialog::tr("Determining operations…"));
}

void FileOpsDialogPrivate::toggleRun()
{
    if(m_running) {
        m_runButton->setText(tr("&Run"));
        m_model->stop();
    }
    else {
        const FileOpPreset preset = currentPreset();
        if(preset.removeSourceArchive) {
            const bool immediateDelete = m_settings->fileValue(Settings::ImmediateDelete, false).toBool();
            const QString message
                = immediateDelete
                    ? tr("Source archive files will be permanently deleted after every file from each archive has "
                         "been extracted. Continue?")
                    : tr("Source archive files will be moved to the trash after every file from each archive has "
                         "been extracted. Continue?");
            const auto result = QMessageBox::question(m_self, tr("Delete source archive after extraction?"), message,
                                                      QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
            if(result != QMessageBox::Yes) {
                return;
            }
        }

        m_runButton->setText(tr("&Abort"));
        m_model->run();
    }

    m_running = !m_running;
}

void FileOpsDialogPrivate::modelUpdated()
{
    const int opCount = m_model->rowCount({});

    if(opCount == 0) {
        m_runButton->setText(tr("&Run"));
        m_running = false;
        m_status->setText(FileOpsDialog::tr("Nothing to do"));
        m_runButton->setEnabled(false);
    }
    else {
        m_status->setText(FileOpsDialog::tr("Pending operation(s): %Ln", nullptr, opCount));
        m_runButton->setEnabled(true);
    }
}

void FileOpsDialogPrivate::populateDestinationOptions() const
{
    m_destination->clear();

    // Populate with library roots
    if(m_libraryManager->hasLibrary()) {
        const auto& libs = m_libraryManager->allLibraries();
        for(const auto& entry : libs) {
            const QString path = entry.second.path;
            if(path.isEmpty()) {
                continue;
            }
            if(m_destination->findText(path) == -1) {
                m_destination->addItem(path);
            }
        }
    }
}

void FileOpsDialogPrivate::browseDestination() const
{
    const QString start = m_destination->currentText();
    const QString path  = !start.isEmpty() ? start : QDir::homePath();
    const QString dir   = QFileDialog::getExistingDirectory(m_self, FileOpsDialog::tr("Select Directory"), path,
                                                            QFileDialog::DontResolveSymlinks);
    if(!dir.isEmpty()) {
        m_destination->setCurrentIndex(-1);
        m_destination->setCurrentText(dir);
    }
}

std::vector<FileOpPreset>::iterator FileOpsDialogPrivate::findPreset(const QString& name)
{
    return std::ranges::find_if(
        m_presets, [this, &name](const auto& preset) { return preset.op == m_operation && preset.name == name; });
}

FileOpsDialog::FileOpsDialog(MusicLibrary* library, std::shared_ptr<AudioLoader> audioLoader, const TrackList& tracks,
                             Operation operation, SettingsManager* settings, LibraryManager* libraryManager,
                             QWidget* parent)
    : QDialog{parent}
    , p{std::make_unique<FileOpsDialogPrivate>(this, library, tracks, operation, std::move(audioLoader), settings,
                                               libraryManager)}
{
    setWindowTitle(tr("File Operation"));
    setModal(true);

    p->setup();
}

void FileOpsDialog::loadPreset(const QString& name)
{
    if(name.isEmpty()) {
        return;
    }

    p->m_loading = true;

    auto preset = p->findPreset(name);
    if(preset != p->m_presets.cend()) {
        p->m_presetBox->setCurrentText(name);

        p->m_destination->setCurrentIndex(-1);
        p->m_destination->setCurrentText(preset->dest);
        p->m_filename->setText(preset->filename);
        p->m_entireSource->setChecked(preset->wholeDir);
        p->m_removeEmpty->setChecked(preset->removeEmpty);
        p->m_removeArchive->setChecked(preset->removeSourceArchive);

        p->updateOptionState();
    }

    p->m_loading = false;
    p->simulateOp();
}

void FileOpsDialog::done(int value)
{
    p->saveCurrentPreset();
    FileOps::savePresets(p->m_presets);

    if(p->m_presetsChanged) {
        Q_EMIT presetsChanged();
    }

    QDialog::done(value);
}
} // namespace Fooyin::FileOps

#include "fileopsdialog.moc"
#include "moc_fileopsdialog.cpp"
