/*
 * KFontInst - KDE Font Installer
 *
 * Copyright 2003-2007 Craig Drummond <craig@kde.org>
 *
 * ----
 *
 * 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 2 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; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#include "DuplicatesDialog.h"
#include "ActionLabel.h"
#include "Fc.h"
#include "FcEngine.h"
#include "FontList.h"
#include <KFileItem>
#include <KFormat>
#include <KIconLoader>
#include <KMessageBox>
#include <KPropertiesDialog>
#include <QApplication>
#include <QContextMenuEvent>
#include <QDesktopWidget>
#include <QDialogButtonBox>
#include <QDir>
#include <QFileInfo>
#include <QFileInfoList>
#include <QGridLayout>
#include <QHeaderView>
#include <QLabel>
#include <QMenu>
#include <QMimeDatabase>
#include <QProcess>
#include <QPushButton>
#include <QVBoxLayout>

namespace KFI
{
enum EDialogColumns {
    COL_FILE,
    COL_TRASH,
    COL_SIZE,
    COL_DATE,
    COL_LINK,
};

CDuplicatesDialog::CDuplicatesDialog(QWidget *parent, CFontList *fl)
    : QDialog(parent)
    , itsFontList(fl)
{
    setWindowTitle(i18n("Duplicate Fonts"));
    itsButtonBox = new QDialogButtonBox(QDialogButtonBox::Cancel);
    connect(itsButtonBox, &QDialogButtonBox::clicked, this, &CDuplicatesDialog::slotButtonClicked);
    QVBoxLayout *mainLayout = new QVBoxLayout;
    setLayout(mainLayout);

    setModal(true);

    QFrame *page = new QFrame(this);
    mainLayout->addWidget(page);
    mainLayout->addWidget(itsButtonBox);

    QGridLayout *layout = new QGridLayout(page);
    layout->setContentsMargins(0, 0, 0, 0);

    itsLabel = new QLabel(page);
    itsView = new CFontFileListView(page);
    itsView->hide();
    layout->addWidget(itsActionLabel = new CActionLabel(this), 0, 0);
    layout->addWidget(itsLabel, 0, 1);
    itsLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
    layout->addWidget(itsView, 1, 0, 1, 2);
    itsFontFileList = new CFontFileList(this);
    connect(itsFontFileList, SIGNAL(finished()), SLOT(scanFinished()));
    connect(itsView, &CFontFileListView::haveDeletions, this, &CDuplicatesDialog::enableButtonOk);
}

int CDuplicatesDialog::exec()
{
    itsActionLabel->startAnimation();
    itsLabel->setText(i18n("Scanning for duplicate fonts. Please wait..."));
    itsFontFileList->start();
    return QDialog::exec();
}

void CDuplicatesDialog::scanFinished()
{
    itsActionLabel->stopAnimation();

    if (itsFontFileList->wasTerminated()) {
        itsFontFileList->wait();
        reject();
    } else {
        CFontFileList::TFontMap duplicates;

        itsFontFileList->getDuplicateFonts(duplicates);

        if (0 == duplicates.count()) {
            itsButtonBox->setStandardButtons(QDialogButtonBox::Close);
            itsLabel->setText(i18n("No duplicate fonts found."));
        } else {
            QSize sizeB4(size());

            itsButtonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Close);
            QPushButton *okButton = itsButtonBox->button(QDialogButtonBox::Ok);
            okButton->setDefault(true);
            okButton->setShortcut(Qt::CTRL | Qt::Key_Return);
            okButton->setText(i18n("Delete Marked Files"));
            okButton->setEnabled(false);
            itsLabel->setText(i18np("%1 duplicate font found.", "%1 duplicate fonts found.", duplicates.count()));
            itsView->show();

            CFontFileList::TFontMap::ConstIterator it(duplicates.begin()), end(duplicates.end());
            QFont boldFont(font());

            boldFont.setBold(true);

            for (; it != end; ++it) {
                QStringList details;

                details << FC::createName(it.key().family, it.key().styleInfo);

                CFontFileListView::StyleItem *top = new CFontFileListView::StyleItem(itsView, details, it.key().family, it.key().styleInfo);

                QSet<QString>::ConstIterator fit((*it).begin()), fend((*it).end());
                int tt(0), t1(0);

                for (; fit != fend; ++fit) {
                    QFileInfo info(*fit);
                    details.clear();
                    details.append(*fit);
                    details.append("");
                    details.append(KFormat().formatByteSize(info.size()));
                    details.append(QLocale().toString(info.created()));
                    if (info.isSymLink())
                        details.append(info.symLinkTarget());
                    new QTreeWidgetItem(top, details);
                    if (Misc::checkExt(*fit, "pfa") || Misc::checkExt(*fit, "pfb"))
                        t1++;
                    else
                        tt++;
                }
                top->setData(COL_FILE, Qt::DecorationRole, QIcon::fromTheme(t1 > tt ? "application-x-font-type1" : "application-x-font-ttf"));
                top->setFont(COL_FILE, boldFont);
            }

            QTreeWidgetItem *item = nullptr;
            for (int i = 0; (item = itsView->topLevelItem(i)); ++i)
                item->setExpanded(true);

            itsView->setSortingEnabled(true);
            itsView->header()->resizeSections(QHeaderView::ResizeToContents);

            int width =
                (itsView->frameWidth() + 8) * 2 + style()->pixelMetric(QStyle::PM_LayoutLeftMargin) + style()->pixelMetric(QStyle::PM_LayoutRightMargin);

            for (int i = 0; i < itsView->header()->count(); ++i)
                width += itsView->header()->sectionSize(i);

            width = qMin(QApplication::desktop()->screenGeometry(this).width(), width);
            resize(width, height());
            QSize sizeNow(size());
            if (sizeNow.width() > sizeB4.width()) {
                int xmod = (sizeNow.width() - sizeB4.width()) / 2, ymod = (sizeNow.height() - sizeB4.height()) / 2;

                move(pos().x() - xmod, pos().y() - ymod);
            }
        }
    }
}

enum EStatus {
    STATUS_NO_FILES,
    STATUS_ALL_REMOVED,
    STATUS_ERROR,
    STATUS_USER_CANCELLED,
};

void CDuplicatesDialog::slotButtonClicked(QAbstractButton *button)
{
    switch (itsButtonBox->standardButton(button)) {
    case QDialogButtonBox::Ok: {
        QSet<QString> files = itsView->getMarkedFiles();
        int fCount = files.count();

        if (1 == fCount ? KMessageBox::Yes == KMessageBox::warningYesNo(this, i18n("Are you sure you wish to delete:\n%1", *files.begin()))
                        : KMessageBox::Yes == KMessageBox::warningYesNoList(this, i18n("Are you sure you wish to delete:"), files.values())) {
            itsFontList->setSlowUpdates(true);

            CJobRunner runner(this);

            connect(&runner, &CJobRunner::configuring, itsFontList, &CFontList::unsetSlowUpdates);
            runner.exec(CJobRunner::CMD_REMOVE_FILE, itsView->getMarkedItems(), false);
            itsFontList->setSlowUpdates(false);
            itsView->removeFiles();
            files = itsView->getMarkedFiles();
            if (fCount != files.count())
                CFcEngine::setDirty();
            if (0 == files.count())
                accept();
        }
        break;
    }
    case QDialogButtonBox::Cancel:
    case QDialogButtonBox::Close:
        if (!itsFontFileList->wasTerminated()) {
            if (itsFontFileList->isRunning()) {
                if (KMessageBox::Yes == KMessageBox::warningYesNo(this, i18n("Cancel font scan?"))) {
                    itsLabel->setText(i18n("Canceling..."));

                    if (itsFontFileList->isRunning())
                        itsFontFileList->terminate();
                    else
                        reject();
                }
            } else
                reject();
        }
        break;
    default:
        break;
    }
}

void CDuplicatesDialog::enableButtonOk(bool on)
{
    QPushButton *okButton = itsButtonBox->button(QDialogButtonBox::Ok);
    if (okButton)
        okButton->setEnabled(on);
}

static uint qHash(const CFontFileList::TFile &key)
{
    return qHash(key.name.toLower());
}

CFontFileList::CFontFileList(CDuplicatesDialog *parent)
    : QThread(parent)
    , itsTerminated(false)
{
}

void CFontFileList::start()
{
    if (!isRunning()) {
        itsTerminated = false;
        QThread::start();
    }
}

void CFontFileList::terminate()
{
    itsTerminated = true;
}

void CFontFileList::getDuplicateFonts(TFontMap &map)
{
    map = itsMap;

    if (!map.isEmpty()) {
        TFontMap::Iterator it(map.begin()), end(map.end());

        // Now re-iterate, and remove any entries that only have 1 file...
        for (it = map.begin(); it != end;)
            if ((*it).count() < 2)
                it = map.erase(it);
            else
                ++it;
    }
}

void CFontFileList::run()
{
    const QList<CFamilyItem *> &families(((CDuplicatesDialog *)parent())->fontList()->families());
    QList<CFamilyItem *>::ConstIterator it(families.begin()), end(families.end());

    for (; it != end; ++it) {
        QList<CFontItem *>::ConstIterator fontIt((*it)->fonts().begin()), fontEnd((*it)->fonts().end());

        for (; fontIt != fontEnd; ++fontIt)
            if (!(*fontIt)->isBitmap()) {
                Misc::TFont font((*fontIt)->family(), (*fontIt)->styleInfo());
                FileCont::ConstIterator fileIt((*fontIt)->files().begin()), fileEnd((*fontIt)->files().end());

                for (; fileIt != fileEnd; ++fileIt)
                    if (!Misc::isMetrics((*fileIt).path()) && !Misc::isBitmap((*fileIt).path()))
                        itsMap[font].insert((*fileIt).path());
            }
    }

    // if we have 2 fonts: /wibble/a.ttf and /wibble/a.TTF fontconfig only returns the 1st, so we
    // now iterate over fontconfig's list, and look for other matching fonts...
    if (!itsMap.isEmpty() && !itsTerminated) {
        // Create a map of folder -> set<files>
        TFontMap::Iterator it(itsMap.begin()), end(itsMap.end());
        QHash<QString, QSet<TFile>> folderMap;

        for (int n = 0; it != end && !itsTerminated; ++it) {
            QStringList add;
            QSet<QString>::const_iterator fIt((*it).begin()), fEnd((*it).end());

            for (; fIt != fEnd && !itsTerminated; ++fIt, ++n)
                folderMap[Misc::getDir(*fIt)].insert(TFile(Misc::getFile(*fIt), it));
        }

        // Go through our folder map, and check for file duplicates...
        QHash<QString, QSet<TFile>>::Iterator folderIt(folderMap.begin()), folderEnd(folderMap.end());

        for (; folderIt != folderEnd && !itsTerminated; ++folderIt)
            fileDuplicates(folderIt.key(), *folderIt);
    }

    emit finished();
}

void CFontFileList::fileDuplicates(const QString &folder, const QSet<TFile> &files)
{
    QDir dir(folder);

    dir.setFilter(QDir::Files | QDir::Hidden);

    QFileInfoList list(dir.entryInfoList());

    for (int i = 0; i < list.size() && !itsTerminated; ++i) {
        QFileInfo fileInfo(list.at(i));

        // Check if this file is already know about - this will do a case-sensitive comparison
        if (!files.contains(TFile(fileInfo.fileName()))) {
            // OK, not found - this means it is a duplicate, but different case. So, find the
            // FontMap iterator, and update its list of files.
            QSet<TFile>::ConstIterator entry = files.find(TFile(fileInfo.fileName(), true));

            if (entry != files.end())
                (*((*entry).it)).insert(fileInfo.absoluteFilePath());
        }
    }
}

inline void markItem(QTreeWidgetItem *item)
{
    item->setData(COL_TRASH, Qt::DecorationRole, QIcon::fromTheme("list-remove"));
}

inline void unmarkItem(QTreeWidgetItem *item)
{
    item->setData(COL_TRASH, Qt::DecorationRole, QVariant());
}

inline bool isMarked(QTreeWidgetItem *item)
{
    return item->data(COL_TRASH, Qt::DecorationRole).isValid();
}

CFontFileListView::CFontFileListView(QWidget *parent)
    : QTreeWidget(parent)
{
    QStringList headers;
    headers.append(i18n("Font/File"));
    headers.append("");
    headers.append(i18n("Size"));
    headers.append(i18n("Date"));
    headers.append(i18n("Links To"));
    setHeaderLabels(headers);
    headerItem()->setData(COL_TRASH, Qt::DecorationRole, QIcon::fromTheme("user-trash"));
    setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
    setSelectionMode(ExtendedSelection);
    sortByColumn(COL_FILE, Qt::AscendingOrder);
    setSelectionBehavior(SelectRows);
    setSortingEnabled(true);
    setAllColumnsShowFocus(true);
    setAlternatingRowColors(true);

    itsMenu = new QMenu(this);
    if (!Misc::app(KFI_VIEWER).isEmpty())
        itsMenu->addAction(QIcon::fromTheme("kfontview"), i18n("Open in Font Viewer"), this, &CFontFileListView::openViewer);
    itsMenu->addAction(QIcon::fromTheme("document-properties"), i18n("Properties"), this, &CFontFileListView::properties);
    itsMenu->addSeparator();
    itsUnMarkAct = itsMenu->addAction(i18n("Unmark for Deletion"), this, &CFontFileListView::unmark);
    itsMarkAct = itsMenu->addAction(QIcon::fromTheme("edit-delete"), i18n("Mark for Deletion"), this, &CFontFileListView::mark);

    connect(this, SIGNAL(itemSelectionChanged()), SLOT(selectionChanged()));
    connect(this, SIGNAL(itemClicked(QTreeWidgetItem *, int)), SLOT(clicked(QTreeWidgetItem *, int)));
}

QSet<QString> CFontFileListView::getMarkedFiles()
{
    QTreeWidgetItem *root = invisibleRootItem();
    QSet<QString> files;

    for (int t = 0; t < root->childCount(); ++t) {
        QList<QTreeWidgetItem *> removeFiles;
        QTreeWidgetItem *font = root->child(t);

        for (int c = 0; c < font->childCount(); ++c) {
            QTreeWidgetItem *file = font->child(c);

            if (isMarked(file))
                files.insert(file->text(0));
        }
    }

    return files;
}

CJobRunner::ItemList CFontFileListView::getMarkedItems()
{
    QTreeWidgetItem *root = invisibleRootItem();
    CJobRunner::ItemList items;
    QString home(Misc::dirSyntax(QDir::homePath()));

    for (int t = 0; t < root->childCount(); ++t) {
        QList<QTreeWidgetItem *> removeFiles;
        StyleItem *style = (StyleItem *)root->child(t);

        for (int c = 0; c < style->childCount(); ++c) {
            QTreeWidgetItem *file = style->child(c);

            if (isMarked(file))
                items.append(CJobRunner::Item(file->text(0), style->family(), style->value(), 0 != file->text(0).indexOf(home)));
        }
    }

    return items;
}

void CFontFileListView::removeFiles()
{
    QTreeWidgetItem *root = invisibleRootItem();
    QList<QTreeWidgetItem *> removeFonts;

    for (int t = 0; t < root->childCount(); ++t) {
        QList<QTreeWidgetItem *> removeFiles;
        QTreeWidgetItem *font = root->child(t);

        for (int c = 0; c < font->childCount(); ++c) {
            QTreeWidgetItem *file = font->child(c);

            if (!Misc::fExists(file->text(0)))
                removeFiles.append(file);
        }

        QList<QTreeWidgetItem *>::ConstIterator it(removeFiles.begin()), end(removeFiles.end());

        for (; it != end; ++it)
            delete (*it);
        if (0 == font->childCount())
            removeFonts.append(font);
    }

    QList<QTreeWidgetItem *>::ConstIterator it(removeFonts.begin()), end(removeFonts.end());
    for (; it != end; ++it)
        delete (*it);
}

void CFontFileListView::openViewer()
{
    // Number of fonts user has selected, before we ask if they really want to view them all...
    static const int constMaxBeforePrompt = 10;

    QList<QTreeWidgetItem *> items(selectedItems());
    QTreeWidgetItem *item;
    QSet<QString> files;

    foreach (item, items)
        if (item->parent()) // Then it is a file, not font name :-)
            files.insert(item->text(0));

    if (!files.isEmpty()
        && (files.count() < constMaxBeforePrompt
            || KMessageBox::Yes == KMessageBox::questionYesNo(this, i18np("Open font in font viewer?", "Open all %1 fonts in font viewer?", files.count())))) {
        QSet<QString>::ConstIterator it(files.begin()), end(files.end());

        for (; it != end; ++it) {
            QStringList args;

            args << (*it);

            QProcess::startDetached(Misc::app(KFI_VIEWER), args);
        }
    }
}

void CFontFileListView::properties()
{
    QList<QTreeWidgetItem *> items(selectedItems());
    QTreeWidgetItem *item;
    KFileItemList files;
    QMimeDatabase db;

    foreach (item, items)
        if (item->parent())
            files.append(
                KFileItem(QUrl::fromLocalFile(item->text(0)), db.mimeTypeForFile(item->text(0)).name(), item->text(COL_LINK).isEmpty() ? S_IFREG : S_IFLNK));

    if (!files.isEmpty()) {
        KPropertiesDialog dlg(files, this);
        dlg.exec();
    }
}

void CFontFileListView::mark()
{
    QList<QTreeWidgetItem *> items(selectedItems());
    QTreeWidgetItem *item;

    foreach (item, items)
        if (item->parent())
            markItem(item);
    checkFiles();
}

void CFontFileListView::unmark()
{
    QList<QTreeWidgetItem *> items(selectedItems());
    QTreeWidgetItem *item;

    foreach (item, items)
        if (item->parent())
            unmarkItem(item);
    checkFiles();
}

void CFontFileListView::selectionChanged()
{
    QList<QTreeWidgetItem *> items(selectedItems());
    QTreeWidgetItem *item;

    foreach (item, items)
        if (!item->parent() && item->isSelected())
            item->setSelected(false);
}

void CFontFileListView::clicked(QTreeWidgetItem *item, int col)
{
    if (item && COL_TRASH == col && item->parent()) {
        if (isMarked(item))
            unmarkItem(item);
        else
            markItem(item);
        checkFiles();
    }
}

void CFontFileListView::contextMenuEvent(QContextMenuEvent *ev)
{
    QTreeWidgetItem *item(itemAt(ev->pos()));

    if (item && item->parent()) {
        if (!item->isSelected())
            item->setSelected(true);

        bool haveUnmarked(false), haveMarked(false);

        QList<QTreeWidgetItem *> items(selectedItems());
        QTreeWidgetItem *item;

        foreach (item, items) {
            if (item->parent() && item->isSelected()) {
                if (isMarked(item))
                    haveMarked = true;
                else
                    haveUnmarked = true;
            }

            if (haveUnmarked && haveMarked)
                break;
        }

        itsMarkAct->setEnabled(haveUnmarked);
        itsUnMarkAct->setEnabled(haveMarked);
        itsMenu->popup(ev->globalPos());
    }
}

void CFontFileListView::checkFiles()
{
    // Need to check that if we mark a file that is linked to, then we also need
    // to mark the sym link.
    QSet<QString> marked(getMarkedFiles());

    if (marked.count()) {
        QTreeWidgetItem *root = invisibleRootItem();

        for (int t = 0; t < root->childCount(); ++t) {
            QTreeWidgetItem *font = root->child(t);

            for (int c = 0; c < font->childCount(); ++c) {
                QTreeWidgetItem *file = font->child(c);
                QString link(font->child(c)->text(COL_LINK));

                if (!link.isEmpty() && marked.contains(link))
                    if (!isMarked(file))
                        markItem(file);
            }
        }

        emit haveDeletions(true);
    } else
        emit haveDeletions(false);
}

}
