/* SPDX-License-Identifier: GPL-3.0-or-later
 * SPDX-FileCopyrightText: 2023-2024, 2026 Soren Stoutner <soren@stoutner.com>
 *
 * This file is part of Privacy Browser PC <https://www.stoutner.com/privacy-browser-pc/>.
 *
 * This program 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.
 *
 * This program 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
 * this program.  If not, see <https://www.gnu.org/licenses/>.
 */

// Application headers.
#include "BookmarksDialog.h"
#include "ui_BookmarksDialog.h"
#include "databases/BookmarksDatabase.h"
#include "dialogs/AddBookmarkDialog.h"
#include "dialogs/AddFolderDialog.h"
#include "dialogs/EditBookmarkDialog.h"
#include "dialogs/EditFolderDialog.h"

// KDE Frameworks headers.
#include <KLocalizedString>

// Qt toolkit headers.
#include <QDebug>
#include <QScrollBar>
#include <QStandardItemModel>

// Construct the class.
BookmarksDialog::BookmarksDialog(QWidget *parentWidgetPointer, QIcon currentWebsiteFavorieIcon, QString currentWebsiteTitle, QString currentWebsiteUrl) :
                                 QDialog(parentWidgetPointer), websiteFavoriteIcon(currentWebsiteFavorieIcon), websiteTitle(currentWebsiteTitle), websiteUrl(currentWebsiteUrl)
{
    // Set the dialog window title.
    setWindowTitle(i18nc("The bookmarks dialog window title", "Bookmarks"));

    // Set the window modality.
    setWindowModality(Qt::WindowModality::ApplicationModal);

    // Instantiate the bookmarks settings dialog UI.
    Ui::BookmarksDialog bookmarksDialogUi;

    // Setup the UI.
    bookmarksDialogUi.setupUi(this);

    // Get a handle for the draggable tree view.
    draggableTreeViewPointer = bookmarksDialogUi.draggableTreeView;

    // Initialize the tree model.
    treeModelPointer = new QStandardItemModel();

    // Auto resize the headers.
    draggableTreeViewPointer->header()->setSectionResizeMode(QHeaderView::ResizeToContents);

    // Indicate that all the rows are the same height, which improves performance.
    draggableTreeViewPointer->setUniformRowHeights(true);

    // Set the selection mode to allow multiple rows to be selected at once.
    draggableTreeViewPointer->setSelectionMode(QAbstractItemView::ExtendedSelection);

    // Allow dragging of bookmarks to reorder.
    draggableTreeViewPointer->setDragDropMode(QAbstractItemView::InternalMove);

    // Set the tree model.
    draggableTreeViewPointer->setModel(treeModelPointer);

    // Get a handle for the tree selection model.
    treeSelectionModelPointer = draggableTreeViewPointer->selectionModel();

    // Listen for selection changes.
    connect(treeSelectionModelPointer, SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(updateSelection()));

    // Repopulate the bookmarks when they are moved.
    connect(draggableTreeViewPointer, SIGNAL(bookmarksMoved()), this, SLOT(refreshBookmarks()));

    // Get handles for the buttons.
    QPushButton *addBookmarkButtonPointer = bookmarksDialogUi.addBookmarkButton;
    QPushButton *addFolderButtonPointer = bookmarksDialogUi.addFolderButton;
    editButtonPointer = bookmarksDialogUi.editButton;
    deleteItemsButtonPointer = bookmarksDialogUi.deleteItemsButton;
    QDialogButtonBox *dialogButtonBoxPointer = bookmarksDialogUi.dialogButtonBox;
    QPushButton *closeButtonPointer = dialogButtonBoxPointer->button(QDialogButtonBox::Close);

    // Connect the buttons.
    connect(addBookmarkButtonPointer, SIGNAL(clicked()), this, SLOT(showAddBookmarkDialog()));
    connect(addFolderButtonPointer, SIGNAL(clicked()), this, SLOT(showAddFolderDialog()));
    connect(editButtonPointer, SIGNAL(clicked()), this, SLOT(showEditDialog()));
    connect(deleteItemsButtonPointer, SIGNAL(clicked()), this, SLOT(deleteItems()));
    connect(dialogButtonBoxPointer, SIGNAL(rejected()), this, SLOT(reject()));

    // Set the close button to be the default.
    closeButtonPointer->setDefault(true);

    // Monitor editing of data in the tree model.
    connect(treeModelPointer, SIGNAL(itemChanged(QStandardItem*)), this, SLOT(updateBookmarkFromTree(QStandardItem*)));

    // Populate the bookmarks.
    populateBookmarks();
}

void BookmarksDialog::deleteItems()
{
    // Create a parent folder ID standard C++ list (which can be sorted and from which duplicates can be removed).
    std::list<double> parentFolderIdList;

    // Get the list of selected model indexes.
    QList<QModelIndex> selectedModelIndexList = treeSelectionModelPointer->selectedRows(DATABASE_ID_COLUMN);

    // Delete each of the bookmarks.
    for (const QModelIndex &modelIndex : selectedModelIndexList)
    {
        // Add the parent folder ID to the list.
        parentFolderIdList.push_back(modelIndex.parent().siblingAtColumn(FOLDER_ID_COLUMN).data().toDouble());

        // Delete the bookmark.
        BookmarksDatabase::deleteBookmark(modelIndex.data().toInt());
    }

    // Sort the parent folder ID.
    parentFolderIdList.sort();

    // Remove duplicates from the parent folder ID list.
    parentFolderIdList.unique();

    // Update the folder contents display order for each folder with a deleted bookmark.
    for (const double parentFolderId : parentFolderIdList)
        BookmarksDatabase::updateFolderContentsDisplayOrder(parentFolderId);

    // Repopulate the bookmarks in this dialog
    populateBookmarks();

    // Emit the bookmark updated signal to redraw the bookmarks in the menu and toolbar.
    Q_EMIT bookmarkUpdated();
}

QList<int> BookmarksDialog::getExpandedFoldersDatabaseIdList(QModelIndex parentIndex) const
{
    // Create a list of expanded folder database ids.
    QList<int> expandedFoldersDatabaseIdsList;

    // Get a count of the number of children rows of the parent index.
    int childrenRows = treeModelPointer->rowCount(parentIndex);

    // Check each child row.
    for (int i = 0; i < childrenRows; ++i)
    {
        // Get the child row model index.
        QModelIndex childModelIndex = treeModelPointer->index(i, NAME_COLUMN, parentIndex);

        // Check if the child model index is a folder.
        bool isFolder = childModelIndex.siblingAtColumn(IS_FOLDER_COLUMN).data().toBool();

        // Check if the child model index is expanded.
        if (isFolder && draggableTreeViewPointer->isExpanded(childModelIndex))
        {
            // Append the database ID to the list of expanded folders.
            expandedFoldersDatabaseIdsList.append(childModelIndex.siblingAtColumn(DATABASE_ID_COLUMN).data().toInt());

            // Check for subfolders.
            expandedFoldersDatabaseIdsList.append(getExpandedFoldersDatabaseIdList(childModelIndex));
        }
    }

    // Return the list of the expanded folders database IDs.
    return expandedFoldersDatabaseIdsList;
}

void BookmarksDialog::populateBookmarks()
{
    // Store the current vertical scroll value.
    verticalScrollValue = draggableTreeViewPointer->verticalScrollBar()->value();

    // Get a list of the database IDs of all of the currently expanded folders.
    QList<int> currentlyExpandedFoldersDatabaseIdsList = getExpandedFoldersDatabaseIdList(draggableTreeViewPointer->rootIndex());

    // Clear the current contents of the tree model.
    treeModelPointer->clear();

    // Set the column count.
    treeModelPointer->setColumnCount(6);

    // Set the tree header data.
    treeModelPointer->setHeaderData(NAME_COLUMN, Qt::Horizontal, i18nc("The bookmark Name header.", "Name"));
    treeModelPointer->setHeaderData(URL_COLUMN, Qt::Horizontal, i18nc("The bookmark URL header.", "URL"));

    // Hide the backend columns.
    draggableTreeViewPointer->setColumnHidden(DATABASE_ID_COLUMN, true);
    draggableTreeViewPointer->setColumnHidden(DISPLAY_ORDER_COLUMN, true);
    draggableTreeViewPointer->setColumnHidden(IS_FOLDER_COLUMN, true);
    draggableTreeViewPointer->setColumnHidden(FOLDER_ID_COLUMN, true);

    // Create a bookmarks root item list.
    QList<QStandardItem*> bookmarksRootItemList;

    // Create the root items.
    QStandardItem *rootItemNamePointer = new QStandardItem(QIcon::fromTheme(QLatin1String("bookmarks"), QIcon::fromTheme(QLatin1String("bookmark-new"))),
                                                           i18nc("The bookmarks root tree widget name", "Bookmarks"));
    QStandardItem *rootItemUrlPointer = new QStandardItem(QLatin1String(""));
    QStandardItem *rootItemDatabaseIdPointer = new QStandardItem(QLatin1String("-1"));  // The root item doesn't have a database ID.
    QStandardItem *rootItemDisplayOrderPointer = new QStandardItem(QLatin1String("-1"));  // The root item doesn't have a display order.
    QStandardItem *rootItemIsFolderPointer = new QStandardItem(QLatin1String("1"));
    QStandardItem *rootItemFolderIdPointer = new QStandardItem(QLatin1String("0"));

    // Enable dropping on the root name column.
    rootItemNamePointer->setDropEnabled(true);

    // Disable dropping on the URL.
    rootItemUrlPointer->setDropEnabled(false);

    // Disable dragging of the bookmarks root item.
    rootItemNamePointer->setDragEnabled(false);
    rootItemUrlPointer->setDragEnabled(false);

    // Disable selecting the URL.
    rootItemUrlPointer->setSelectable(false);

    // Populate the bookmarks root item list.
    bookmarksRootItemList.append(rootItemNamePointer);
    bookmarksRootItemList.append(rootItemUrlPointer);
    bookmarksRootItemList.append(rootItemDatabaseIdPointer);
    bookmarksRootItemList.append(rootItemDisplayOrderPointer);
    bookmarksRootItemList.append(rootItemIsFolderPointer);
    bookmarksRootItemList.append(rootItemFolderIdPointer);

    // Add the bookmarks root item to the tree.
    treeModelPointer->appendRow(bookmarksRootItemList);

    // Populate the subfolders, starting with the root folder ID (`0`).
    populateSubfolders(rootItemNamePointer, 0, currentlyExpandedFoldersDatabaseIdsList);

    //  Restore the vertical scroll value after the draggable tree view is populated, which changes the range of the scroll bar.
    restoreVerticalScrollBarConnection = connect(draggableTreeViewPointer->verticalScrollBar(), SIGNAL(rangeChanged(int, int)), this, SLOT(restoreVerticalScrollBarPosition(int, int)));

    // Expand the root database folder.
    draggableTreeViewPointer->setExpanded(treeModelPointer->indexFromItem(rootItemNamePointer), true);

    // Update the UI.
    updateUi();
}

void BookmarksDialog::populateSubfolders(QStandardItem *folderItemNamePointer, const double folderId, QList<int> foldersToExpandDatabaseIdList) const
{
    // Get the folder contents.
    QList<BookmarkStruct> *folderContentsListPointer = BookmarksDatabase::getFolderContents(folderId);

    // Populate the bookmarks tree view.
    for (const BookmarkStruct &bookmarkStruct : *folderContentsListPointer)
    {
        // Create a bookmark item list.
        QList<QStandardItem*> bookmarkItemList;

        // Create the bookmark items.
        QStandardItem *nameItemPointer = new QStandardItem(bookmarkStruct.favoriteIcon, bookmarkStruct.name);
        QStandardItem *urlItemPointer = new QStandardItem(bookmarkStruct.url);
        QStandardItem *databaseIdItemPointer = new QStandardItem(QString::number(bookmarkStruct.databaseId));
        QStandardItem *displayOrderItemPointer = new QStandardItem(QString::number(bookmarkStruct.displayOrder));
        QStandardItem *isFolderItemPointer = new QStandardItem(QString::number(bookmarkStruct.isFolder));
        QStandardItem *folderIdItemPointer = new QStandardItem(QString::number(bookmarkStruct.folderId, 'f', 0));  // Format the folder ID as a floating point with no trailing zeros.

        // Enable dragging and dropping of the name column.
        nameItemPointer->setDragEnabled(true);

        // Only allow dropping on the name if this is a folder.
        nameItemPointer->setDropEnabled(bookmarkStruct.isFolder);

        // Disable dragging and dropping on the URL.
        urlItemPointer->setDragEnabled(false);
        urlItemPointer->setDropEnabled(false);

        // Disable selecting the URL.
        urlItemPointer->setSelectable(false);

        // Populate the bookmark item list.
        bookmarkItemList.append(nameItemPointer);
        bookmarkItemList.append(urlItemPointer);
        bookmarkItemList.append(databaseIdItemPointer);
        bookmarkItemList.append(displayOrderItemPointer);
        bookmarkItemList.append(isFolderItemPointer);
        bookmarkItemList.append(folderIdItemPointer);

        // Add the bookmark to the parent folder.
        folderItemNamePointer->appendRow(bookmarkItemList);

        // Expand any folders that were previously expanded.
        if (bookmarkStruct.isFolder && foldersToExpandDatabaseIdList.contains(bookmarkStruct.databaseId))
            draggableTreeViewPointer->setExpanded(treeModelPointer->indexFromItem(nameItemPointer), true);

        // Populate subfolders if this item is a folder.
        if (bookmarkStruct.isFolder)
            populateSubfolders(nameItemPointer, bookmarkStruct.folderId, foldersToExpandDatabaseIdList);
    }
}

void BookmarksDialog::refreshBookmarks()
{
    // Repopulate the bookmarks in this dialog
    populateBookmarks();

    // Emit the bookmark updated signal to redraw the bookmarks in the menu and toolbar.
    Q_EMIT bookmarkUpdated();
}

void BookmarksDialog::restoreVerticalScrollBarPosition(int, int) const
{
    // Restore the vertical scroll value.
    draggableTreeViewPointer->verticalScrollBar()->setValue(verticalScrollValue);

    //  Disconnect the scroll bar updater.  It should only run once each time the bookmarks are populated.
    disconnect(restoreVerticalScrollBarConnection);
}


void BookmarksDialog::selectSubfolderContents(const QModelIndex &parentModelIndex) const
{
    // Get the index model.
    const QAbstractItemModel *modelIndexAbstractItemPointer = parentModelIndex.model();

    // Get the number of items in the folder.
    int numberOfChildrenInFolder = modelIndexAbstractItemPointer->rowCount(parentModelIndex);

    // Select any child items.
    if (numberOfChildrenInFolder > 0)
    {
        // Select the contents of any subfolders.
        for (int i = 0; i < numberOfChildrenInFolder; ++i)
        {
            // Get the child model index.
            QModelIndex childModelIndex = modelIndexAbstractItemPointer->index(i, 0, parentModelIndex);

            // Select the subfolder contents if it is a folder.
            if (childModelIndex.siblingAtColumn(IS_FOLDER_COLUMN).data().toBool())
                selectSubfolderContents(childModelIndex);
        }

        // Get the first and last child model indexes.
        QModelIndex firstChildModelIndex = modelIndexAbstractItemPointer->index(0, 0, parentModelIndex);
        QModelIndex lastChildModelIndex = modelIndexAbstractItemPointer->index((numberOfChildrenInFolder - 1), 0, parentModelIndex);

        // Create an item selection that includes all the child items.
        QItemSelection folderChildItemsSelection = QItemSelection(firstChildModelIndex, lastChildModelIndex);

        // Get the current selection.
        QItemSelection currentSelection = treeSelectionModelPointer->selection();

        // Combine the current selection and the folder child items selection.
        currentSelection.merge(folderChildItemsSelection, QItemSelectionModel::SelectCurrent);

        // Selected the updated list of items.
        treeSelectionModelPointer->select(currentSelection, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
    }
}

void BookmarksDialog::showAddBookmarkDialog()
{
    // Return the most recently selected index.
    QModelIndex currentIndex = treeSelectionModelPointer->currentIndex();

    // Instantiate a parent folder ID.
    double parentFolderId;

    // Get the parent folder ID.
    if (currentIndex.siblingAtColumn(IS_FOLDER_COLUMN).data().toInt() == 1)  // The current index is a folder.
    {
        // Store the parent folder ID.
        parentFolderId = currentIndex.siblingAtColumn(FOLDER_ID_COLUMN).data().toDouble();
    }
    else  // The current index is not a folder.
    {
        // Store the parent folder ID of the folder that contains the bookmark.
        parentFolderId = currentIndex.parent().siblingAtColumn(FOLDER_ID_COLUMN).data().toDouble();
    }

    // Instantiate an add bookmark dialog.
    AddBookmarkDialog *addBookmarkDialogPointer = new AddBookmarkDialog(this, websiteTitle, websiteUrl, websiteFavoriteIcon, parentFolderId);

    // Update the displayed bookmarks when a new one is added.
    connect(addBookmarkDialogPointer, SIGNAL(bookmarkAdded()), this, SLOT(refreshBookmarks()));

    // Show the dialog.
    addBookmarkDialogPointer->show();
}

void BookmarksDialog::showAddFolderDialog()
{
    // Get the most recently selected index.
    QModelIndex currentIndex = treeSelectionModelPointer->currentIndex();

    // Instantiate a parent folder ID.
    double parentFolderId;

    // Get the parent folder ID.
    if (currentIndex.siblingAtColumn(IS_FOLDER_COLUMN).data().toInt() == 1)  // The current index is a folder.
    {
        // Store the parent folder ID.
        parentFolderId = currentIndex.siblingAtColumn(FOLDER_ID_COLUMN).data().toDouble();
    }
    else  // The current index is not a folder.
    {
        // Store the parent folder ID of the folder that contains the bookmark.
        parentFolderId = currentIndex.parent().siblingAtColumn(FOLDER_ID_COLUMN).data().toDouble();
    }

    // Instantiate an add folder dialog.
    AddFolderDialog *addFolderDialogPointer = new AddFolderDialog(this, websiteFavoriteIcon, parentFolderId);

    // Update the displayed bookmarks when a folder is added.
    connect(addFolderDialogPointer, SIGNAL(folderAdded()), this, SLOT(refreshBookmarks()));

    // Show the dialog.
    addFolderDialogPointer->show();
}

void BookmarksDialog::showEditDialog()
{
    // Get the current model index.
    QModelIndex currentIndex = treeSelectionModelPointer->currentIndex();

    // Check to see if the selected item is a folder.
    if (currentIndex.siblingAtColumn(IS_FOLDER_COLUMN).data().toInt() == 1)  // The selected item is a folder.
    {
        // Instantiate an edit folder dialog.
        QDialog *editFolderDialogPointer = new EditFolderDialog(this, currentIndex.siblingAtColumn(DATABASE_ID_COLUMN).data().toInt(), websiteFavoriteIcon);

        // Show the dialog.
        editFolderDialogPointer->show();

        // Update the bookmarks UI.
        connect(editFolderDialogPointer, SIGNAL(folderSaved()), this, SLOT(refreshBookmarks()));
    }
    else  // The selected item is a bookmark.
    {
        // Instantiate an edit bookmark dialog.
        QDialog *editBookmarkDialogPointer = new EditBookmarkDialog(this, currentIndex.siblingAtColumn(DATABASE_ID_COLUMN).data().toInt(), websiteFavoriteIcon);

        // Show the dialog.
        editBookmarkDialogPointer->show();

        // Update the bookmarks UI.
        connect(editBookmarkDialogPointer, SIGNAL(bookmarkSaved()), this, SLOT(refreshBookmarks()));
    }
}

void BookmarksDialog::updateBookmarkFromTree(QStandardItem *modifiedStandardItem)
{
    // Get the model index of the modified item.
    QModelIndex modifiedItemModelIndex = modifiedStandardItem->index();

    // Get the model index of the database ID.
    QModelIndex databaseIdModelIndex = modifiedItemModelIndex.siblingAtColumn(DATABASE_ID_COLUMN);

    // Get the database ID.
    int databaseId = databaseIdModelIndex.data().toInt();

    // Check to see if the bookmark name or the URL was edited.
    if (modifiedStandardItem->column() == NAME_COLUMN)  // The bookmark name was edited.
    {
        // Update the bookmark name.
        BookmarksDatabase::updateBookmarkName(databaseId, modifiedStandardItem->text());
    }
    else  // The bookmark URL was edited.
    {
        // Update the bookmark URL.
        BookmarksDatabase::updateBookmarkUrl(databaseId, modifiedStandardItem->text());
    }

    // Emit the bookmark updated signal.
    Q_EMIT bookmarkUpdated();
}

void BookmarksDialog::updateSelection() const
{
    // Set the status of the buttons.
    if (treeSelectionModelPointer->hasSelection())  // A bookmark or folder is selected.
    {
        // Get the list of selected model indexes.
        QModelIndexList selectedRowsModelIndexList = treeSelectionModelPointer->selectedRows();

        // Check to see if each selected item is a folder.
        for(QModelIndex modelIndex : selectedRowsModelIndexList)
        {
            // If it is a folder, select all the children bookmarks.
            if (modelIndex.siblingAtColumn(IS_FOLDER_COLUMN).data().toBool())
                selectSubfolderContents(modelIndex);
        }
    }

    // Update the UI.
    updateUi();
}

void BookmarksDialog::updateUi() const
{
    // Set the status of the buttons.
    if (treeSelectionModelPointer->hasSelection())  // A bookmark or folder is selected.
    {
        // Get the currently selected index.
        QModelIndex currentSelectedIndex = treeSelectionModelPointer->currentIndex();

        // Get the list of selected model indexes.
        QModelIndexList selectedRowsModelIndexList = treeSelectionModelPointer->selectedRows();

        // Get the number of selected rows.
        int numberOfSelectedRows = selectedRowsModelIndexList.count();

        // Enable the edit button if a folder or only one bookmark is selected.
        editButtonPointer->setEnabled(currentSelectedIndex.siblingAtColumn(IS_FOLDER_COLUMN).data().toBool() || (numberOfSelectedRows == 1));

        // Check if the root folder is selected.
        if (treeSelectionModelPointer->isRowSelected(0))
        {
            // Disable the edit button.
            editButtonPointer->setEnabled(false);

            // Decrease the number of selected rows by 1.
            --numberOfSelectedRows;
        }

        // Enabled the delete button if at least one real bookmark or folder is selected.
        deleteItemsButtonPointer->setEnabled(numberOfSelectedRows > 0);

        // Update the delete items button text.
        if (numberOfSelectedRows > 0)
            deleteItemsButtonPointer->setText(i18ncp("Delete items button populated text.", "Delete %1 item", "Delete %1 items", numberOfSelectedRows));
        else
            deleteItemsButtonPointer->setText(i18nc("Delete items button default text", "Delete items"));
    }
    else  // Nothing is selected.
    {
        // Disable the buttons.
        editButtonPointer->setEnabled(false);
        deleteItemsButtonPointer->setEnabled(false);

        // Update the delete items button text.
        deleteItemsButtonPointer->setText(i18nc("Delete items button default text", "Delete items"));
    }
}
