// SPDX-FileCopyrightText: Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later

#include "SingleImagePackModel.h"

#include <QFile>
#include <QFileInfo>
#include <QMimeDatabase>

#include <mtx/events/mscs/image_packs.hpp>
#include <nlohmann/json.hpp>

#include <unordered_set>

#include <mtx/responses/media.hpp>

#include "Cache_p.h"
#include "ChatPage.h"
#include "Logging.h"
#include "MatrixClient.h"
#include "Utils.h"
#include "timeline/Permissions.h"
#include "timeline/TimelineModel.h"

SingleImagePackModel::SingleImagePackModel(ImagePackInfo pack_, QObject *parent)
  : QAbstractListModel(parent)
  , roomid_(std::move(pack_.source_room))
  , statekey_(std::move(pack_.state_key))
  , old_statekey_(statekey_)
  , pack(std::move(pack_.pack))
  , fromSpace_(pack_.from_space)
{
    if (!pack.pack)
        pack.pack = mtx::events::msc2545::ImagePack::PackDescription{};

    shortcodes.reserve(pack.images.size());
    for (const auto &e : pack.images)
        shortcodes.push_back(e.first);

    connect(this, &SingleImagePackModel::addImage, this, &SingleImagePackModel::addImageCb);
    connect(this, &SingleImagePackModel::avatarUploaded, this, &SingleImagePackModel::setAvatarUrl);
}

int
SingleImagePackModel::rowCount(const QModelIndex &) const
{
    return (int)shortcodes.size();
}

QHash<int, QByteArray>
SingleImagePackModel::roleNames() const
{
    return {
      {Roles::Url, "url"},
      {Roles::ShortCode, "shortCode"},
      {Roles::Body, "body"},
      {Roles::IsEmote, "isEmote"},
      {Roles::IsSticker, "isSticker"},
    };
}

QVariant
SingleImagePackModel::data(const QModelIndex &index, int role) const
{
    if (hasIndex(index.row(), index.column(), index.parent())) {
        const auto &img = pack.images.at(shortcodes.at(index.row()));
        switch (role) {
        case Url:
            return QString::fromStdString(img.url);
        case ShortCode:
            return QString::fromStdString(shortcodes.at(index.row()));
        case Body:
            return QString::fromStdString(img.body);
        case IsEmote:
            return img.overrides_usage() ? img.is_emoji() : pack.pack->is_emoji();
        case IsSticker:
            return img.overrides_usage() ? img.is_sticker() : pack.pack->is_sticker();
        default:
            return {};
        }
    }
    return {};
}

bool
SingleImagePackModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    using mtx::events::msc2545::PackUsage;

    if (hasIndex(index.row(), index.column(), index.parent())) {
        auto &img = pack.images.at(shortcodes.at(index.row()));
        switch (role) {
        case ShortCode: {
            auto newCode = value.toString().toStdString();

            // otherwise we delete this by accident
            newCode = unconflictingShortcode(newCode);

            auto tmp     = img;
            auto oldCode = shortcodes.at(index.row());
            pack.images.erase(oldCode);
            shortcodes[index.row()] = newCode;
            pack.images.insert({newCode, tmp});

            emit dataChanged(
              this->index(index.row()), this->index(index.row()), {Roles::ShortCode});
            return true;
        }
        case Body:
            img.body = value.toString().toStdString();
            emit dataChanged(this->index(index.row()), this->index(index.row()), {Roles::Body});
            return true;
        case IsEmote: {
            bool isEmote   = value.toBool();
            bool isSticker = img.overrides_usage() ? img.is_sticker() : pack.pack->is_sticker();

            img.usage.set(PackUsage::Emoji, isEmote);
            img.usage.set(PackUsage::Sticker, isSticker);

            if (img.usage == pack.pack->usage)
                img.usage.reset();

            emit dataChanged(this->index(index.row()), this->index(index.row()), {Roles::IsEmote});

            return true;
        }
        case IsSticker: {
            bool isEmote   = img.overrides_usage() ? img.is_emoji() : pack.pack->is_emoji();
            bool isSticker = value.toBool();

            img.usage.set(PackUsage::Emoji, isEmote);
            img.usage.set(PackUsage::Sticker, isSticker);

            if (img.usage == pack.pack->usage)
                img.usage.reset();

            emit dataChanged(
              this->index(index.row()), this->index(index.row()), {Roles::IsSticker});

            return true;
        }
        }
    }
    return false;
}

bool
SingleImagePackModel::isGloballyEnabled() const
{
    if (auto roomPacks = cache::client()->getAccountData(mtx::events::EventType::ImagePackRooms)) {
        if (auto tmp =
              std::get_if<mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePackRooms>>(
                &*roomPacks)) {
            if (tmp->content.rooms.count(roomid_) &&
                tmp->content.rooms.at(roomid_).count(statekey_))
                return true;
        }
    }
    return false;
}
void
SingleImagePackModel::setGloballyEnabled(bool enabled)
{
    mtx::events::msc2545::ImagePackRooms content{};
    if (auto roomPacks = cache::client()->getAccountData(mtx::events::EventType::ImagePackRooms)) {
        if (auto tmp =
              std::get_if<mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePackRooms>>(
                &*roomPacks)) {
            content = tmp->content;
        }
    }

    if (enabled)
        content.rooms[roomid_][statekey_] = {};
    else
        content.rooms[roomid_].erase(statekey_);

    http::client()->put_account_data(content, [](mtx::http::RequestErr) {
        // emit this->globallyEnabledChanged();
    });
}

bool
SingleImagePackModel::canEdit() const
{
    if (roomid_.empty())
        return true;
    else
        return Permissions(QString::fromStdString(roomid_))
          .canChange(qml_mtx_events::ImagePackInRoom);
}

void
SingleImagePackModel::setPackname(QString val)
{
    auto val_ = val.toStdString();
    if (val_ != this->pack.pack->display_name) {
        this->pack.pack->display_name = val_;
        emit packnameChanged();
    }
}

void
SingleImagePackModel::setAttribution(QString val)
{
    auto val_ = val.toStdString();
    if (val_ != this->pack.pack->attribution) {
        this->pack.pack->attribution = val_;
        emit attributionChanged();
    }
}

void
SingleImagePackModel::setAvatarUrl(QString val)
{
    auto val_ = val.toStdString();
    if (val_ != this->pack.pack->avatar_url) {
        this->pack.pack->avatar_url = val_;
        emit avatarUrlChanged();
    }
}

QString
SingleImagePackModel::avatarUrl() const
{
    if (!pack.pack->avatar_url.empty())
        return QString::fromStdString(pack.pack->avatar_url);
    else if (!pack.images.empty())
        return QString::fromStdString(pack.images.begin()->second.url);
    else
        return QString();
}

void
SingleImagePackModel::setStatekey(QString val)
{
    auto val_ = val.toStdString();
    if (val_ != statekey_) {
        statekey_ = val_;

        // prevent deleting current pack
        if (!roomid_.empty() && statekey_ != old_statekey_) {
            statekey_ = unconflictingStatekey(roomid_, statekey_);
        }

        emit statekeyChanged();
    }
}

void
SingleImagePackModel::setIsStickerPack(bool val)
{
    using mtx::events::msc2545::PackUsage;
    if (val != pack.pack->is_sticker()) {
        pack.pack->usage.set(PackUsage::Sticker, val);
        if (!val)
            pack.pack->usage.set(PackUsage::Emoji, true);
        emit isEmotePackChanged();
        emit isStickerPackChanged();
    }
}

void
SingleImagePackModel::setIsEmotePack(bool val)
{
    using mtx::events::msc2545::PackUsage;
    if (val != pack.pack->is_emoji()) {
        pack.pack->usage.set(PackUsage::Emoji, val);
        if (!val)
            pack.pack->usage.set(PackUsage::Sticker, true);
        emit isEmotePackChanged();
        emit isStickerPackChanged();
    }
}

void
SingleImagePackModel::save()
{
    if (roomid_.empty()) {
        http::client()->put_account_data(pack, [](mtx::http::RequestErr e) {
            if (e)
                ChatPage::instance()->showNotification(
                  tr("Failed to update image pack: %1")
                    .arg(QString::fromStdString(e->matrix_error.error)));
        });
    } else {
        if (old_statekey_ != statekey_) {
            this->remove();
        }

        http::client()->send_state_event(
          roomid_,
          statekey_,
          pack,
          [this](const mtx::responses::EventId &, mtx::http::RequestErr e) {
              if (e)
                  ChatPage::instance()->showNotification(
                    tr("Failed to update image pack: %1")
                      .arg(QString::fromStdString(e->matrix_error.error)));

              nhlog::net()->info("Uploaded image pack: %1", statekey_);
          });
    }
}

void
SingleImagePackModel::remove()
{
    // handle account pack deletion.
    // Sadly we cannot actually delete the pack,
    // so we just send an empty pack to clear out its information.
    if (roomid_.empty()) {
        http::client()->put_account_data(
          mtx::events::msc2545::ImagePack(), [](mtx::http::RequestErr e) {
              if (e)
                  ChatPage::instance()->showNotification(
                    tr("Failed to update image pack: %1")
                      .arg(QString::fromStdString(e->matrix_error.error)));
          });
        return;
    }

    http::client()->send_state_event(
      roomid_,
      to_string(mtx::events::EventType::ImagePackInRoom),
      old_statekey_,
      nlohmann::json::object(),
      [](const mtx::responses::EventId &, mtx::http::RequestErr e) {
          if (e)
              ChatPage::instance()->showNotification(
                tr("Failed to delete old image pack: %1")
                  .arg(QString::fromStdString(e->matrix_error.error)));
      });
    old_statekey_ = statekey_;
}

void
SingleImagePackModel::addStickers(QList<QUrl> files)
{
    for (const auto &f : files) {
        auto file = QFile(f.toLocalFile());
        if (!file.open(QFile::ReadOnly)) {
            ChatPage::instance()->showNotification(
              tr("Failed to open image: %1").arg(f.toLocalFile()));
            return;
        }

        auto bytes = file.readAll();
        auto img   = utils::readImage(bytes);

        mtx::common::ImageInfo info{};

        auto sz = img.size() / 2;
        if (sz.width() > 512 || sz.height() > 512) {
            sz.scale(512, 512, Qt::AspectRatioMode::KeepAspectRatio);
        } else if (img.height() < 128 && img.width() < 128) {
            sz = img.size();
        }

        info.h        = sz.height();
        info.w        = sz.width();
        info.size     = bytes.size();
        info.mimetype = QMimeDatabase().mimeTypeForFile(f.toLocalFile()).name().toStdString();

        auto filename = f.fileName().toStdString();
        auto basename = QFileInfo(file).baseName().toStdString();
        http::client()->upload(
          bytes.toStdString(),
          QMimeDatabase().mimeTypeForFile(f.toLocalFile()).name().toStdString(),
          filename,
          [this, basename, info](const mtx::responses::ContentURI &uri, mtx::http::RequestErr e) {
              if (e) {
                  ChatPage::instance()->showNotification(
                    tr("Failed to upload image: %1")
                      .arg(QString::fromStdString(e->matrix_error.error)));
                  return;
              }

              emit addImage(uri.content_uri, basename, info);
          });
    }
}

void
SingleImagePackModel::setAvatar(QUrl f)
{
    auto file = QFile(f.toLocalFile());
    if (!file.open(QFile::ReadOnly)) {
        ChatPage::instance()->showNotification(tr("Failed to open image: %1").arg(f.toLocalFile()));
        return;
    }

    auto bytes = file.readAll();

    auto filename = f.fileName().toStdString();
    http::client()->upload(
      bytes.toStdString(),
      QMimeDatabase().mimeTypeForFile(f.toLocalFile()).name().toStdString(),
      filename,
      [this, filename](const mtx::responses::ContentURI &uri, mtx::http::RequestErr e) {
          if (e) {
              ChatPage::instance()->showNotification(
                tr("Failed to upload image: %1")
                  .arg(QString::fromStdString(e->matrix_error.error)));
              return;
          }

          emit avatarUploaded(QString::fromStdString(uri.content_uri));
      });
}

void
SingleImagePackModel::remove(int idx)
{
    if (idx < (int)shortcodes.size() && idx >= 0) {
        beginRemoveRows(QModelIndex(), idx, idx);
        auto s = shortcodes.at(idx);
        shortcodes.erase(shortcodes.begin() + idx);
        pack.images.erase(s);
        endRemoveRows();
    }
}

std::string
SingleImagePackModel::unconflictingShortcode(const std::string &shortcode)
{
    if (pack.images.count(shortcode)) {
        // more images won't fit in an event anyway
        for (int i = 0; i < 64'000; i++) {
            auto tempCode = shortcode + std::to_string(i);
            if (!pack.images.count(tempCode)) {
                return tempCode;
            }
        }
    }
    return shortcode;
}

std::string
SingleImagePackModel::unconflictingStatekey(const std::string &roomid, const std::string &key)
{
    if (roomid.empty())
        return key;

    std::unordered_set<std::string> statekeys;
    auto currentPacks =
      cache::client()->getStateEventsWithType<mtx::events::msc2545::ImagePack>(roomid);
    for (const auto &pack : currentPacks) {
        if (!pack.content.images.empty())
            statekeys.insert(pack.state_key);
    }
    auto defaultPack = cache::client()->getStateEvent<mtx::events::msc2545::ImagePack>(roomid);
    if (defaultPack && defaultPack->content.images.size()) {
        statekeys.insert(defaultPack->state_key);
    }

    if (statekeys.count(key)) {
        // arbitrary count. More than 64k image packs in a room are unlikely and if you have that,
        // you probably know what you are doing :)
        for (int i = 0; i < 64'000; i++) {
            auto tempCode = key + std::to_string(i);
            if (!statekeys.count(tempCode)) {
                return tempCode;
            }
        }
    }

    return key;
}

void
SingleImagePackModel::addImageCb(std::string uri, std::string filename, mtx::common::ImageInfo info)
{
    mtx::events::msc2545::PackImage img{};
    img.url  = uri;
    img.info = info;
    beginInsertRows(
      QModelIndex(), static_cast<int>(shortcodes.size()), static_cast<int>(shortcodes.size()));

    auto shortcode = unconflictingShortcode(filename);

    pack.images[shortcode] = img;
    shortcodes.push_back(shortcode);

    endInsertRows();

    if (this->pack.pack->avatar_url.empty())
        this->setAvatarUrl(QString::fromStdString(uri));
}

#include "moc_SingleImagePackModel.cpp"
