/*******************************************************************
* kquery.cpp
*
* 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.  If not, see <http://www.gnu.org/licenses/>.
*
******************************************************************/

#include "kquery.h"
#include "kfind_debug.h"
#include <stdlib.h>

#include <QCoreApplication>
#include <QFileInfo>
#include <QTextCodec>
#include <QTextStream>
#include <QList>
#include <kmimetype.h>
#include <kfileitem.h>
#include <kfilemetainfo.h>
#include <KLocalizedString>
#include <kmessagebox.h>
#include <kstandarddirs.h>
#include <kzip.h>

KQuery::KQuery(QObject *parent)
    : QObject(parent)
    , m_filetype(0)
    , m_sizemode(0)
    , m_sizeboundary1(0)
    , m_sizeboundary2(0)
    , m_timeFrom(0)
    , m_timeTo(0)
    , m_recursive(false)
    , m_casesensitive(false)
    , m_search_binary(false)
    , m_regexpForContent(false)
    , m_useLocate(false)
    , m_showHiddenFiles(false)
    , job(0)
    , m_insideCheckEntries(false)
    , m_result(0)
{
    processLocate = new KProcess(this);
    connect(processLocate, SIGNAL(readyReadStandardOutput()), this, SLOT(slotreadyReadStandardOutput()));
    connect(processLocate, SIGNAL(readyReadStandardError()), this, SLOT(slotreadyReadStandardError()));
    connect(processLocate, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(slotendProcessLocate(int,QProcess::ExitStatus)));

    // Files with these mime types can be ignored, even if
    // findFormatByFileContent() in some cases may claim that
    // these are text files:
    ignore_mimetypes.append(QStringLiteral("application/pdf"));
    ignore_mimetypes.append(QStringLiteral("application/postscript"));

    // PLEASE update the documentation when you add another
    // file type here:
    ooo_mimetypes.append(QStringLiteral("application/vnd.sun.xml.writer"));
    ooo_mimetypes.append(QStringLiteral("application/vnd.sun.xml.calc"));
    ooo_mimetypes.append(QStringLiteral("application/vnd.sun.xml.impress"));
    // OASIS mimetypes, used by OOo-2.x and KOffice >= 1.4
    //ooo_mimetypes.append("application/vnd.oasis.opendocument.chart");
    //ooo_mimetypes.append("application/vnd.oasis.opendocument.graphics");
    //ooo_mimetypes.append("application/vnd.oasis.opendocument.graphics-template");
    //ooo_mimetypes.append("application/vnd.oasis.opendocument.formula");
    //ooo_mimetypes.append("application/vnd.oasis.opendocument.image");
    ooo_mimetypes.append(QStringLiteral("application/vnd.oasis.opendocument.presentation-template"));
    ooo_mimetypes.append(QStringLiteral("application/vnd.oasis.opendocument.presentation"));
    ooo_mimetypes.append(QStringLiteral("application/vnd.oasis.opendocument.spreadsheet-template"));
    ooo_mimetypes.append(QStringLiteral("application/vnd.oasis.opendocument.spreadsheet"));
    ooo_mimetypes.append(QStringLiteral("application/vnd.oasis.opendocument.text-template"));
    ooo_mimetypes.append(QStringLiteral("application/vnd.oasis.opendocument.text"));
    // KOffice-1.3 mimetypes
    koffice_mimetypes.append(QStringLiteral("application/x-kword"));
    koffice_mimetypes.append(QStringLiteral("application/x-kspread"));
    koffice_mimetypes.append(QStringLiteral("application/x-kpresenter"));
}

KQuery::~KQuery()
{
    while (!m_regexps.isEmpty()) {
        delete m_regexps.takeFirst();
    }
    m_fileItems.clear();
    if (processLocate->state() == QProcess::Running) {
        disconnect(processLocate);
        processLocate->kill();
        processLocate->waitForFinished(5000);
        delete processLocate;
    }
}

void KQuery::kill()
{
    if (job) {
        job->kill(KJob::EmitResult);
    }
    if (processLocate->state() == QProcess::Running) {
        processLocate->kill();
    }
    m_fileItems.clear();
}

void KQuery::start()
{
    m_fileItems.clear();
    if (m_useLocate) { //Use "locate" instead of the internal search method
        bufferLocate.clear();
        m_url = m_url.adjusted(QUrl::NormalizePathSegments);

        processLocate->clearProgram();
        processLocate->setProgram(QStringLiteral("locate"), QStringList() <<  m_url.toLocalFile());

        processLocate->setOutputChannelMode(KProcess::SeparateChannels);
        processLocate->start();
    } else { //Use KIO
        if (m_recursive) {
            job = KIO::listRecursive(m_url, KIO::HideProgressInfo);
        } else {
            job = KIO::listDir(m_url, KIO::HideProgressInfo);
        }

        connect(job, SIGNAL(entries(KIO::Job *,KIO::UDSEntryList)),
                SLOT(slotListEntries(KIO::Job *,KIO::UDSEntryList)));
        connect(job, SIGNAL(result(KJob *)), SLOT(slotResult(KJob *)));
        connect(job, SIGNAL(canceled(KJob *)), SLOT(slotCanceled(KJob *)));
    }
}

void KQuery::slotResult(KJob *_job)
{
    if (job != _job) {
        return;
    }
    job = nullptr;

    m_result = _job->error();
    checkEntries();
}

void KQuery::slotCanceled(KJob *_job)
{
    if (job != _job) {
        return;
    }
    job = nullptr;

    m_fileItems.clear();

    m_result = KIO::ERR_USER_CANCELED;
    checkEntries();
}

void KQuery::slotListEntries(KIO::Job *, const KIO::UDSEntryList &list)
{
    const KIO::UDSEntryList::ConstIterator end = list.constEnd();

    for (KIO::UDSEntryList::ConstIterator it = list.constBegin(); it != end; ++it) {
        m_fileItems.enqueue(KFileItem(*it, m_url, true, true));
    }

    checkEntries();
}

void KQuery::checkEntries()
{
    if (m_insideCheckEntries) {
        return;
    }

    m_insideCheckEntries = true;

    metaKeyRx = QRegExp(m_metainfokey);
    metaKeyRx.setPatternSyntax(QRegExp::Wildcard);

    m_foundFilesList.clear();

    int processingCount = 0;
    while (!m_fileItems.isEmpty())
    {
        processQuery(m_fileItems.dequeue());
        processingCount++;

        /* This is a workaround. As the qApp->processEvents() call inside processQuery
         * will bring more KIO entries, m_fileItems will increase even inside this loop
         * and that will lead to a big loop, it will take time to report found items to the GUI
         * so we are going to force emit results every 100 files processed */
        if (processingCount == 100) {
            processingCount = 0;
            if (m_foundFilesList.size() > 0) {
                emit foundFileList(m_foundFilesList);
                m_foundFilesList.clear();
            }
        }
    }

    if (m_foundFilesList.size() > 0) {
        emit foundFileList(m_foundFilesList);
    }

    if (job == 0) {
        emit result(m_result);
    }

    m_insideCheckEntries = false;
}

/* List of files found using slocate */
void KQuery::slotListEntries(QStringList list)
{
    metaKeyRx = QRegExp(m_metainfokey);
    metaKeyRx.setPatternSyntax(QRegExp::Wildcard);

    QStringList::const_iterator it = list.constBegin();
    QStringList::const_iterator end = list.constEnd();

    m_foundFilesList.clear();
    for (; it != end; ++it) {
        processQuery(KFileItem(KFileItem::Unknown, KFileItem::Unknown, QUrl::fromLocalFile(*it)));
    }

    if (!m_foundFilesList.isEmpty()) {
        emit foundFileList(m_foundFilesList);
    }
}

/* Check if file meets the find's requirements*/
void KQuery::processQuery(const KFileItem &file)
{
    if (file.name() == QLatin1String(".") || file.name() == QLatin1String("..")) {
        return;
    }

    if (!m_showHiddenFiles && file.isHidden()) {
        return;
    }

    bool matched = false;

    QListIterator<QRegExp *> nextItem(m_regexps);
    while (nextItem.hasNext())
    {
        QRegExp *reg = nextItem.next();
        matched = matched || (reg == nullptr) || (reg->exactMatch(file.url().adjusted(QUrl::StripTrailingSlash).fileName()));
    }
    if (!matched) {
        return;
    }

    // make sure the files are in the correct range
    switch (m_sizemode) {
    case 1: // "at least"
        if (file.size() < m_sizeboundary1) {
            return;
        }
        break;
    case 2: // "at most"
        if (file.size() > m_sizeboundary1) {
            return;
        }
        break;
    case 3: // "equal"
        if (file.size() != m_sizeboundary1) {
            return;
        }
        break;
    case 4: // "between"
        if ((file.size() < m_sizeboundary1)
            || (file.size() > m_sizeboundary2)) {
            return;
        }
        break;
    case 0: // "none" -> Fall to default
    default:
        break;
    }

    // make sure it's in the correct date range
    // what about 0 times?
    if (m_timeFrom && ((uint)m_timeFrom) > file.time(KFileItem::ModificationTime).toTime_t()) {
        return;
    }
    if (m_timeTo && ((uint)m_timeTo) < file.time(KFileItem::ModificationTime).toTime_t()) {
        return;
    }

    // username / group match
    if ((!m_username.isEmpty()) && (m_username != file.user())) {
        return;
    }
    if ((!m_groupname.isEmpty()) && (m_groupname != file.group())) {
        return;
    }

    // file type
    switch (m_filetype) {
    case 0:
        break;
    case 1: // plain file
        if (!S_ISREG(file.mode())) {
            return;
        }
        break;
    case 2:
        if (!file.isDir()) {
            return;
        }
        break;
    case 3:
        if (!file.isLink()) {
            return;
        }
        break;
    case 4:
        if (!S_ISCHR(file.mode()) && !S_ISBLK(file.mode())
            && !S_ISFIFO(file.mode()) && !S_ISSOCK(file.mode())) {
            return;
        }
        break;
    case 5: // binary
        if ((file.permissions() & 0111) != 0111 || file.isDir()) {
            return;
        }
        break;
    case 6: // suid
        if ((file.permissions() & 04000) != 04000) { // fixme
            return;
        }
        break;
    default:
        if (!m_mimetype.isEmpty() && !m_mimetype.contains(file.mimetype())) {
            return;
        }
    }

    // match data in metainfo...
    if ((!m_metainfo.isEmpty()) && (!m_metainfokey.isEmpty())) {
        //Avoid sequential files (fifo,char devices)
        if (!file.isRegularFile()) {
            return;
        }

        bool foundmeta = false;
        QString filename = file.url().path();

        if (filename.startsWith(QLatin1String("/dev/"))) {
            return;
        }

        KFileMetaInfo metadatas(filename);
        QStringList metakeys;
        QString strmetakeycontent;

        metakeys = metadatas.supportedKeys();
        for (QStringList::const_iterator it = metakeys.constBegin(); it != metakeys.constEnd(); ++it) {
            if (!metaKeyRx.exactMatch(*it)) {
                continue;
            }
            strmetakeycontent = metadatas.item(*it).value().toString();
            if (strmetakeycontent.indexOf(m_metainfo) != -1) {
                foundmeta = true;
                break;
            }
        }
        if (!foundmeta) {
            return;
        }
    }

    // match contents...
    QString matchingLine;
    if (!m_context.isEmpty()) {
        //Avoid sequential files (fifo,char devices)
        if (!file.isRegularFile()) {
            return;
        }

        if (!m_search_binary && ignore_mimetypes.indexOf(file.mimetype()) != -1) {
            return;
        }

        bool found = false;
        bool isZippedOfficeDocument = false;
        int matchingLineNumber = 0;

        // FIXME: doesn't work with non local files

        QString filename;
        QTextStream *stream = 0;
        QFile qf;
        QRegExp xmlTags;
        QByteArray zippedXmlFileContent;

        // KWord's and OpenOffice.org's files are zipped...
        if (ooo_mimetypes.indexOf(file.mimetype()) != -1
            || koffice_mimetypes.indexOf(file.mimetype()) != -1) {
            KZip zipfile(file.url().path());
            KZipFileEntry *zipfileEntry;

            if (zipfile.open(QIODevice::ReadOnly)) {
                const KArchiveDirectory *zipfileContent = zipfile.directory();

                if (koffice_mimetypes.indexOf(file.mimetype()) != -1) {
                    zipfileEntry = (KZipFileEntry *)zipfileContent->entry(QStringLiteral("maindoc.xml"));
                } else {
                    zipfileEntry = (KZipFileEntry *)zipfileContent->entry(QStringLiteral("content.xml")); //for OpenOffice.org
                }
                if (!zipfileEntry) {
                    qCWarning(KFING_LOG) << "Expected XML file not found in ZIP archive " << file.url();
                    return;
                }

                zippedXmlFileContent = zipfileEntry->data();
                xmlTags.setPattern(QStringLiteral("<.*>"));
                xmlTags.setMinimal(true);
                stream = new QTextStream(zippedXmlFileContent, QIODevice::ReadOnly);
                stream->setCodec("UTF-8");
                isZippedOfficeDocument = true;
            } else {
                qCWarning(KFING_LOG) << "Cannot open supposed ZIP file " << file.url();
            }
        } else if (!m_search_binary && !file.mimetype().startsWith(QLatin1String("text/"))
                   && file.url().isLocalFile() && !file.url().path().startsWith(QLatin1String("/dev"))) {
            if (KMimeType::isBinaryData(file.url().path())) {
                return;
            }
        }

        if (!isZippedOfficeDocument) { //any other file or non-compressed KWord
            filename = file.url().path();
            if (filename.startsWith(QLatin1String("/dev/"))) {
                return;
            }
            qf.setFileName(filename);
            qf.open(QIODevice::ReadOnly);
            stream = new QTextStream(&qf);
            stream->setCodec(QTextCodec::codecForLocale());
        }

        while (!stream->atEnd())
        {
            QString str = stream->readLine();
            matchingLineNumber++;

            //If the stream ended (readLine().isNull() is true) the file was read completely
            //Do *not* use isEmpty() because that will exit if there is an empty line in the file
            if (str.isNull()) {
                break;
            }
            if (isZippedOfficeDocument) {
                str.remove(xmlTags);
            }

            if (m_regexpForContent) {
                if (m_regexp.indexIn(str) >= 0) {
                    matchingLine = QString::number(matchingLineNumber)+QStringLiteral(": ")+str.trimmed();
                    found = true;
                    break;
                }
            } else {
                if (str.indexOf(m_context, 0, m_casesensitive ? Qt::CaseSensitive : Qt::CaseInsensitive) != -1) {
                    matchingLine = QString::number(matchingLineNumber)+QStringLiteral(": ")+str.trimmed();
                    found = true;
                    break;
                }
            }
            qApp->processEvents();
        }

        delete stream;

        if (!found) {
            return;
        }
    }

    m_foundFilesList.append(QPair<KFileItem, QString>(file, matchingLine));
}

void KQuery::setContext(const QString &context, bool casesensitive, bool search_binary, bool useRegexp)
{
    m_context = context;
    m_casesensitive = casesensitive;
    m_search_binary = search_binary;
    m_regexpForContent = useRegexp;
    if (!m_regexpForContent) {
        m_regexp.setPatternSyntax(QRegExp::Wildcard);
    } else {
        m_regexp.setPatternSyntax(QRegExp::RegExp);
    }
    if (casesensitive) {
        m_regexp.setCaseSensitivity(Qt::CaseSensitive);
    } else {
        m_regexp.setCaseSensitivity(Qt::CaseInsensitive);
    }
    if (m_regexpForContent) {
        m_regexp.setPattern(m_context);
    }
}

void KQuery::setMetaInfo(const QString &metainfo, const QString &metainfokey)
{
    m_metainfo = metainfo;
    m_metainfokey = metainfokey;
}

void KQuery::setMimeType(const QStringList &mimetype)
{
    m_mimetype = mimetype;
}

void KQuery::setFileType(int filetype)
{
    m_filetype = filetype;
}

void KQuery::setSizeRange(int mode, KIO::filesize_t value1, KIO::filesize_t value2)
{
    m_sizemode = mode;
    m_sizeboundary1 = value1;
    m_sizeboundary2 = value2;
}

void KQuery::setTimeRange(time_t from, time_t to)
{
    m_timeFrom = from;
    m_timeTo = to;
}

void KQuery::setUsername(const QString &username)
{
    m_username = username;
}

void KQuery::setGroupname(const QString &groupname)
{
    m_groupname = groupname;
}

void KQuery::setRegExp(const QString &regexp, bool caseSensitive)
{
    QRegExp *regExp = nullptr;
    QRegExp sep(QStringLiteral(";"));
    const QStringList strList = regexp.split(sep, QString::SkipEmptyParts);
    //  QRegExp globChars ("[\\*\\?\\[\\]]", TRUE, FALSE);
    while (!m_regexps.isEmpty()) {
        delete m_regexps.takeFirst();
    }

    //  m_regexpsContainsGlobs.clear();
    for (QStringList::ConstIterator it = strList.constBegin(); it != strList.constEnd(); ++it) {
        regExp = new QRegExp((*it), (caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive), QRegExp::Wildcard);
        //m_regexpsContainsGlobs.append(regExp->pattern().contains(globChars));
        m_regexps.append(regExp);
    }
}

void KQuery::setRecursive(bool recursive)
{
    m_recursive = recursive;
}

void KQuery::setPath(const QUrl &url)
{
    m_url = url;
}

void KQuery::setUseFileIndex(bool useLocate)
{
    m_useLocate = useLocate;
}

void KQuery::setShowHiddenFiles(bool showHidden)
{
    m_showHiddenFiles = showHidden;
}

void KQuery::slotreadyReadStandardError()
{
    KMessageBox::error(NULL, QString::fromLocal8Bit(processLocate->readAllStandardOutput()), i18nc("@title:window", "Error while using locate"));
}

void KQuery::slotreadyReadStandardOutput()
{
    bufferLocate += processLocate->readAllStandardOutput();
}

void KQuery::slotendProcessLocate(int code, QProcess::ExitStatus)
{
    if (code == 0) {
        if (!bufferLocate.isEmpty()) {
            QString str = QString::fromLocal8Bit(bufferLocate);
            bufferLocate.clear();
            slotListEntries(str.split(QLatin1Char('\n'), QString::SkipEmptyParts));
        }
    }
    emit result(0);
}
