#include "./settingsdialog.h"

#include "./optioncategory.h"
#include "./optioncategoryfiltermodel.h"
#include "./optioncategorymodel.h"
#include "./optionpage.h"

#include "../misc/dialogutils.h"

#include "ui_settingsdialog.h"

#include <QItemSelectionModel>
#include <QMessageBox>
#include <QScrollArea>
#include <QShowEvent>
#include <QStringBuilder>

namespace QtUtilities {

/*!
 * \class SettingsDialog
 * \brief The SettingsDialog class provides a framework for creating settings
 * dialogs with different categories and subcategories.
 */

/*!
 * \brief Constructs a settings dialog.
 * \param parent Specifies the parent widget.
 */
SettingsDialog::SettingsDialog(QWidget *parent)
    : QDialog(parent)
    , m_ui(new Ui::SettingsDialog)
    , m_categoryModel(new OptionCategoryModel(this))
    , m_categoryFilterModel(new OptionCategoryFilterModel(this))
    , m_currentCategory(nullptr)
    , m_tabBarAlwaysVisible(true)
{
    m_ui->setupUi(this);
    makeHeading(m_ui->headingLabel);
    setStyleSheet(dialogStyleForPalette(palette()));

    // setup models
    m_categoryFilterModel->setSourceModel(m_categoryModel);
    m_ui->categoriesListView->setModel(m_categoryFilterModel);

    // connect signals and slots
    //  selection models
    connect(m_ui->categoriesListView->selectionModel(), &QItemSelectionModel::currentChanged, this, &SettingsDialog::currentCategoryChanged);
    //  buttons
    connect(m_ui->abortPushButton, &QPushButton::clicked, this, &SettingsDialog::reject);
    connect(m_ui->applyPushButton, &QPushButton::clicked, this, &SettingsDialog::apply);
    connect(m_ui->okPushButton, &QPushButton::clicked, this, &SettingsDialog::accept);
    //  dialog
    connect(this, &SettingsDialog::accepted, this, &SettingsDialog::apply);
    connect(this, &SettingsDialog::rejected, this, &SettingsDialog::reset);
    //  misc
    connect(m_ui->filterLineEdit, &QLineEdit::textChanged, m_categoryFilterModel, &OptionCategoryFilterModel::setFilterFixedString);
    connect(m_ui->filterLineEdit, &QLineEdit::textChanged, this, &SettingsDialog::updateTabWidget);
}

/*!
 * \brief Destroys the settings dialog.
 */
SettingsDialog::~SettingsDialog()
{
}

/*!
 * \brief Sets whether the tab bar is always visible.
 * \sa SettingsDialog::isTabBarAlwaysVisible()
 */
void SettingsDialog::setTabBarAlwaysVisible(bool value)
{
    m_tabBarAlwaysVisible = value;
    if (m_currentCategory) {
        m_ui->pagesTabWidget->tabBar()->setHidden(!value && m_currentCategory->pages().size() == 1);
    }
}

/*!
 * \brief Returns the category for the specified \a categoryIndex.
 *
 * The settings dialog keeps ownership over the returned category.
 * If no category exists for the specified index a null pointer is returned.
 */
OptionCategory *SettingsDialog::category(int categoryIndex) const
{
    return m_categoryModel->category(categoryIndex);
}

/*!
 * \brief Returns the page for the specified \a categoryIndex and the specified
 * \a pageIndex.
 *
 * The settings dialog keeps ownership over the returned category.
 * If no page for the specified indices a null pointer is returned.
 */
OptionPage *SettingsDialog::page(int categoryIndex, int pageIndex) const
{
    if (OptionCategory *const category = this->category(categoryIndex)) {
        if (pageIndex < category->pages().length()) {
            return category->pages()[pageIndex];
        }
    }
    return nullptr;
}

/*!
 * \brief Resets all pages before the dialog is shown by the application.
 */
void SettingsDialog::showEvent(QShowEvent *event)
{
    if (event->spontaneous()) {
        return;
    }
    for (OptionCategory *const category : m_categoryModel->categories()) {
        for (OptionPage *const page : category->pages()) {
            if (page->hasBeenShown()) {
                page->reset();
            }
        }
    }
}

/*!
 * \brief Shows the selected category specified by its model \a index in the
 * category filter model.
 *
 * This private slot is called when
 * m_ui->categoriesListView->selectionModel()->currentChanged() is emitted.
 */
void SettingsDialog::currentCategoryChanged(const QModelIndex &index)
{
    showCategory(m_categoryModel->category(m_categoryFilterModel->mapToSource(index)));
}

/*!
 * \brief Sets the current category to the specified \a category and updates the
 * relevant widgets to show it.
 */
void SettingsDialog::showCategory(OptionCategory *category)
{
    if (m_currentCategory) {
        m_currentCategory->setCurrentIndex(m_ui->pagesTabWidget->currentIndex());
    }
    if (category) {
        if (m_currentCategory != category) {
            m_currentCategory = category;
            m_ui->headingLabel->setText(category->displayName());
        }
    } else {
        m_currentCategory = nullptr;
        m_ui->headingLabel->setText(tr("No category selected"));
    }
    updateTabWidget();
}

/*!
 * \brief Allows to set the \a categories display name so that it is retranslated as needed.
 * \remarks
 * - The specified \a translator is supposed to return the display name to assign to \a category for the current
 *   locale. The \a translator is called immediately for the initial assignment and on language change events.
 * - This function is experimental and might change or be removed completely in the next minor release.
 */
void SettingsDialog::translateCategory(OptionCategory *category, const std::function<QString()> &translator)
{
    category->setDisplayName(translator());
    connect(this, &SettingsDialog::retranslationRequired, category,
        [category, translator = std::move(translator)] { category->setDisplayName(translator()); });
}

/*!
 * \brief Enables *single-category mode* to show only the specified \a
 * singleCategory.
 * \remarks
 * - In *single-category mode* category selection, filter and heading are
 * hidden.
 * - The *single-category mode* can be disabled again by setting \a
 * singleCategory to nullptr.
 */
void SettingsDialog::setSingleCategory(OptionCategory *singleCategory)
{
    const bool hasSingleCategory = singleCategory != nullptr;
    m_ui->filterLineEdit->setHidden(hasSingleCategory);
    m_ui->categoriesListView->setHidden(hasSingleCategory);
    m_ui->headingLabel->setHidden(hasSingleCategory);
    if (hasSingleCategory) {
        m_ui->filterLineEdit->clear();
        categoryModel()->setCategories(QList<OptionCategory *>({ singleCategory }));
        showCategory(singleCategory);
    }
}

/*!
 * \brief Returns the tab-widget's corner widget.
 */
QWidget *SettingsDialog::cornerWidget(Qt::Corner corner) const
{
    return m_ui->pagesTabWidget->cornerWidget(corner);
}

/*!
 * \brief Sets the tab-widget's corner widget.
 */
void SettingsDialog::setCornerWidget(QWidget *widget, Qt::Corner corner)
{
    m_ui->pagesTabWidget->setCornerWidget(widget, corner);
}

/*!
 * \brief Adds a widget next to the heading.
 */
void SettingsDialog::addHeadingWidget(QWidget *widget)
{
    m_ui->headingLayout->addWidget(widget);
}

/*!
 * \brief Selects the specified page within the specified category.
 */
void SettingsDialog::selectPage(int categoryIndex, int pageIndex)
{
    m_categoryFilterModel->setFilterFixedString(QString());
    m_ui->filterLineEdit->clear();
    showCategory(m_categoryModel->category(categoryIndex));
    m_ui->categoriesListView->selectionModel()->select(
        m_categoryFilterModel->mapFromSource(m_categoryModel->index(categoryIndex)), QItemSelectionModel::ClearAndSelect);
    m_ui->pagesTabWidget->setCurrentIndex(pageIndex);
}

/*!
 * \brief Updates the tab widget to show the pages for the current category.
 */
void SettingsDialog::updateTabWidget()
{
    if (!m_currentCategory) {
        m_ui->pagesTabWidget->clear();
        return;
    }
    m_ui->pagesTabWidget->setUpdatesEnabled(false);

    const auto searchKeyWord = m_ui->filterLineEdit->text();
    int index = 0, pageIndex = 0;
    for (OptionPage *const page : m_currentCategory->pages()) {
        if (page->matches(searchKeyWord)) {
            // ensure the page's widget has no parent anymore; otherwise windowIcon() might return the parent's icon
            auto *const widget = page->widget();
            widget->setParent(nullptr);

            // add the widget to the tab widget within a scroll area
            QScrollArea *scrollArea;
            if (index < m_ui->pagesTabWidget->count()) {
                scrollArea = qobject_cast<QScrollArea *>(m_ui->pagesTabWidget->widget(index));
                scrollArea->takeWidget();
                m_ui->pagesTabWidget->setTabText(index, widget->windowTitle());
                m_ui->pagesTabWidget->setTabIcon(index, widget->windowIcon());
            } else {
                scrollArea = new QScrollArea(m_ui->pagesTabWidget);
                scrollArea->setFrameStyle(QFrame::NoFrame);
                scrollArea->setBackgroundRole(QPalette::Base);
                scrollArea->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
                scrollArea->setWidgetResizable(true);
                m_ui->pagesTabWidget->addTab(scrollArea, widget->windowTitle());
                m_ui->pagesTabWidget->setTabIcon(index, widget->windowIcon());
            }
            if (auto *const layout = widget->layout()) {
                layout->setAlignment(Qt::AlignTop | Qt::AlignLeft);
            }
            scrollArea->setWidget(widget);
            ++index;
        }
        if (pageIndex == m_currentCategory->currentIndex()) {
            m_ui->pagesTabWidget->setCurrentIndex(pageIndex);
        }
        ++pageIndex;
    }

    // remove surplus tabs
    while (index < m_ui->pagesTabWidget->count()) {
        auto *const scrollArea = qobject_cast<QScrollArea *>(m_ui->pagesTabWidget->widget(index));
        scrollArea->takeWidget();
        m_ui->pagesTabWidget->removeTab(index);
        delete scrollArea;
    }

    m_ui->pagesTabWidget->tabBar()->setHidden(!m_tabBarAlwaysVisible && m_ui->pagesTabWidget->count() == 1);
    m_ui->pagesTabWidget->setUpdatesEnabled(true);
}

/*!
 * \brief Updates the tab widget's tab texts to apply possible translation changes.
 */
void SettingsDialog::retranslateTabWidget()
{
    for (auto index = 0; index < m_ui->pagesTabWidget->count(); ++index) {
        const auto *const scrollArea = qobject_cast<const QScrollArea *>(m_ui->pagesTabWidget->widget(index));
        const auto *const widget = scrollArea->widget();
        m_ui->pagesTabWidget->setTabText(index, widget->windowTitle());
    }
}

/*!
 * \brief Applies all changes. Calls OptionCategory::applyAllPages() for each category.
 * \remarks Pages which have not been shown yet must have not been initialized anyways
 *          and hence are skipped.
 */
bool SettingsDialog::apply()
{
    // apply each page in each category and gather error messages
    QString errorMessage;
    for (OptionCategory *const category : m_categoryModel->categories()) {
        for (OptionPage *const page : category->pages()) {
            if (!page->hasBeenShown() || page->apply()) {
                // nothing to apply or no error
                continue;
            }

            // add error message
            if (errorMessage.isEmpty()) {
                errorMessage = tr("<p><b>Errors occurred when applying changes:</b></p><ul>");
            }
            QStringList &errors = const_cast<OptionPage *>(page)->errors();
            if (errors.isEmpty()) {
                errorMessage.append(QStringLiteral("<li><i>") % category->displayName() % QLatin1Char('/') % page->widget()->windowTitle()
                    % QStringLiteral("</i>: ") % tr("unknown error") % QStringLiteral("</li>"));
            } else {
                for (const QString &error : errors) {
                    errorMessage.append(QStringLiteral("<li><i>") % category->displayName() % QLatin1Char('/') % page->widget()->windowTitle()
                        % QStringLiteral("</i>: ") % error % QStringLiteral("</li>"));
                }
                errors.clear();
            }
        }
    }

    // show error messages (if errors occurred)
    if (!errorMessage.isEmpty()) {
        errorMessage.append(QStringLiteral("</ul>"));
        QMessageBox::warning(this, windowTitle(), errorMessage);
    }

    // return status
    emit applied();
    return errorMessage.isEmpty();
}

/*!
 * \brief Resets all changes. Calls OptionCategory::resetAllPages() for each
 * category.
 */
void SettingsDialog::reset()
{
    for (OptionCategory *const category : m_categoryModel->categories()) {
        category->resetAllPages();
    }
    emit resetted();
}

bool SettingsDialog::event(QEvent *event)
{
    const auto res = QDialog::event(event);
    switch (event->type()) {
    case QEvent::PaletteChange:
        setStyleSheet(dialogStyleForPalette(palette()));
        break;
    case QEvent::LanguageChange:
        m_ui->retranslateUi(this);
        retranslateTabWidget();
        emit retranslationRequired();
        break;
    default:;
    }
    return res;
}

} // namespace QtUtilities
