/* Copyright 2013-2021 MultiMC Contributors
 *
 * Authors: Orochimarufan <orochimarufan.x3@gmail.com>
 *
 * 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 "MinecraftAccount.h"
#include "flows/AuthContext.h"

#include <QUuid>
#include <QJsonObject>
#include <QJsonArray>
#include <QRegExp>
#include <QStringList>
#include <QJsonDocument>

#include <QDebug>

#include <QCryptographicHash>

#include <QPainter>
#include <minecraft/auth/flows/MSASilent.h>
#include <minecraft/auth/flows/MSAInteractive.h>

#include <minecraft/auth/flows/MojangRefresh.h>
#include <minecraft/auth/flows/MojangLogin.h>

MinecraftAccountPtr MinecraftAccount::loadFromJsonV2(const QJsonObject& json) {
    MinecraftAccountPtr account(new MinecraftAccount());
    if(account->data.resumeStateFromV2(json)) {
        return account;
    }
    return nullptr;
}

MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json) {
    MinecraftAccountPtr account(new MinecraftAccount());
    if(account->data.resumeStateFromV3(json)) {
        return account;
    }
    return nullptr;
}

MinecraftAccountPtr MinecraftAccount::createFromUsername(const QString &username)
{
    MinecraftAccountPtr account(new MinecraftAccount());
    account->data.type = AccountType::Mojang;
    account->data.yggdrasilToken.extra["userName"] = username;
    account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegExp("[{}-]"));
    return account;
}

MinecraftAccountPtr MinecraftAccount::createFromUsernameOffline(const QString &username)
{
    // Offline mode based by this code:
    // https://github.com/MultiMC/MultiMC5/commit/6ede3c13b2bcda315e65dd78f2bfd729bc8b699b
    MinecraftAccountPtr account(new MinecraftAccount());
    account->data.type = AccountType::Mojang;
    account->data.yggdrasilToken.extra["userName"] = username;
    account->data.yggdrasilToken.extra["clientToken"] = "ff64ff64ff64ff64ff64ff64ff64ff64";
    account->data.yggdrasilToken.token = "ff64ff64ff64ff64ff64ff64ff64ff64";
    account->data.minecraftProfile.name = username;
    account->data.minecraftProfile.id = QCryptographicHash::hash(username.toLocal8Bit(), QCryptographicHash::Md5).toHex();
    return account;
}

MinecraftAccountPtr MinecraftAccount::createBlankMSA()
{
    MinecraftAccountPtr account(new MinecraftAccount());
    account->data.type = AccountType::MSA;
    return account;
}


QJsonObject MinecraftAccount::saveToJson() const
{
    return data.saveState();
}

AccountStatus MinecraftAccount::accountStatus() const {
    if(data.type == AccountType::Mojang) {
        if (data.accessToken().isEmpty()) {
            return NotVerified;
        }
        else {
            return Verified;
        }
    }
    // MSA
    // FIXME: this is extremely crude and probably wrong
    if(data.msaToken.token.isEmpty()) {
        return NotVerified;
    }
    else {
        return Verified;
    }
}

QPixmap MinecraftAccount::getFace() const {
    QPixmap skinTexture;
    if(!skinTexture.loadFromData(data.minecraftProfile.skin.data, "PNG")) {
        return QPixmap();
    }
    QPixmap skin = QPixmap(8, 8);
    QPainter painter(&skin);
    painter.drawPixmap(0, 0, skinTexture.copy(8, 8, 8, 8));
    painter.drawPixmap(0, 0, skinTexture.copy(40, 8, 8, 8));
    return skin.scaled(64, 64, Qt::KeepAspectRatio);
}


std::shared_ptr<AccountTask> MinecraftAccount::login(AuthSessionPtr session, QString password)
{
    Q_ASSERT(m_currentTask.get() == nullptr);

    // take care of the true offline status
    if (accountStatus() == NotVerified && password.isEmpty())
    {
        if (session)
        {
            session->status = AuthSession::RequiresPassword;
            fillSession(session);
        }
        return nullptr;
    }

    if(accountStatus() == Verified && !session->wants_online)
    {
        session->status = AuthSession::PlayableOffline;
        session->auth_server_online = false;
        fillSession(session);
        return nullptr;
    }
    else
    {
        if (password.isEmpty())
        {
            m_currentTask.reset(new MojangRefresh(&data));
        }
        else
        {
            m_currentTask.reset(new MojangLogin(&data, password));
        }
        m_currentTask->assignSession(session);

        connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
        connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
    }
    return m_currentTask;
}

std::shared_ptr<AccountTask> MinecraftAccount::loginMSA(AuthSessionPtr session) {
    Q_ASSERT(m_currentTask.get() == nullptr);

    if(accountStatus() == Verified && !session->wants_online)
    {
        session->status = AuthSession::PlayableOffline;
        session->auth_server_online = false;
        fillSession(session);
        return nullptr;
    }
    else
    {
        m_currentTask.reset(new MSAInteractive(&data));
        m_currentTask->assignSession(session);

        connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
        connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
    }
    return m_currentTask;
}

std::shared_ptr<AccountTask> MinecraftAccount::refresh(AuthSessionPtr session) {
    Q_ASSERT(m_currentTask.get() == nullptr);

    // take care of the true offline status
    if (accountStatus() == NotVerified)
    {
        if (session)
        {
            if(data.type == AccountType::MSA) {
                session->status = AuthSession::RequiresOAuth;
            }
            else {
                session->status = AuthSession::RequiresPassword;
            }
            fillSession(session);
        }
        return nullptr;
    }

    if(accountStatus() == Verified && !session->wants_online)
    {
        session->status = AuthSession::PlayableOffline;
        session->auth_server_online = false;
        fillSession(session);
        return nullptr;
    }
    else
    {
        if(data.type == AccountType::MSA) {
            m_currentTask.reset(new MSASilent(&data));
        }
        else {
            m_currentTask.reset(new MojangRefresh(&data));
        }
        m_currentTask->assignSession(session);

        connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
        connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
    }
    return m_currentTask;
}


void MinecraftAccount::authSucceeded()
{
    auto session = m_currentTask->getAssignedSession();
    if (session)
    {
        session->status =
            session->wants_online ? AuthSession::PlayableOnline : AuthSession::PlayableOffline;
        fillSession(session);
        session->auth_server_online = true;
    }
    m_currentTask.reset();
    emit changed();
}

void MinecraftAccount::authFailed(QString reason)
{
    auto session = m_currentTask->getAssignedSession();
    // This is emitted when the yggdrasil tasks time out or are cancelled.
    // -> we treat the error as no-op
    switch (m_currentTask->accountState()) {
        case AccountTask::STATE_FAILED_SOFT: {
            if (session)
            {
                if(accountStatus() == Verified) {
                    session->status = AuthSession::PlayableOffline;
                }
                else {
                    if(data.type == AccountType::MSA) {
                        session->status = AuthSession::RequiresOAuth;
                    }
                    else {
                        session->status = AuthSession::RequiresPassword;
                    }
                }
                session->auth_server_online = false;
                fillSession(session);
            }
        }
        break;
        case AccountTask::STATE_FAILED_HARD: {
            // FIXME: MSA data clearing
            data.yggdrasilToken.token = QString();
            data.yggdrasilToken.validity = Katabasis::Validity::None;
            data.validity_ = Katabasis::Validity::None;
            emit changed();
            if (session)
            {
                if(data.type == AccountType::MSA) {
                    session->status = AuthSession::RequiresOAuth;
                }
                else {
                    session->status = AuthSession::RequiresPassword;
                }
                session->auth_server_online = true;
                fillSession(session);
            }
        }
        break;
        case AccountTask::STATE_FAILED_GONE: {
            data.validity_ = Katabasis::Validity::None;
            emit changed();
            if (session)
            {
                session->status = AuthSession::GoneOrMigrated;
                session->auth_server_online = true;
                fillSession(session);
            }
        }
        break;
        case AccountTask::STATE_CREATED:
        case AccountTask::STATE_WORKING:
        case AccountTask::STATE_SUCCEEDED: {
            // Not reachable here, as they are not failures.
        }
    }
    m_currentTask.reset();
}

void MinecraftAccount::fillSession(AuthSessionPtr session)
{
    // the user name. you have to have an user name
    // FIXME: not with MSA
    session->username = data.userName();
    // volatile auth token
    session->access_token = data.accessToken();
    // the semi-permanent client token
    session->client_token = data.clientToken();
    // profile name
    session->player_name = data.profileName();
    // profile ID
    session->uuid = data.profileId();
    // 'legacy' or 'mojang', depending on account type
    session->user_type = typeString();
    if (!session->access_token.isEmpty())
    {
        session->session = "token:" + data.accessToken() + ":" + data.profileId();
    }
    else
    {
        session->session = "-";
    }
    session->m_accountPtr = shared_from_this();
}

void MinecraftAccount::decrementUses()
{
    Usable::decrementUses();
    if(!isInUse())
    {
        emit changed();
        // FIXME: we now need a better way to identify accounts...
        qWarning() << "Profile" << data.profileId() << "is no longer in use.";
    }
}

void MinecraftAccount::incrementUses()
{
    bool wasInUse = isInUse();
    Usable::incrementUses();
    if(!wasInUse)
    {
        emit changed();
        // FIXME: we now need a better way to identify accounts...
        qWarning() << "Profile" << data.profileId() << "is now in use.";
    }
}
