/*
    SPDX-FileCopyrightText: 2009 Joris Guisson <joris.guisson@gmail.com>
    SPDX-License-Identifier: GPL-2.0-or-later
*/

#include <KLocalizedString>
#include <QFile>

#include "shutdownruleset.h"
#include <bcodec/bdecoder.h>
#include <bcodec/bencoder.h>
#include <bcodec/bnode.h>
#include <torrent/queuemanager.h>
#include <util/error.h>
#include <util/file.h>
#include <util/log.h>
#include <util/sha1hash.h>

using namespace bt;

namespace kt
{
ShutdownRuleSet::ShutdownRuleSet(kt::CoreInterface *core, QObject *parent)
    : QObject(parent)
    , core(core)
    , on(false)
    , all_rules_must_be_hit(false)
{
    connect(core, &CoreInterface::torrentAdded, this, &ShutdownRuleSet::torrentAdded);
    connect(core, &CoreInterface::torrentRemoved, this, &ShutdownRuleSet::torrentRemoved);
    QueueManager *qman = core->getQueueManager();
    for (QueueManager::iterator i = qman->begin(); i != qman->end(); i++) {
        torrentAdded(*i);
    }
}

ShutdownRuleSet::~ShutdownRuleSet()
{
}

void ShutdownRuleSet::clear()
{
    rules.clear();
}

void ShutdownRuleSet::addRule(kt::Action action, kt::Target target, kt::Trigger trigger, bt::TorrentInterface *tc)
{
    ShutdownRule rule;
    rule.action = action;
    rule.target = target;
    rule.trigger = trigger;
    rule.tc = tc;
    rule.hit = false;
    rules.append(rule);
}

void ShutdownRuleSet::torrentFinished(bt::TorrentInterface *tc)
{
    triggered(DOWNLOADING_COMPLETED, tc);
}

void ShutdownRuleSet::seedingAutoStopped(bt::TorrentInterface *tc, bt::AutoStopReason reason)
{
    Q_UNUSED(reason);
    triggered(SEEDING_COMPLETED, tc);
}

void ShutdownRuleSet::triggered(Trigger trigger, TorrentInterface *tc)
{
    if (!on) {
        return;
    }

    bool hit = false;
    bool all_hit = true;
    for (QList<ShutdownRule>::iterator i = rules.begin(); i != rules.end(); i++) {
        bool rule_hit = false;
        if (trigger == DOWNLOADING_COMPLETED) {
            rule_hit = i->downloadingFinished(tc, core->getQueueManager());
        } else {
            rule_hit = i->seedingFinished(tc, core->getQueueManager());
        }

        if (rule_hit) {
            hit = true;
        } else if (!i->hit) {
            all_hit = false;
        }
    }

    if ((!all_rules_must_be_hit && hit) || (all_rules_must_be_hit && all_hit)) {
        switch (currentAction()) {
        case QUIT_APP:
            Q_EMIT quitApp();
            break;
        case SHUTDOWN:
            Q_EMIT shutdown();
            break;
        case LOCK:
            Q_EMIT lock();
            break;
        case SUSPEND_TO_DISK:
            Q_EMIT suspendToDisk();
            break;
        case SUSPEND_TO_RAM:
            Q_EMIT suspendToRAM();
            break;
        }
    }
}

void ShutdownRuleSet::torrentAdded(bt::TorrentInterface *tc)
{
    connect(tc, &bt::TorrentInterface::seedingAutoStopped, this, &ShutdownRuleSet::seedingAutoStopped);
    connect(tc, &bt::TorrentInterface::finished, this, &ShutdownRuleSet::torrentFinished);
}

void ShutdownRuleSet::torrentRemoved(bt::TorrentInterface *tc)
{
    // Throw away all rules for this torrent
    for (QList<ShutdownRule>::iterator i = rules.begin(); i != rules.end();) {
        if (i->tc == tc) {
            i = rules.erase(i);
        } else {
            i++;
        }
    }
}

void ShutdownRuleSet::setEnabled(bool on)
{
    this->on = on;
}

void ShutdownRuleSet::save(const QString &file)
{
    File fptr;
    if (!fptr.open(file, QStringLiteral("wt"))) {
        Out(SYS_GEN | LOG_DEBUG) << "Failed to open file " << file << " : " << fptr.errorString() << endl;
        return;
    }

    BEncoder enc(std::make_unique<BEncoderFileOutput>(&fptr));
    enc.beginList();
    for (QList<ShutdownRule>::iterator i = rules.begin(); i != rules.end(); i++) {
        enc.beginDict();
        enc.write("Action", (bt::Uint32)i->action);
        enc.write("Trigger", (bt::Uint32)i->trigger);
        enc.write("Target", (bt::Uint32)i->target);
        if (i->target == SPECIFIC_TORRENT) {
            enc.write("Torrent", i->tc->getInfoHash());
        }
        enc.write("hit", i->hit);
        enc.end();
    }
    enc.write(on);
    enc.write(all_rules_must_be_hit);
    enc.end();
}

void ShutdownRuleSet::load(const QString &file)
{
    QFile fptr(file);
    if (!fptr.open(QIODevice::ReadOnly)) {
        Out(SYS_GEN | LOG_DEBUG) << "Failed to open file " << file << " : " << fptr.errorString() << endl;
        return;
    }

    QByteArray data = fptr.readAll();
    BDecoder dec(data, false);
    try {
        clear();
        const std::unique_ptr<BListNode> list = dec.decodeList();
        if (!list) {
            throw bt::Error(QStringLiteral("Toplevel node not a list"));
        }

        BListNode *const l = list.get();
        Uint32 i = 0;
        for (; i < l->getNumChildren(); ++i) {
            if (l->getChild(i)->getType() != BNode::DICT) {
                break;
            }

            BDictNode *const d = l->getDict(i);
            if (!d) {
                continue;
            }

            ShutdownRule rule;
            rule.action = (Action)d->getInt("Action");
            rule.target = (Target)d->getInt("Target");
            rule.trigger = (Trigger)d->getInt("Trigger");
            rule.hit = d->keys().contains(QByteArrayLiteral("hit")) && d->getInt("hit") == 1;
            rule.tc = nullptr;
            if (d->getValue("Torrent")) {
                const QByteArray hash = d->getByteArray("Torrent");
                bt::TorrentInterface *const tc = torrentForHash(hash);
                if (tc) {
                    rule.tc = tc;
                } else {
                    continue; // no valid torrent found so skip this rule
                }
            }
            rules.append(rule);
        }

        on = (l->getInt(i++) == 1);
        if (i < l->getNumChildren()) {
            all_rules_must_be_hit = (l->getInt(i) == 1);
        } else {
            all_rules_must_be_hit = false;
        }
    } catch (bt::Error &err) {
        Out(SYS_GEN | LOG_DEBUG) << "Failed to parse " << file << " : " << err.toString() << endl;
    }
}

bt::TorrentInterface *ShutdownRuleSet::torrentForHash(const QByteArray &hash)
{
    bt::SHA1Hash ih((const bt::Uint8 *)hash.data());
    QueueManager *qman = core->getQueueManager();
    for (QueueManager::iterator i = qman->begin(); i != qman->end(); i++) {
        bt::TorrentInterface *t = *i;
        if (t->getInfoHash() == ih) {
            return t;
        }
    }

    return nullptr;
}

kt::Action ShutdownRuleSet::currentAction() const
{
    if (rules.count() == 0) {
        return SHUTDOWN;
    } else {
        return rules.front().action;
    }
}

QString ShutdownRuleSet::toolTip() const
{
    if (rules.isEmpty()) {
        return i18n("Automatic shutdown not active");
    } else {
        QString msg;
        Action action = currentAction();
        switch (action) {
        case QUIT_APP:
            msg = i18n("Quit KTorrent");
            break;
        case SHUTDOWN:
            msg = i18n("Shutdown");
            break;
        case LOCK:
            msg = i18n("Lock");
            break;
        case SUSPEND_TO_RAM:
            msg = i18n("Sleep (suspend to RAM)");
            break;
        case SUSPEND_TO_DISK:
            msg = i18n("Hibernate (suspend to disk)");
            break;
        }

        if (all_rules_must_be_hit) {
            msg += i18n(" when all of the following events have occurred:<br/><br/> ");
        } else {
            msg += i18n(" when one of the following events occur:<br/><br/> ");
        }

        QStringList items;
        for (const ShutdownRule &r : std::as_const(rules)) {
            items += QStringLiteral("- ") + r.toolTip();
        }

        msg += items.join(QStringLiteral("<br/>"));
        return msg;
    }
}

//////////////////////////////////////

bool ShutdownRule::downloadingFinished(bt::TorrentInterface *tor, QueueManager *qman)
{
    if (target != ALL_TORRENTS && tc != tor) {
        return false;
    }

    if (trigger != DOWNLOADING_COMPLETED) {
        return false;
    }

    if (target != ALL_TORRENTS) {
        hit = tc == tor;
        return hit;
    } else {
        // target is all torrents, so check if all torrents have completed downloading
        for (QueueManager::iterator i = qman->begin(); i != qman->end(); i++) {
            bt::TorrentInterface *t = *i;
            const bt::TorrentStats &stats = t->getStats();
            if (t != tor && !stats.completed && stats.running) {
                return false;
            }
        }

        hit = true;
        return true;
    }
}

bool ShutdownRule::seedingFinished(bt::TorrentInterface *tor, QueueManager *qman)
{
    if (target != ALL_TORRENTS && tc != tor) {
        return false;
    }

    if (trigger != SEEDING_COMPLETED) {
        return false;
    }

    if (target != ALL_TORRENTS) {
        hit = tc == tor;
        return hit;
    } else {
        // target is all torrents, so check if all torrents have completed seeding
        for (QueueManager::iterator i = qman->begin(); i != qman->end(); i++) {
            bt::TorrentInterface *t = *i;
            if (t == tor) {
                continue;
            }

            const bt::TorrentStats &stats = t->getStats();
            if (stats.running) {
                return false;
            }
        }

        hit = true;
        return true;
    }
}

QString ShutdownRule::toolTip() const
{
    if (target == ALL_TORRENTS && trigger == kt::DOWNLOADING_COMPLETED) {
        return i18n("<b>All torrents</b> finish downloading");
    } else if (target == ALL_TORRENTS && trigger == kt::SEEDING_COMPLETED) {
        return i18n("<b>All torrents</b> finish seeding");
    } else if (target == SPECIFIC_TORRENT && trigger == kt::DOWNLOADING_COMPLETED) {
        return i18n("<b>%1</b> finishes downloading", tc->getDisplayName());
    } else if (target == SPECIFIC_TORRENT && trigger == kt::SEEDING_COMPLETED) {
        return i18n("<b>%1</b> finishes seeding", tc->getDisplayName());
    } else {
        return QString();
    }
}

}

#include "moc_shutdownruleset.cpp"
