// SPDX-License-Identifier: GPL-3.0-only
/*
 *  PolyMC - Minecraft Launcher
 *  Copyright (c) 2022 flowln <flowlnlnln@gmail.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, version 3.
 *
 *  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 <https://www.gnu.org/licenses/>.
 *
 * This file incorporates work covered by the following copyright and
 * permission notice:
 *
 *      Copyright 2013-2021 MultiMC Contributors
 *
 *      Licensed under the Apache License, Version 2.0 (the "License");
 *      you may not use this file except in compliance with the License.
 *      You may obtain a copy of the License at
 *
 *          http://www.apache.org/licenses/LICENSE-2.0
 *
 *      Unless required by applicable law or agreed to in writing, software
 *      distributed under the License is distributed on an "AS IS" BASIS,
 *      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *      See the License for the specific language governing permissions and
 *      limitations under the License.
 */

#include "NetJob.h"
#include "Download.h"

auto NetJob::addNetAction(NetAction::Ptr action) -> bool
{
    action->m_index_within_job = m_downloads.size();
    m_downloads.append(action);
    part_info pi;
    m_parts_progress.append(pi);

    partProgress(m_parts_progress.count() - 1, action->getProgress(), action->getTotalProgress());

    if (action->isRunning()) {
        connect(action.get(), &NetAction::succeeded, [this, action]{ partSucceeded(action->index()); });
        connect(action.get(), &NetAction::failed, [this, action](QString){ partFailed(action->index()); });
        connect(action.get(), &NetAction::aborted, [this, action](){ partAborted(action->index()); });
        connect(action.get(), &NetAction::progress, [this, action](qint64 done, qint64 total) { partProgress(action->index(), done, total); });
        connect(action.get(), &NetAction::status, this, &NetJob::status);
    } else {
        m_todo.append(m_parts_progress.size() - 1);
    }

    return true;
}

auto NetJob::canAbort() const -> bool
{
    bool canFullyAbort = true;

    // can abort the downloads on the queue?
    for (auto index : m_todo) {
        auto part = m_downloads[index];
        canFullyAbort &= part->canAbort();
    }
    // can abort the active downloads?
    for (auto index : m_doing) {
        auto part = m_downloads[index];
        canFullyAbort &= part->canAbort();
    }

    return canFullyAbort;
}

void NetJob::executeTask()
{
    // hack that delays early failures so they can be caught easier
    QMetaObject::invokeMethod(this, "startMoreParts", Qt::QueuedConnection);
}

auto NetJob::getFailedFiles() -> QStringList
{
    QStringList failed;
    for (auto index : m_failed) {
        failed.push_back(m_downloads[index]->url().toString());
    }
    failed.sort();
    return failed;
}

auto NetJob::abort() -> bool
{
    bool fullyAborted = true;

    // fail all downloads on the queue
    m_failed.unite(m_todo.toSet());
    m_todo.clear();

    // abort active downloads
    auto toKill = m_doing.toList();
    for (auto index : toKill) {
        auto part = m_downloads[index];
        fullyAborted &= part->abort();
    }

    return fullyAborted;
}

void NetJob::partSucceeded(int index)
{
    // do progress. all slots are 1 in size at least
    auto& slot = m_parts_progress[index];
    partProgress(index, slot.total_progress, slot.total_progress);

    m_doing.remove(index);
    m_done.insert(index);
    m_downloads[index].get()->disconnect(this);

    startMoreParts();
}

void NetJob::partFailed(int index)
{
    m_doing.remove(index);

    auto& slot = m_parts_progress[index];
    // Can try 3 times before failing by definitive
    if (slot.failures == 3) {
        m_failed.insert(index);
    } else {
        slot.failures++;
        m_todo.enqueue(index);
    }

    m_downloads[index].get()->disconnect(this);

    startMoreParts();
}

void NetJob::partAborted(int index)
{
    m_aborted = true;

    m_doing.remove(index);
    m_failed.insert(index);
    m_downloads[index].get()->disconnect(this);

    startMoreParts();
}

void NetJob::partProgress(int index, qint64 bytesReceived, qint64 bytesTotal)
{
    auto& slot = m_parts_progress[index];
    slot.current_progress = bytesReceived;
    slot.total_progress = bytesTotal;

    int done = m_done.size();
    int doing = m_doing.size();
    int all = m_parts_progress.size();

    qint64 bytesAll = 0;
    qint64 bytesTotalAll = 0;
    for (auto& partIdx : m_doing) {
        auto part = m_parts_progress[partIdx];
        // do not count parts with unknown/nonsensical total size
        if (part.total_progress <= 0) {
            continue;
        }
        bytesAll += part.current_progress;
        bytesTotalAll += part.total_progress;
    }

    qint64 inprogress = (bytesTotalAll == 0) ? 0 : (bytesAll * 1000) / bytesTotalAll;
    auto current = done * 1000 + doing * inprogress;
    auto current_total = all * 1000;
    // HACK: make sure it never jumps backwards.
    // FAIL: This breaks if the size is not known (or is it something else?) and jumps to 1000, so if it is 1000 reset it to inprogress
    if (m_current_progress == 1000) {
        m_current_progress = inprogress;
    }
    if (m_current_progress > current) {
        current = m_current_progress;
    }
    m_current_progress = current;
    setProgress(current, current_total);
}

void NetJob::startMoreParts()
{
    if (!isRunning()) {
        // this actually makes sense. You can put running m_downloads into a NetJob and then not start it until much later.
        return;
    }

    // OK. We are actively processing tasks, proceed.
    // Check for final conditions if there's nothing in the queue.
    if (!m_todo.size()) {
        if (!m_doing.size()) {
            if (!m_failed.size()) {
                emitSucceeded();
            } else if (m_aborted) {
                emitAborted();
            } else {
                emitFailed(tr("Job '%1' failed to process:\n%2").arg(objectName()).arg(getFailedFiles().join("\n")));
            }
        }
        return;
    }

    // There's work to do, try to start more parts, to a maximum of 6 concurrent ones.
    while (m_doing.size() < 6) {
        if (m_todo.size() == 0)
            return;
        int doThis = m_todo.dequeue();
        m_doing.insert(doThis);

        auto part = m_downloads[doThis];

        // connect signals :D
        connect(part.get(), &NetAction::succeeded, this, [this, part]{ partSucceeded(part->index()); });
        connect(part.get(), &NetAction::failed, this, [this, part](QString){ partFailed(part->index()); });
        connect(part.get(), &NetAction::aborted, this, [this, part]{ partAborted(part->index()); });
        connect(part.get(), &NetAction::progress, this, [this, part](qint64 done, qint64 total) { partProgress(part->index(), done, total); });
        connect(part.get(), &NetAction::status, this, &NetJob::status);

        part->startAction(m_network);
    }
}
