/****************************************************************************
 *   Copyright (C) 2018 Savoir-faire Linux                                  *
 *   Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com>       *
 *                                                                          *
 *   This library is free software; you can redistribute it and/or          *
 *   modify it under the terms of the GNU Lesser General Public             *
 *   License as published by the Free Software Foundation; either           *
 *   version 2.1 of the License, or (at your option) any later version.     *
 *                                                                          *
 *   This library 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      *
 *   Lesser 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/>.  *
 ***************************************************************************/

// LRC
#include "api/datatransfermodel.h"
#include "api/behaviorcontroller.h"
#include "callbackshandler.h"
#include "database.h"

// Dbus
#include "dbus/configurationmanager.h"

// DRing
#include <datatransfer_interface.h>

// Std
#include <map>
#include <stdexcept>

// Qt
#include <QUuid>

namespace lrc { namespace api {

/// DRING to LRC event code conversion
static inline
datatransfer::Status
convertDataTransferEvent(DRing::DataTransferEventCode event)
{
    switch (event) {
        case DRing::DataTransferEventCode::created: return datatransfer::Status::on_connection;
        case DRing::DataTransferEventCode::unsupported: return datatransfer::Status::unsupported;
        case DRing::DataTransferEventCode::wait_peer_acceptance: return datatransfer::Status::on_connection;
        case DRing::DataTransferEventCode::wait_host_acceptance: return datatransfer::Status::on_connection;
        case DRing::DataTransferEventCode::ongoing: return datatransfer::Status::on_progress;
        case DRing::DataTransferEventCode::finished: return datatransfer::Status::success;
        case DRing::DataTransferEventCode::closed_by_host: return datatransfer::Status::stop_by_host;
        case DRing::DataTransferEventCode::closed_by_peer: return datatransfer::Status::stop_by_peer;
        case DRing::DataTransferEventCode::invalid_pathname: return datatransfer::Status::invalid_pathname;
        case DRing::DataTransferEventCode::unjoinable_peer: return datatransfer::Status::unjoinable_peer;
    }
    throw std::runtime_error("BUG: broken convertDataTransferEvent() switch");
}

class DataTransferModel::Impl : public QObject
{
    Q_OBJECT

public:
    Impl(DataTransferModel& up_link,
         Database& database,
         const CallbacksHandler& callbacksHandler,
         const api::BehaviorController& behaviorController);

    DataTransferModel& upLink;
    std::map<DRing::DataTransferId, std::string> dring2lrcIdMap;
    std::map<std::string, DRing::DataTransferId> lrc2dringIdMap; // stricly the reverse map of dring2lrcIdMap
    Database& database;
    const CallbacksHandler& callbacksHandler;
    const BehaviorController& behaviorController;

    std::string registerTransferId(DRing::DataTransferId id);

public Q_SLOTS:
    void slotDataTransferEvent(qulonglong id, uint code);
};

DataTransferModel::Impl::Impl(DataTransferModel& up_link,
                              Database& database,
                              const CallbacksHandler& callbacksHandler,
                              const api::BehaviorController& behaviorController)
    : QObject {}
    , behaviorController {behaviorController}
    , callbacksHandler {callbacksHandler}
    , database {database}
    , upLink {up_link}
{
    connect(&ConfigurationManager::instance(), &ConfigurationManagerInterface::dataTransferEvent,
            this, &DataTransferModel::Impl::slotDataTransferEvent);
}

std::string
DataTransferModel::Impl::registerTransferId(DRing::DataTransferId dring_id)
{
    const auto& iter = dring2lrcIdMap.find(dring_id);
    if (iter != std::cend(dring2lrcIdMap))
        return iter->second;
    while (true) {
        auto res = dring2lrcIdMap.emplace(dring_id, QUuid::createUuid().toString().toStdString());
        if (res.second) {
            lrc2dringIdMap.emplace(res.first->second, dring_id);
            return res.first->second;
        }
    }
}

void
DataTransferModel::Impl::slotDataTransferEvent(qulonglong dring_id, uint code)
{
    auto lrc_id = registerTransferId(dring_id);
    auto event = DRing::DataTransferEventCode(code);
    if (event == DRing::DataTransferEventCode::created) {
        auto info = static_cast<DataTransferInfo>(ConfigurationManager::instance().dataTransferInfo(dring_id));
        if (!info.isOutgoing) {
            emit upLink.incomingTransfer(lrc_id, info.displayName.toStdString(), info.totalSize, info.bytesProgress);
            return;
        }
    }

    emit upLink.transferStatusChanged(lrc_id, convertDataTransferEvent(event));
}

DataTransferModel::DataTransferModel(Database& database,
                                     const CallbacksHandler& callbacksHandler,
                                     const api::BehaviorController& behaviorController)
    : QObject()
    , pimpl_ { std::make_unique<Impl>(*this, database, callbacksHandler, behaviorController) }
{}

DataTransferModel::~DataTransferModel() = default;

std::vector<std::string>
DataTransferModel::transferIdList() const
{
    VectorULongLong dring_list = ConfigurationManager::instance().dataTransferList();
    for (auto dring_id : dring_list) {
         pimpl_->registerTransferId(dring_id);
    }
    std::vector<std::string> result;
    result.reserve(dring_list.size());
    for (auto& item : pimpl_->lrc2dringIdMap) {
        result.push_back(item.first);
    }
    return result;
}

std::string
DataTransferModel::sendFile(const std::string& account_id, const std::string& peer_uri,
                            const std::string& file_path, const std::string& display_name)
{
    auto dring_id = static_cast<DRing::DataTransferId>(ConfigurationManager::instance().sendFile(
                                                           QString::fromStdString(account_id),
                                                           QString::fromStdString(peer_uri),
                                                           QString::fromStdString(file_path),
                                                           QString::fromStdString(display_name)));
    return pimpl_->registerTransferId(dring_id);
}

datatransfer::Info
DataTransferModel::transferInfo(const std::string& lrc_id)
{
    auto dring_id = pimpl_->lrc2dringIdMap.at(lrc_id);
    auto dring_info = static_cast<DataTransferInfo>(ConfigurationManager::instance().dataTransferInfo(dring_id));
    datatransfer::Info lrc_info;
    lrc_info.uid = lrc_id;
    lrc_info.isOutgoing = dring_info.isOutgoing;
    lrc_info.totalSize = dring_info.totalSize;
    lrc_info.progress = dring_info.lastEvent;
    lrc_info.path = dring_info.displayName.toStdString();
    lrc_info.displayName = dring_info.displayName.toStdString();
    lrc_info.status = convertDataTransferEvent(DRing::DataTransferEventCode(dring_info.lastEvent));
    return lrc_info;
}

std::streamsize
DataTransferModel::bytesProgress(const std::string& lrc_id)
{
    return ConfigurationManager::instance().dataTransferBytesProgress(pimpl_->lrc2dringIdMap.at(lrc_id));
}

void
DataTransferModel::acceptFile(const std::string& lrc_id,
                                      const std::string& file_path,
                                      std::size_t offset)
{
    auto dring_id = pimpl_->lrc2dringIdMap.at(lrc_id);
    ConfigurationManager::instance().acceptFileTransfer(dring_id, QString::fromStdString(file_path), offset);
}

void
DataTransferModel::cancel(const std::string& lrc_id)
{
    auto dring_id = pimpl_->lrc2dringIdMap.at(lrc_id);
    ConfigurationManager::instance().cancelDataTransfer(dring_id);
}

}} // namespace lrc::api

#include "api/moc_datatransfermodel.cpp"
#include "datatransfermodel.moc"