/* ============================================================
* Copyright (c) 2013, 2014 Montel Laurent <montel@kde.org>
* based on code from rekonq
* This file is a part of the rekonq project
*
* Copyright (C) 2010-2012 by Andrea Diamantini <adjam7 at gmail dot com>
*
*
* 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) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* 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/>.
*
* ============================================================ */


// Self Includes
#include "adblockmanager.h"

#include "settings/globalsettings.h"
#include "adblock/adblockutil.h"

#include "webpage.h"

// KDE Includes
#include <KIO/FileCopyJob>
#include <KStandardDirs>
#include <KNotification>

// Qt Includes
#include <QUrl>
#include <QTimer>
#include <QWebElement>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QtConcurrentRun>
#include <QFile>
#include <QDateTime>
#include <QWebFrame>

using namespace MessageViewer;
QWeakPointer<AdBlockManager> AdBlockManager::s_adBlockManager;


AdBlockManager *AdBlockManager::self()
{
    if (s_adBlockManager.isNull())
    {
        s_adBlockManager = new AdBlockManager(qApp);
    }
    return s_adBlockManager.data();
}


// ----------------------------------------------------------------------------------------------


AdBlockManager::AdBlockManager(QObject *parent)
    : QObject(parent)
{
    loadSettings();
}


AdBlockManager::~AdBlockManager()
{
    _whiteList.clear();
    _blackList.clear();
}


bool AdBlockManager::isEnabled()
{
    return GlobalSettings::self()->adBlockEnabled();
}


bool AdBlockManager::isHidingElements()
{
    return GlobalSettings::self()->hideAdsEnabled();
}

void AdBlockManager::reloadConfig()
{
    loadSettings();
}

void AdBlockManager::loadSettings()
{
    KConfig config(QLatin1String("messagevieweradblockrc"));
    // ----------------

    _hostWhiteList.clear();
    _hostBlackList.clear();

    _whiteList.clear();
    _blackList.clear();

    _elementHiding.clear();

    if (!isEnabled())
        return;
    // ----------------------------------------------------------

    QDateTime today = QDateTime::currentDateTime();
    const int days = GlobalSettings::self()->adBlockUpdateInterval();

    const QStringList itemList = config.groupList().filter( QRegExp( QLatin1String("FilterList \\d+") ) );
    Q_FOREACH(const QString &item, itemList) {
        KConfigGroup filtersGroup(&config, item);
        const bool isFilterEnabled = filtersGroup.readEntry(QLatin1String("FilterEnabled"), false);
        if (!isFilterEnabled) {
            continue;
        }
        const QString url = filtersGroup.readEntry(QLatin1String("url"));
        if (url.isEmpty()) {
            continue;
        }
        const QString path = filtersGroup.readEntry(QLatin1String("path"));
        if (path.isEmpty())
            continue;

        const QDateTime lastDateTime = filtersGroup.readEntry(QLatin1String("lastUpdate"), QDateTime());
        if (!lastDateTime.isValid() || today > lastDateTime.addDays(days) || !QFile(path).exists()) {
            updateSubscription(path, url, item);
        } else {
            loadRules(path);
        }
    }

    // load local rules
    const QString localRulesFilePath = MessageViewer::AdBlockUtil::localFilterPath();
    loadRules(localRulesFilePath);
}


void AdBlockManager::loadRules(const QString &rulesFilePath)
{
    QFile ruleFile(rulesFilePath);
    if (!ruleFile.open(QFile::ReadOnly | QFile::Text)) {
        qDebug() << "Unable to open rule file" << rulesFilePath;
        return;
    }

    QTextStream in(&ruleFile);
    while (!in.atEnd())
    {
        QString stringRule = in.readLine();
        loadRuleString(stringRule);
    }
}


void AdBlockManager::loadRuleString(const QString &stringRule)
{
    // ! rules are comments
    if (stringRule.startsWith(QLatin1Char('!')))
        return;

    // [ rules are ABP info
    if (stringRule.startsWith(QLatin1Char('[')))
        return;

    // empty rules are just dangerous..
    // (an empty rule in whitelist allows all, in blacklist blocks all..)
    if (stringRule.isEmpty())
        return;

    // white rules
    if (stringRule.startsWith(QLatin1String("@@")))
    {
        if (_hostWhiteList.tryAddFilter(stringRule))
            return;

        const QString filter = stringRule.mid(2);
        if (filter.isEmpty())
            return;

        AdBlockRule rule(filter);
        _whiteList << rule;
        return;
    }

    // hide (CSS) rules
    if (stringRule.contains(QLatin1String("##")))
    {
        _elementHiding.addRule(stringRule);
        return;
    }

    if (_hostBlackList.tryAddFilter(stringRule))
        return;

    AdBlockRule rule(stringRule);
    _blackList << rule;
}


bool AdBlockManager::blockRequest(const QNetworkRequest &request)
{
    if (!isEnabled())
        return false;

    // we (ad)block just http & https traffic
    if (request.url().scheme() != QLatin1String("http")
            && request.url().scheme() != QLatin1String("https"))
        return false;

    const QStringList whiteRefererList = GlobalSettings::self()->whiteReferer();
    const QString referer = QString::fromLatin1(request.rawHeader("referer"));
    Q_FOREACH(const QString & host, whiteRefererList)
    {
        if (referer.contains(host))
            return false;
    }

    QString urlString = request.url().toString();
    // We compute a lowercase version of the URL so each rule does not
    // have to do it.
    const QString urlStringLowerCase = urlString.toLower();
    const QString host = request.url().host();

    // check white rules before :)
    if (_hostWhiteList.match(host))
    {
        kDebug() << "ADBLOCK: WHITE RULE (@@) Matched by string: " << urlString;
        return false;
    }

    Q_FOREACH(const AdBlockRule & filter, _whiteList)
    {
        if (filter.match(request, urlString, urlStringLowerCase))
        {
            kDebug() << "ADBLOCK: WHITE RULE (@@) Matched by string: " << urlString;
            return false;
        }
    }

    // then check the black ones :(
    if (_hostBlackList.match(host))
    {
        kDebug() << "ADBLOCK: BLACK RULE Matched by string: " << urlString;
        return true;
    }

    Q_FOREACH(const AdBlockRule & filter, _blackList)
    {
        if (filter.match(request, urlString, urlStringLowerCase))
        {
            kDebug() << "ADBLOCK: BLACK RULE Matched by string: " << urlString;
            return true;
        }
    }

    // no match
    return false;
}


void AdBlockManager::updateSubscription(const QString &path, const QString &url, const QString &itemName)
{
    KUrl subUrl = KUrl(url);

    const QString rulesFilePath = path;
    KUrl destUrl = KUrl(rulesFilePath);

    KIO::FileCopyJob* job = KIO::file_copy(subUrl , destUrl, -1, KIO::HideProgressInfo | KIO::Overwrite);
    job->metaData().insert(QLatin1String("ssl_no_client_cert"), QLatin1String("TRUE"));
    job->metaData().insert(QLatin1String("ssl_no_ui"), QLatin1String("TRUE"));
    job->metaData().insert(QLatin1String("UseCache"), QLatin1String("false"));
    job->metaData().insert(QLatin1String("cookies"), QLatin1String("none"));
    job->metaData().insert(QLatin1String("no-auth"), QLatin1String("true"));
    job->setProperty("itemname", itemName);

    connect(job, SIGNAL(finished(KJob*)), this, SLOT(slotFinished(KJob*)));
}


void AdBlockManager::slotFinished(KJob *job)
{
    if (job->error()) {
        KNotification *notify = new KNotification( QLatin1String("adblock-list-download-failed") );
        notify->setComponentData( KComponentData("messageviewer") );
        notify->setText( i18n("Download new ad-block list was failed." ) );
        notify->sendEvent();
        return;
    }

    KNotification *notify = new KNotification( QLatin1String("adblock-list-download-done") );
    notify->setComponentData( KComponentData("messageviewer") );
    notify->setText( i18n("Download new ad-block list was done." ) );
    notify->sendEvent();
    const QString itemName = job->property("itemname").toString();
    if (!itemName.isEmpty()) {
        KConfig config(QLatin1String("messagevieweradblockrc"));
        if (config.hasGroup(itemName)) {
            KConfigGroup grp = config.group(itemName);
            grp.writeEntry(QLatin1String("lastUpdate"), QDateTime::currentDateTime());
        }
    }

    KIO::FileCopyJob *fJob = qobject_cast<KIO::FileCopyJob *>(job);
    KUrl url = fJob->destUrl();
    url.setProtocol(QString()); // this is needed to load local url well :(
    loadRules(url.url());
}


bool AdBlockManager::subscriptionFileExists(int i)
{
    const QString n = QString::number(i + 1);

    const QString rulesFilePath = KStandardDirs::locateLocal("data" , QLatin1String("kmail2/adblockrules_") + n);
    return QFile::exists(rulesFilePath);
}

void AdBlockManager::addCustomRule(const QString &stringRule, bool reloadPage)
{
    // save rule in local filters
    const QString localRulesFilePath = MessageViewer::AdBlockUtil::localFilterPath();

    QFile ruleFile(localRulesFilePath);
    if (!ruleFile.open(QFile::ReadOnly)) {
        kDebug() << "Unable to open rule file" << localRulesFilePath;
        return;
    }

    QTextStream in(&ruleFile);
    while (!in.atEnd()) {
        QString readStringRule = in.readLine();
        if (stringRule == readStringRule) {
            ruleFile.close();
            return;
        }
    }
    ruleFile.close();
    if (!ruleFile.open(QFile::WriteOnly | QFile::Append)) {
        kDebug() << "Unable to open rule file" << localRulesFilePath;
        return;
    }

    QTextStream out(&ruleFile);
    out << stringRule << '\n';

    ruleFile.close();

    // load it
    loadRuleString(stringRule);

    // eventually reload page
    if (reloadPage)
        emit reloadCurrentPage();
}


bool AdBlockManager::isAdblockEnabledForHost(const QString &host)
{
    if (!isEnabled())
        return false;

    return ! _hostWhiteList.match(host);
}


void AdBlockManager::applyHidingRules(QWebFrame *frame)
{
    if (!frame)
        return;

    if (!isEnabled())
        return;

    connect(frame, SIGNAL(loadFinished(bool)), this, SLOT(applyHidingRules(bool)));
}


void AdBlockManager::applyHidingRules(bool ok)
{
    if (!ok)
        return;

    QWebFrame *frame = qobject_cast<QWebFrame *>(sender());
    if (!frame)
        return;
    MessageViewer::WebPage *page = qobject_cast<MessageViewer::WebPage *>(frame->page());
    if (!page)
        return;

    QString mainPageHost = page->loadingUrl().host();
    const QStringList hosts = GlobalSettings::self()->whiteReferer();
    if (hosts.contains(mainPageHost))
        return;

    QWebElement document = frame->documentElement();

    _elementHiding.apply(document, mainPageHost);
}

