/*
 * Copyright (C) 2019-2020 by Savoir-faire Linux
 * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
 * Author: Isa Nanic <isa.nanic@savoirfairelinux.com>
 * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.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 3 of the License, or
 * (at your option) any later version.
 *
 * 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/>.
 */

#pragma once

#ifdef _MSC_VER
#undef ERROR
#endif

#include "accountlistmodel.h"
#include "rendermanager.h"
#include "settingskey.h"
#include "utils.h"

#include "api/account.h"
#include "api/avmodel.h"
#include "api/pluginmodel.h"
#include "api/behaviorcontroller.h"
#include "api/contact.h"
#include "api/contactmodel.h"
#include "api/conversation.h"
#include "api/conversationmodel.h"
#include "api/datatransfermodel.h"
#include "api/lrc.h"
#include "api/newaccountmodel.h"
#include "api/newcallmodel.h"
#include "api/newcodecmodel.h"
#include "api/newdevicemodel.h"
#include "api/peerdiscoverymodel.h"

#include <QBuffer>
#include <QMutex>
#include <QObject>
#include <QPixmap>
#include <QRegularExpression>
#include <QSettings>
#include <QtConcurrent/QtConcurrent>

#include <memory>

using namespace lrc::api;

using migrateCallback = std::function<void()>;
using getConvPredicate = std::function<bool(const conversation::Info &conv)>;

class LRCInstance : public QObject
{
    Q_OBJECT

public:
    static LRCInstance &
    instance(migrateCallback willMigrate = {}, migrateCallback didMigrate = {})
    {
        static LRCInstance instance_(willMigrate, didMigrate);
        return instance_;
    };
    static void
    init(migrateCallback willMigrate = {}, migrateCallback didMigrate = {})
    {
        instance(willMigrate, didMigrate);
    };
    static Lrc &
    getAPI()
    {
        return *(instance().lrc_);
    };
    static RenderManager *
    renderer()
    {
        return instance().renderer_.get();
    }
    static void
    connectivityChanged()
    {
        instance().lrc_->connectivityChanged();
    };
    static NewAccountModel &
    accountModel()
    {
        return instance().lrc_->getAccountModel();
    };
    static BehaviorController &
    behaviorController()
    {
        return instance().lrc_->getBehaviorController();
    };
    static DataTransferModel &
    dataTransferModel()
    {
        return instance().lrc_->getDataTransferModel();
    };
    static AVModel &
    avModel()
    {
        return instance().lrc_->getAVModel();
    };
    static PluginModel &
    pluginModel()
    {
        return instance().lrc_->getPluginModel();
    };
    static bool
    isConnected()
    {
        return instance().lrc_->isConnected();
    };
    static VectorString
    getActiveCalls()
    {
        return instance().lrc_->activeCalls();
    };
    static const account::Info &
    getAccountInfo(const QString &accountId)
    {
        return accountModel().getAccountInfo(accountId);
    };
    static const account::Info &
    getCurrentAccountInfo()
    {
        return getAccountInfo(getCurrAccId());
    };
    static bool
    hasVideoCall()
    {
        auto activeCalls = instance().lrc_->activeCalls();
        auto accountList = accountModel().getAccountList();
        bool result = false;
        for (const auto &callId : activeCalls) {
            for (const auto &accountId : accountList) {
                auto &accountInfo = accountModel().getAccountInfo(accountId);
                if (accountInfo.callModel->hasCall(callId)) {
                    auto call = accountInfo.callModel->getCall(callId);
                    result |= !(call.isAudioOnly || call.videoMuted);
                }
            }
        }
        return result;
    };
    static QString
    getCallIdForConversationUid(const QString &convUid, const QString &accountId)
    {
        auto convInfo = LRCInstance::getConversationFromConvUid(convUid, accountId);
        if (convInfo.uid.isEmpty()) {
            return {};
        }
        return convInfo.confId.isEmpty() ? convInfo.callId : convInfo.confId;
    }
    static const call::Info *
    getCallInfo(const QString &callId, const QString &accountId)
    {
        try {
            auto &accInfo = LRCInstance::accountModel().getAccountInfo(accountId);
            if (!accInfo.callModel->hasCall(callId)) {
                return nullptr;
            }
            return &accInfo.callModel->getCall(callId);
        } catch (...) {
            return nullptr;
        }
    }
    static const call::Info *
    getCallInfoForConversation(const conversation::Info &convInfo, bool forceCallOnly = {})
    {
        try {
            auto accountId = convInfo.accountId;
            auto &accInfo = LRCInstance::accountModel().getAccountInfo(accountId);
            auto callId = forceCallOnly
                              ? convInfo.callId
                              : (convInfo.confId.isEmpty() ? convInfo.callId : convInfo.confId);
            if (!accInfo.callModel->hasCall(callId)) {
                return nullptr;
            }
            return &accInfo.callModel->getCall(callId);
        } catch (...) {
            return nullptr;
        }
    }
    static const conversation::Info &
    getConversation(const QString &accountId, getConvPredicate pred = {}, bool filtered = false)
    {
        using namespace lrc::api;
        static conversation::Info invalid = {};
        try {
            auto &accInfo = LRCInstance::getAccountInfo(accountId);
            auto &convModel = accInfo.conversationModel;
            if (filtered) {
                auto &convs = convModel->allFilteredConversations();
                auto conv = std::find_if(convs.begin(), convs.end(), pred);
                if (conv != convs.end()) {
                    return *conv;
                }
            } else {
                for (int i = Utils::toUnderlyingValue(profile::Type::RING);
                     i <= Utils::toUnderlyingValue(profile::Type::TEMPORARY);
                     ++i) {
                    auto filter = Utils::toEnum<profile::Type>(i);
                    auto &convs = convModel->getFilteredConversations(filter);
                    auto conv = std::find_if(convs.begin(), convs.end(), pred);
                    if (conv != convs.end()) {
                        return *conv;
                    }
                }
            }
        } catch (...) {
        }
        return invalid;
    }
    static const conversation::Info &
    getConversationFromCallId(const QString &callId,
                              const QString &accountId = {},
                              bool filtered = false)
    {
        return getConversation(
            !accountId.isEmpty() ? accountId : getCurrAccId(),
            [&](const conversation::Info &conv) -> bool { return callId == conv.callId; },
            filtered);
    }
    static const conversation::Info &
    getConversationFromConvUid(const QString &convUid,
                               const QString &accountId = {},
                               bool filtered = false)
    {
        return getConversation(
            !accountId.isEmpty() ? accountId : getCurrAccId(),
            [&](const conversation::Info &conv) -> bool { return convUid == conv.uid; },
            filtered);
    }
    static const conversation::Info &
    getConversationFromPeerUri(const QString &peerUri,
                               const QString &accountId = {},
                               bool filtered = false)
    {
        return getConversation(
            !accountId.isEmpty() ? accountId : getCurrAccId(),
            [&](const conversation::Info &conv) -> bool { return peerUri == conv.participants[0]; },
            filtered);
    }
    static const conversation::Info &
    getCurrentConversation()
    {
        return getConversationFromConvUid(getCurrentConvUid());
    }

    static ConversationModel *
    getCurrentConversationModel()
    {
        return getCurrentAccountInfo().conversationModel.get();
    };

    static NewCallModel *
    getCurrentCallModel()
    {
        return getCurrentAccountInfo().callModel.get();
    };

    static const QString &
    getCurrAccId()
    {
        auto accountList = accountModel().getAccountList();
        if (instance().selectedAccountId_.isEmpty() && accountList.size()) {
            instance().selectedAccountId_ = accountList.at(0);
        }
        return instance().selectedAccountId_;
    };

    static void
    setSelectedAccountId(const QString &accountId = {})
    {
        instance().selectedAccountId_ = accountId;
        QSettings settings("jami.net", "Jami");
        settings.setValue(SettingsKey::selectedAccount, accountId);

        // Last selected account should be set as preferred.
        accountModel().setTopAccount(accountId);
    };

    static const QString &
    getCurrentConvUid()
    {
        return instance().selectedConvUid_;
    };

    static void
    setSelectedConvId(const QString &convUid = {})
    {
        instance().selectedConvUid_ = convUid;
    };

    static void
    reset(bool newInstance = false)
    {
        if (newInstance) {
            instance().renderer_.reset(new RenderManager(avModel()));
            instance().lrc_.reset(new Lrc());
        } else {
            instance().renderer_.reset();
            instance().lrc_.reset();
        }
    };

    static const int
    getCurrentAccountIndex()
    {
        for (int i = 0; i < accountModel().getAccountList().size(); i++) {
            if (accountModel().getAccountList()[i] == getCurrAccId()) {
                return i;
            }
        }
        return -1;
    };

    static const QPixmap
    getCurrAccPixmap()
    {
        return instance()
            .accountListModel_
            .data(instance().accountListModel_.index(getCurrentAccountIndex()),
                  AccountListModel::Role::Picture)
            .value<QPixmap>();
    };

    static void
    setAvatarForAccount(const QPixmap &avatarPixmap, const QString &accountID)
    {
        QByteArray ba;
        QBuffer bu(&ba);
        bu.open(QIODevice::WriteOnly);
        avatarPixmap.save(&bu, "PNG");
        auto str = QString::fromLocal8Bit(ba.toBase64());
        accountModel().setAvatar(accountID, str);
    };

    static void
    setCurrAccAvatar(const QPixmap &avatarPixmap)
    {
        QByteArray ba;
        QBuffer bu(&ba);
        bu.open(QIODevice::WriteOnly);
        avatarPixmap.save(&bu, "PNG");
        auto str = QString::fromLocal8Bit(ba.toBase64());
        accountModel().setAvatar(getCurrAccId(), str);
    };

    static void
    setCurrAccAvatar(const QString &avatar)
    {
        accountModel().setAvatar(getCurrAccId(), avatar);
    };

    static void
    setCurrAccDisplayName(const QString &displayName)
    {
        auto accountId = LRCInstance::getCurrAccId();
        accountModel().setAlias(accountId, displayName);
        /*
         * Force save to .yml.
         */
        auto confProps = LRCInstance::accountModel().getAccountConfig(accountId);
        LRCInstance::accountModel().setAccountConfig(accountId, confProps);
    };

    static const account::ConfProperties_t &
    getCurrAccConfig()
    {
        return instance().getCurrentAccountInfo().confProperties;
    }

    static void
    subscribeToDebugReceived()
    {
        instance().lrc_->subscribeToDebugReceived();
    }

    static void
    startAudioMeter(bool async)
    {
        auto f = [] {
            if (!LRCInstance::getActiveCalls().size()) {
                LRCInstance::avModel().startAudioDevice();
            }
            LRCInstance::avModel().setAudioMeterState(true);
        };
        if (async) {
            QtConcurrent::run(f);
        } else {
            f();
        }
    }

    static void
    stopAudioMeter(bool async)
    {
        auto f = [] {
            if (!LRCInstance::getActiveCalls().size()) {
                LRCInstance::avModel().stopAudioDevice();
            }
            LRCInstance::avModel().setAudioMeterState(false);
        };
        if (async) {
            QtConcurrent::run(f);
        } else {
            f();
        }
    }

    static QString
    getContentDraft(const QString &convUid, const QString &accountId)
    {
        auto draftKey = accountId + "_" + convUid;
        return instance().contentDrafts_[draftKey];
    }

    static void
    setContentDraft(const QString &convUid, const QString &accountId, const QString &content)
    {
        auto draftKey = accountId + "_" + convUid;
        instance().contentDrafts_[draftKey] = content;
    }

    static void
    pushLastConferencee(const QString &confId, const QString &callId)
    {
        instance().lastConferencees_[confId] = callId;
    }

    static QString
    popLastConferencee(const QString &confId)
    {
        QString callId = {};
        auto iter = instance().lastConferencees_.find(confId);
        if (iter != instance().lastConferencees_.end()) {
            callId = iter.value();
            instance().lastConferencees_.erase(iter);
        }
        return callId;
    }

signals:
    void accountListChanged();

private:
    LRCInstance(migrateCallback willMigrateCb = {}, migrateCallback didMigrateCb = {})
    {
        lrc_ = std::make_unique<Lrc>(willMigrateCb, didMigrateCb);
        renderer_ = std::make_unique<RenderManager>(lrc_->getAVModel());
    };

    std::unique_ptr<Lrc> lrc_;
    std::unique_ptr<RenderManager> renderer_;
    AccountListModel accountListModel_;
    QString selectedAccountId_;
    QString selectedConvUid_;
    MapStringString contentDrafts_;
    MapStringString lastConferencees_;
};
#if QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)
Q_DECLARE_METATYPE(LRCInstance *)
#endif
