// SPDX-License-Identifier: GPL-3.0-only
/*
 *  PolyMC - Minecraft Launcher
 *  Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
 *
 *  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 <meta/VersionList.h>
#include <meta/Index.h>
#include "Component.h"

#include <QSaveFile>

#include "meta/Version.h"
#include "VersionFile.h"
#include "minecraft/PackProfile.h"
#include "FileSystem.h"
#include "OneSixVersionFormat.h"
#include "Application.h"

#include <assert.h>

Component::Component(PackProfile * parent, const QString& uid)
{
    assert(parent);
    m_parent = parent;

    m_uid = uid;
}

Component::Component(PackProfile * parent, std::shared_ptr<Meta::Version> version)
{
    assert(parent);
    m_parent = parent;

    m_metaVersion = version;
    m_uid = version->uid();
    m_version = m_cachedVersion = version->version();
    m_cachedName = version->name();
    m_loaded = version->isLoaded();
}

Component::Component(PackProfile * parent, const QString& uid, std::shared_ptr<VersionFile> file)
{
    assert(parent);
    m_parent = parent;

    m_file = file;
    m_uid = uid;
    m_cachedVersion = m_file->version;
    m_cachedName = m_file->name;
    m_loaded = true;
}

std::shared_ptr<Meta::Version> Component::getMeta()
{
    return m_metaVersion;
}

void Component::applyTo(LaunchProfile* profile)
{
    // do not apply disabled components
    if(!isEnabled())
    {
        return;
    }
    auto vfile = getVersionFile();
    if(vfile)
    {
        vfile->applyTo(profile, m_parent->runtimeContext());
    }
    else
    {
        profile->applyProblemSeverity(getProblemSeverity());
    }
}

std::shared_ptr<class VersionFile> Component::getVersionFile() const
{
    if(m_metaVersion)
    {
        if(!m_metaVersion->isLoaded())
        {
            m_metaVersion->load(Net::Mode::Online);
        }
        return m_metaVersion->data();
    }
    else
    {
        return m_file;
    }
}

std::shared_ptr<class Meta::VersionList> Component::getVersionList() const
{
    // FIXME: what if the metadata index isn't loaded yet?
    if(APPLICATION->metadataIndex()->hasUid(m_uid))
    {
        return APPLICATION->metadataIndex()->get(m_uid);
    }
    return nullptr;
}

int Component::getOrder()
{
    if(m_orderOverride)
        return m_order;

    auto vfile = getVersionFile();
    if(vfile)
    {
        return vfile->order;
    }
    return 0;
}
void Component::setOrder(int order)
{
    m_orderOverride = true;
    m_order = order;
}
QString Component::getID()
{
    return m_uid;
}
QString Component::getName()
{
    if (!m_cachedName.isEmpty())
        return m_cachedName;
    return m_uid;
}
QString Component::getVersion()
{
    return m_cachedVersion;
}
QString Component::getFilename()
{
    return m_parent->patchFilePathForUid(m_uid);
}
QDateTime Component::getReleaseDateTime()
{
    if(m_metaVersion)
    {
        return m_metaVersion->time();
    }
    auto vfile = getVersionFile();
    if(vfile)
    {
        return vfile->releaseTime;
    }
    // FIXME: fake
    return QDateTime::currentDateTime();
}

bool Component::isEnabled()
{
    return !canBeDisabled() || !m_disabled;
}

bool Component::canBeDisabled()
{
    return isRemovable() && !m_dependencyOnly;
}

bool Component::setEnabled(bool state)
{
    bool intendedDisabled = !state;
    if (!canBeDisabled())
    {
        intendedDisabled = false;
    }
    if(intendedDisabled != m_disabled)
    {
        m_disabled = intendedDisabled;
        emit dataChanged();
        return true;
    }
    return false;
}

bool Component::isCustom()
{
    return m_file != nullptr;
}

bool Component::isCustomizable()
{
    if(m_metaVersion)
    {
        if(getVersionFile())
        {
            return true;
        }
    }
    return false;
}
bool Component::isRemovable()
{
    return !m_important;
}
bool Component::isRevertible()
{
    if (isCustom())
    {
        if(APPLICATION->metadataIndex()->hasUid(m_uid))
        {
            return true;
        }
    }
    return false;
}
bool Component::isMoveable()
{
    // HACK, FIXME: this was too dumb and wouldn't follow dependency constraints anyway. For now hardcoded to 'true'.
    return true;
}
bool Component::isVersionChangeable()
{
    auto list = getVersionList();
    if(list)
    {
        if(!list->isLoaded())
        {
            list->load(Net::Mode::Online);
        }
        return list->count() != 0;
    }
    return false;
}

void Component::setImportant(bool state)
{
    if(m_important != state)
    {
        m_important = state;
        emit dataChanged();
    }
}

ProblemSeverity Component::getProblemSeverity() const
{
    auto file = getVersionFile();
    if(file)
    {
        return file->getProblemSeverity();
    }
    return ProblemSeverity::Error;
}

const QList<PatchProblem> Component::getProblems() const
{
    auto file = getVersionFile();
    if(file)
    {
        return file->getProblems();
    }
    return {{ProblemSeverity::Error, QObject::tr("Patch is not loaded yet.")}};
}

void Component::setVersion(const QString& version)
{
    if(version == m_version)
    {
        return;
    }
    m_version = version;
    if(m_loaded)
    {
        // we are loaded and potentially have state to invalidate
        if(m_file)
        {
            // we have a file... explicit version has been changed and there is nothing else to do.
        }
        else
        {
            // we don't have a file, therefore we are loaded with metadata
            m_cachedVersion = version;
            // see if the meta version is loaded
            auto metaVersion = APPLICATION->metadataIndex()->get(m_uid, version);
            if(metaVersion->isLoaded())
            {
                // if yes, we can continue with that.
                m_metaVersion = metaVersion;
            }
            else
            {
                // if not, we need loading
                m_metaVersion.reset();
                m_loaded = false;
            }
            updateCachedData();
        }
    }
    else
    {
        // not loaded... assume it will be sorted out later by the update task
    }
    emit dataChanged();
}

bool Component::customize()
{
    if(isCustom())
    {
        return false;
    }

    auto filename = getFilename();
    if(!FS::ensureFilePathExists(filename))
    {
        return false;
    }
    // FIXME: get rid of this try-catch.
    try
    {
        QSaveFile jsonFile(filename);
        if(!jsonFile.open(QIODevice::WriteOnly))
        {
            return false;
        }
        auto vfile = getVersionFile();
        if(!vfile)
        {
            return false;
        }
        auto document = OneSixVersionFormat::versionFileToJson(vfile);
        jsonFile.write(document.toJson());
        if(!jsonFile.commit())
        {
            return false;
        }
        m_file = vfile;
        m_metaVersion.reset();
        emit dataChanged();
    }
    catch (const Exception &error)
    {
        qWarning() << "Version could not be loaded:" << error.cause();
    }
    return true;
}

bool Component::revert()
{
    if(!isCustom())
    {
        // already not custom
        return true;
    }
    auto filename = getFilename();
    bool result = true;
    // just kill the file and reload
    if(QFile::exists(filename))
    {
        result = QFile::remove(filename);
    }
    if(result)
    {
        // file gone...
        m_file.reset();

        // check local cache for metadata...
        auto version = APPLICATION->metadataIndex()->get(m_uid, m_version);
        if(version->isLoaded())
        {
            m_metaVersion = version;
        }
        else
        {
            m_metaVersion.reset();
            m_loaded = false;
        }
        emit dataChanged();
    }
    return result;
}

/**
 * deep inspecting compare for requirement sets
 * By default, only uids are compared for set operations.
 * This compares all fields of the Require structs in the sets.
 */
static bool deepCompare(const std::set<Meta::Require> & a, const std::set<Meta::Require> & b)
{
    // NOTE: this needs to be rewritten if the type of Meta::RequireSet changes
    if(a.size() != b.size())
    {
        return false;
    }
    for(const auto & reqA :a)
    {
        const auto &iter2 = b.find(reqA);
        if(iter2 == b.cend())
        {
            return false;
        }
        const auto & reqB = *iter2;
        if(!reqA.deepEquals(reqB))
        {
            return false;
        }
    }
    return true;
}

void Component::updateCachedData()
{
    auto file = getVersionFile();
    if(file)
    {
        bool changed = false;
        if(m_cachedName != file->name)
        {
            m_cachedName = file->name;
            changed = true;
        }
        if(m_cachedVersion != file->version)
        {
            m_cachedVersion = file->version;
            changed = true;
        }
        if(m_cachedVolatile != file->m_volatile)
        {
            m_cachedVolatile = file->m_volatile;
            changed = true;
        }
        if(!deepCompare(m_cachedRequires, file->required))
        {
            m_cachedRequires = file->required;
            changed = true;
        }
        if(!deepCompare(m_cachedConflicts, file->conflicts))
        {
            m_cachedConflicts = file->conflicts;
            changed = true;
        }
        if(changed)
        {
            emit dataChanged();
        }
    }
    else
    {
        // in case we removed all the metadata
        m_cachedRequires.clear();
        m_cachedConflicts.clear();
        emit dataChanged();
    }
}
