/***
   This file is part of fly-ocr.

   Copyright (C) 2016-2020 Sandro Mani <manisandro@gmail.com>
   Copyright (C) 2021 LLC "RusBITech-Astra" (https://astralinux.ru)

    Based on gImageReader source code by Sandro Mani under the GNU
    General Public License (GPL) version 3 or later

   Foobar 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.

   Foobar 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 fly-ocr. If not, see <https://www.gnu.org/licenses/>.
***/

#include <memory>
#include <QClipboard>
#include <QDesktopWidget>
#include <QDesktopServices>
#include <QDir>
#include <QDirIterator>
#include <QDragEnterEvent>
#include <QFile>
#include <QFileDialog>
#include <QFileInfo>
#include <QImageReader>
#include <QInputDialog>
#include <QMessageBox>
#include <QScreen>
#include <QString>
#include <QTemporaryFile>
#include <QMimeData>
#include <poppler-qt5.h>

#include "configsettings.h"
#include "filedialogs.h"
#include "mainwindow.h"
#include "mainwindow_ui.h"
#include "recognizer.h"
#include "sourcemanager.h"
#include "utils.h"

SourceManager::SourceManager(const MainWindowUi* _ui)
    : m_ui(_ui)
{
    //m_fileTreeModel = new FileTreeModel(this);
    m_sourceDocumentModel = new SourceDocumentModel(this);

    m_ui->sourceDocumentView->setModel(m_sourceDocumentModel);

//    m_ui->treeViewSources->setModel(m_fileTreeModel);
//    m_ui->treeViewSources->setAcceptDrops(true);
//    m_ui->treeViewSources->installEventFilter(this);
//    m_ui->treeViewSources->header()->setSectionResizeMode(0, QHeaderView::Stretch);
//    m_ui->treeViewSources->header()->setSectionResizeMode(1, QHeaderView::Fixed);
//    m_ui->treeViewSources->header()->resizeSection(1, 16);

    m_ui->actionPasteFromClipboard->setEnabled(Utils::isSupported(QApplication::clipboard()->mimeData()));

    connect(m_ui->actionAddImages, &QAction::triggered, this, &SourceManager::openSources);
    connect(m_ui->actionAddDirectory, &QAction::triggered, this, &SourceManager::addFolder);
    connect(m_ui->actionPasteFromClipboard, &QAction::triggered, this, &SourceManager::pasteClipboard);
    connect(m_ui->actionTakeScreenshot, &QAction::triggered, this, &SourceManager::takeScreenshot);
    connect(m_ui->actionSourceRemove, &QAction::triggered, this, &SourceManager::removeSelected);
    connect(m_ui->actionListMode, &QAction::triggered, m_sourceDocumentModel, &SourceDocumentModel::setListMode);
    connect(m_ui->actionSourceClear, &QAction::triggered, [ = ] {
        MAIN->recognizer()->destroyEngine();
        m_sourceDocumentModel->clear();
        emit selectedPageChanged();
    } );
    connect(m_ui->actionRecognitionResultsClear, &QAction::triggered, [ = ] {
        MAIN->recognizer()->destroyEngine();
        currentSourceChanged();
    } );

    connect(m_ui->sourceDocumentView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [this] {
        currentSourceChanged();
        updateActions();
    });
//    connect(m_ui->treeViewSources->selectionModel(), &QItemSelectionModel::selectionChanged, this, [this](const QItemSelection&, const QItemSelection&) { currentSourceChanged(); });
//    connect(m_ui->treeViewSources, &QTreeView::clicked, this, &SourceManager::indexClicked);
    connect(QApplication::clipboard(), &QClipboard::dataChanged, this, [this] {
        m_ui->actionPasteFromClipboard->setEnabled(Utils::isSupported(QApplication::clipboard()->mimeData()));
    });
    connect(m_sourceDocumentModel, &QAbstractListModel::rowsInserted, this, &SourceManager::updateActions);
    connect(m_sourceDocumentModel, &QAbstractListModel::rowsRemoved, this, &SourceManager::updateActions);
    connect(m_sourceDocumentModel, &QAbstractListModel::modelReset, this, &SourceManager::updateActions);
    connect(m_sourceDocumentModel, &QAbstractListModel::dataChanged, this, &SourceManager::updateActions);
    connect(m_ui->spinBoxPage, qOverload<int>(&QSpinBox::valueChanged), [ = ](int page) {
        m_ui->sourceDocumentView->selectionModel()->setCurrentIndex(m_sourceDocumentModel->index(page - 1, 0), QItemSelectionModel::ClearAndSelect);
    });
    ADD_SETTING(VarSetting<QStringList>("recentitems"));
}

SourceManager::~SourceManager()
{
    m_sourceDocumentModel->clear();
}

bool SourceManager::add(const QStringList& paths, bool suppressWarnings)
{
    QStringList files, filesToAdd, failed;
    QStringList recentItems = ConfigSettings::get<VarSetting<QStringList>>("recentitems")->getValue();

    for (const QString& path : paths) {
        QFileInfo info(path);

        if (!info.exists()) {
            failed.append(info.fileName());
            continue;
        }

        if (info.isFile())
            files << info.canonicalFilePath();
        else if (info.isDir())
            files << findImageFiles(info.canonicalFilePath());
    }

    PdfWithTextAction textAction = suppressWarnings ? PdfWithTextAction::Add : PdfWithTextAction::Ask;

    for (const QString& file : qAsConst(files)) {
        QModelIndex index = m_sourceDocumentModel->findFile(file);
        if (index.isValid())
            continue;

        std::shared_ptr<SourceDocumentInfo> info;

        if (file.endsWith(QStringLiteral(".pdf"), Qt::CaseInsensitive)) {
            info = std::make_shared<SourceDocumentInfo>();
            if (checkPdfSource(info, file, &textAction))
                filesToAdd << file;
            else
                continue;
        } else {
            filesToAdd << file;
        }
        QString fileName = file.section("/", -1);
        recentItems.removeAll(fileName);
        recentItems.prepend(fileName);
        m_sourceDocumentModel->add(file, false, info);
    }
    currentSourceChanged();
    if (!failed.isEmpty() && !suppressWarnings) {
        QMessageBox::critical(MAIN, tr("Unable to open files"), tr("The following files could not be opened:\n%1").arg(failed.join("\n")));
    }
    return !filesToAdd.isEmpty();
}

bool SourceManager::add(const QList<QUrl>& urls, bool suppressWarnings)
{
    QStringList paths;
    for (const QUrl& url : qAsConst(urls)) {
        if (url.isLocalFile())
            paths << url.toLocalFile();
    }
    return add(paths, suppressWarnings);
}

bool SourceManager::add(const QString& path, bool suppressWarnings)
{
    return add(QStringList() << path, suppressWarnings);
}

QList<int> SourceManager::findPages(const QString& filePath) const
{
    return m_sourceDocumentModel->findPages(filePath);
}

QList<int> SourceManager::selectedPages() const
{
    QModelIndexList selected = m_ui->sourceDocumentView->selectionModel()->selectedRows();
    QList<int> rows;

    for (const QModelIndex& index : qAsConst(selected)) {
        rows << index.row();
    }

    return rows;
}

void SourceManager::selectPage(int p)
{
    m_ui->sourceDocumentView->setCurrentIndex(m_sourceDocumentModel->index(p, 0));
}

void SourceManager::setRecognized(int p, bool recognized)
{
    m_sourceDocumentModel->setRecognized(p, recognized);
}

void SourceManager::clearReconized()
{
    m_sourceDocumentModel->clearReconized();
}

void SourceManager::rotateAll(double delta)
{
    for (int i = 0; i < m_sourceDocumentModel->rowCount(); ++i) {
        auto page = m_sourceDocumentModel->page(i);
        double newangle = page->angle + delta;
        newangle = newangle < 0.0 ? newangle + 360.0 : newangle >= 360.0 ? newangle - 360.0 : newangle,
        page->angle = newangle;
    }
}

bool SourceManager::checkPdfSource(std::shared_ptr<SourceDocumentInfo> info, const QString& path, PdfWithTextAction* textAction) const
{
    std::unique_ptr<Poppler::Document> document(Poppler::Document::load(path));
    if (!document)
        return false;

    // Unlock if necessary
    if (document->isLocked()) {
        bool ok = false;
        QString message = QString(tr("Enter password for file '%1':")).arg(QFileInfo(path).fileName());
        while (true) {
            QString text = QInputDialog::getText(MAIN, tr("Protected PDF"), message, QLineEdit::Password, text, &ok);
            if (!ok)
                return false;

            if (!document->unlock(text.toLocal8Bit(), text.toLocal8Bit())) {
                info->password = text.toLocal8Bit();
                break;
            }
        }
    }

    // Check whether the PDF already contains text
    if (*textAction != PdfWithTextAction::Add) {
        bool haveText = false;
        for (int i = 0, n = document->numPages(); i < n; ++i) {
            if (!document->page(i)->text(QRectF()).isEmpty()) {
                haveText = true;
                break;
            }
        }
        if (haveText && *textAction == PdfWithTextAction::Skip) {
            return false;
        }
        int response = QMessageBox::question(MAIN, tr("PDF with text"), tr("The PDF file already contains text:\n%1\nOpen it regardless?").arg(path), QMessageBox::Yes | QMessageBox::YesAll | QMessageBox::No | QMessageBox::NoAll);
        if (response == QMessageBox::No) {
            return false;
        } else if (response == QMessageBox::NoAll) {
            *textAction = PdfWithTextAction::Skip;
            return false;
        } else if (response == QMessageBox::YesAll) {
            *textAction = PdfWithTextAction::Add;
        }
    }

    // Extract document metadata
    info->author = document->author();
    info->creator = document->creator();
    info->keywords = document->keywords();
    info->producer = document->producer();
    info->title = document->title();
    info->subject = document->subject();
    document->getPdfVersion(&info->pdfVersionMajor, &info->pdfVersionMinor);

    return true;
}

std::shared_ptr<SourceDocumentPage> SourceManager::selectedPage() const
{
    QModelIndexList indexes = m_ui->sourceDocumentView->selectionModel()->selectedRows();
    if (indexes.isEmpty())
        return nullptr;

    return m_sourceDocumentModel->page(indexes.first().row());
}

std::shared_ptr<SourceDocumentPage> SourceManager::page(int i) const
{
    return m_sourceDocumentModel->page(i);
}

void SourceManager::openSources()
{
    QString initialFolder = Utils::documentsFolder();
    QSet<QString> formats;
    for (const QByteArray& format : QImageReader::supportedImageFormats()) {
        formats.insert(QString("*.%1").arg(QString(format).toLower()));
    }
    formats.insert("*.pdf");
    formats.insert("*.djvu");
    QString filter = QString("%1 (%2)").arg(tr("Images and PDFs")).arg(QStringList(formats.values()).join(" "));

    QStringList files = QFileDialog::getOpenFileNames(MAIN, tr("Select Files"), initialFolder, filter);
    m_sourceDocumentModel->add(files);

    //addSources(FileDialogs::openDialog(tr("Select Files"), initialFolder, "sourcedir", filter, true));
}

void SourceManager::addFolder()
{
    QString initialFolder = Utils::documentsFolder();
    QString dir = QFileDialog::getExistingDirectory(MAIN, tr("Select folder..."), initialFolder);
    if (dir.isEmpty()) {
        return;
    }
    QStringList nameFilters;
    QSet<QString> formats;
    for (const QByteArray& format : QImageReader::supportedImageFormats()) {
        formats.insert(QString("*.%1").arg(QString(format).toLower()));
    }
    formats.insert("*.pdf");
    formats.insert("*.djvu");
    nameFilters = formats.values();

    QDirIterator it(dir, nameFilters, QDir::Files, QDirIterator::Subdirectories);

    QStringList filenames;
    while (it.hasNext()) {
        filenames.append(it.next());
    }
    if (!filenames.isEmpty()) {
        add(filenames);
    }
}

void SourceManager::openRecentItem()
{
    const QString& filename = qobject_cast<QAction*>(QObject::sender())->toolTip();
    add(QStringList() << filename);
}

void SourceManager::pasteClipboard()
{
    if (QApplication::clipboard()->mimeData()->hasImage())
        savePixmap(QApplication::clipboard()->pixmap());
    else if (!QApplication::clipboard()->mimeData()->urls().isEmpty())
        add(QApplication::clipboard()->mimeData()->urls());
    else
        QMessageBox::critical(MAIN, tr("Clipboard Error"),  tr("Failed to read the clipboard."));
}

void SourceManager::add(const QImage& image)
{
    savePixmap(QPixmap::fromImage(image));
}

void SourceManager::takeScreenshot()
{
    MAIN->hide();
    QApplication::processEvents();
    QTimer* timer = new QTimer();
    connect(timer, &QTimer::timeout, [this, timer] {
        timer->deleteLater();
        QPixmap pixmap = QGuiApplication::primaryScreen()->grabWindow(QApplication::desktop()->winId());
        MAIN->show();
        if (pixmap.isNull())
        {
            QMessageBox::critical(MAIN, tr("Screenshot Error"),  tr("Failed to take screenshot."));
            return;
        }
        savePixmap(pixmap);
        page(m_sourceDocumentModel->rowCount() - 1)->resolution = qApp->primaryScreen()->physicalDotsPerInch();
        selectPage(m_sourceDocumentModel->rowCount() - 1);
    });
    timer->start(250);
}

void SourceManager::updateActions()
{
    m_ui->actionSourceClear->setEnabled(m_sourceDocumentModel->rowCount() > 0);
    m_ui->actionRecognitionResultsClear->setEnabled(m_sourceDocumentModel->hasRecognitionResults());
    m_ui->actionSourceRemove->setEnabled(m_sourceDocumentModel->rowCount() > 0 && m_ui->sourceDocumentView->currentIndex().isValid());
    m_ui->actionPage->setVisible(m_sourceDocumentModel->rowCount() > 1);
    QSignalBlocker blocker(m_ui->spinBoxPage);
    m_ui->spinBoxPage->setRange(1, m_sourceDocumentModel->rowCount());
}

void SourceManager::savePixmap(const QPixmap& pixmap)
{
    MAIN->pushState(MainWindow::State::Busy, tr("Saving image..."));

    bool success = false;
    QTemporaryFile tmpfile(QDir::temp().absoluteFilePath("fly-ocr_XXXXXX.png"));
    if (tmpfile.open()) {
        tmpfile.setAutoRemove(false);
        success = pixmap.save(&tmpfile, "PNG");
        tmpfile.close();
    }

    MAIN->popState();

    if (success) {
        m_sourceDocumentModel->add(tmpfile.fileName(), true);
    } else {
        QMessageBox::critical(MAIN, tr("Cannot Write File"),  tr("Could not write to %1.").arg(tmpfile.fileName()));
    }
}

void SourceManager::removeSelected()
{
    QList<int> rows = selectedPages();

    std::sort(rows.begin(), rows.end(), std::greater<>());

    for (int row : qAsConst(rows)) {
        m_sourceDocumentModel->removeRow(row);
        MAIN->recognizer()->removePage(row);
    }

    currentSourceChanged();
}

void SourceManager::currentSourceChanged()
{
    QSignalBlocker blocker(m_ui->spinBoxPage);
    m_ui->spinBoxPage->setValue(m_ui->sourceDocumentView->currentIndex().row() + 1);
    emit selectedPageChanged();
}

QStringList SourceManager::findImageFiles(const QString& path) const
{
    QDir dir(path);
    QStringList out, formats;

    for (const QByteArray& format : QImageReader::supportedImageFormats()) {
        formats << (QString("*.%1").arg(QString(format)).toLower());
    }
    formats << QStringLiteral("*.pdf");
    formats << QStringLiteral("*.djvu");
    out << dir.entryList(formats, QDir::Files);

    for (const QString& subdir : dir.entryList(QDir::Dirs))
        out << findImageFiles(subdir);

    return out;
}

bool SourceManager::eventFilter(QObject* object, QEvent* event)
{
    if (event->type() == QEvent::DragEnter) {
        QDragEnterEvent* dragEnterEvent = static_cast<QDragEnterEvent*>(event);
        if (Utils::handleSourceDragEvent(dragEnterEvent->mimeData())) {
            dragEnterEvent->acceptProposedAction();
        }
        return true;
    } else if (event->type() == QEvent::Drop) {
        QDropEvent* dropEvent = static_cast<QDropEvent*>(event);
        Utils::handleSourceDropEvent(dropEvent->mimeData());
        return true;
    }
    return QObject::eventFilter(object, event);
}
