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

#pragma once

#include <QIODevice>
#include <QImage>
#include <QObject>
#include <QQmlEngine>
#include <QSize>
#include <QStringList>
#include <QTimer>
#include <QUrl>
#include <QVariantList>

#include <deque>
#include <memory>

#include <mtx/common.hpp>
#include <mtx/events/common.hpp>

class TimelineModel;
class CombinedImagePackModel;
class QMimeData;
class QDropEvent;

struct DeleteLaterDeleter
{
    void operator()(QObject *p)
    {
        if (p)
            p->deleteLater();
    }
};

enum class MarkdownOverride
{
    NOT_SPECIFIED, // no override set
    ON,
    OFF,
    CMARK,
};

class MediaUpload final : public QObject
{
    Q_OBJECT

    QML_ELEMENT
    QML_UNCREATABLE("")

    Q_PROPERTY(int mediaType READ type NOTIFY mediaTypeChanged)
    // https://stackoverflow.com/questions/33422265/pass-qimage-to-qml/68554646#68554646
    Q_PROPERTY(QUrl thumbnail READ thumbnailDataUrl NOTIFY thumbnailChanged)
    //    Q_PROPERTY(QString humanSize READ humanSize NOTIFY huSizeChanged)
    Q_PROPERTY(QString filename READ filename WRITE setFilename NOTIFY filenameChanged)

    // thumbnail video
    // https://stackoverflow.com/questions/26229633/display-on-screen-using-qabstractvideosurface

public:
    enum MediaType
    {
        File,
        Image,
        Video,
        Audio,
    };
    Q_ENUM(MediaType)

    explicit MediaUpload(std::unique_ptr<QIODevice> data,
                         const QString &mimetype,
                         const QString &originalFilename,
                         bool encrypt,
                         QObject *parent = nullptr);

    [[nodiscard]] int type() const
    {
        if (mimeClass_ == u"video")
            return MediaType::Video;
        else if (mimeClass_ == u"audio")
            return MediaType::Audio;
        else if (mimeClass_ == u"image")
            return MediaType::Image;
        else
            return MediaType::File;
    }
    [[nodiscard]] QString url() const { return url_; }
    [[nodiscard]] QString mimetype() const { return mimetype_; }
    [[nodiscard]] QString mimeClass() const { return mimeClass_; }
    [[nodiscard]] QString filename() const { return originalFilename_; }
    [[nodiscard]] QString blurhash() const { return blurhash_; }
    [[nodiscard]] uint64_t size() const { return size_; }
    [[nodiscard]] uint64_t duration() const { return duration_; }
    [[nodiscard]] std::optional<mtx::crypto::EncryptedFile> encryptedFile_()
    {
        return encryptedFile;
    }
    [[nodiscard]] std::optional<mtx::crypto::EncryptedFile> thumbnailEncryptedFile_()
    {
        return thumbnailEncryptedFile;
    }
    [[nodiscard]] QSize dimensions() const { return dimensions_; }

    QImage thumbnailImg() const { return thumbnail_; }
    QString thumbnailUrl() const { return thumbnailUrl_; }
    QUrl thumbnailDataUrl() const;
    [[nodiscard]] uint64_t thumbnailSize() const { return thumbnailSize_; }

    void setFilename(QString fn)
    {
        if (fn != originalFilename_) {
            originalFilename_ = std::move(fn);
            emit filenameChanged();
        }
    }

signals:
    void uploadComplete(MediaUpload *self, QString url);
    void uploadFailed(MediaUpload *self);
    void filenameChanged();
    void thumbnailChanged();
    void mediaTypeChanged();

public slots:
    void startUpload();

private slots:
    void setThumbnail(QImage img)
    {
        this->thumbnail_ = std::move(img);
        emit thumbnailChanged();
    }

public:
    // void uploadThumbnail(QImage img);

    std::unique_ptr<QIODevice> source;
    QByteArray data;
    QString mimetype_;
    QString mimeClass_;
    QString originalFilename_;
    QString blurhash_;
    QString thumbnailUrl_;
    QString url_;
    std::optional<mtx::crypto::EncryptedFile> encryptedFile, thumbnailEncryptedFile;

    QImage thumbnail_;

    QSize dimensions_;
    uint64_t size_          = 0;
    uint64_t thumbnailSize_ = 0;
    uint64_t duration_      = 0;
    bool encrypt_;
};

class InputBar final : public QObject
{
    Q_OBJECT
    Q_PROPERTY(bool uploading READ uploading NOTIFY uploadingChanged)
    Q_PROPERTY(
      bool containsInvalidCommand READ containsInvalidCommand NOTIFY containsInvalidCommandChanged)
    Q_PROPERTY(bool containsIncompleteCommand READ containsIncompleteCommand NOTIFY
                 containsIncompleteCommandChanged)
    Q_PROPERTY(QString currentCommand READ currentCommand NOTIFY currentCommandChanged)
    Q_PROPERTY(QStringList mentions READ mentions NOTIFY mentionsChanged)
    Q_PROPERTY(QString text READ text NOTIFY textChanged)
    Q_PROPERTY(QVariantList uploads READ uploads NOTIFY uploadsChanged)

public:
    explicit InputBar(TimelineModel *parent)
      : QObject()
      , room(parent)
    {
        typingRefresh_.setInterval(10'000);
        typingRefresh_.setSingleShot(true);
        typingTimeout_.setInterval(5'000);
        typingTimeout_.setSingleShot(true);
        connect(&typingRefresh_, &QTimer::timeout, this, &InputBar::startTyping);
        connect(&typingTimeout_, &QTimer::timeout, this, &InputBar::stopTyping);
    }

    QVariantList uploads() const;

public slots:
    [[nodiscard]] QString text() const;
    QString previousText();
    QString nextText();
    void setText(const QString &newText);

    [[nodiscard]] QStringList mentions() const { return mentions_; }
    void addMention(QString m, QString text);
    void removeMention(QString m);

    void storeForEdit()
    {
        textBeforeEdit       = text();
        mentionsBefore       = mentions_;
        mentionTextsBefore   = mentionTexts_;
        containsAtRoomBefore = containsAtRoom_;
        emit mentionsChanged();
    }
    void restoreAfterEdit()
    {
        mentions_       = mentionsBefore;
        mentionTexts_   = mentionTextsBefore;
        containsAtRoom_ = containsAtRoomBefore;
        mentionsBefore.clear();
        mentionTextsBefore.clear();
        setText(textBeforeEdit);
        textBeforeEdit.clear();
        emit mentionsChanged();
    }
    void replaceMentions(QStringList newMentions, QStringList newMentionTexts)
    {
        if (newMentions.size() != newMentionTexts.size())
            return;

        mentions_     = newMentions;
        mentionTexts_ = newMentionTexts;

        this->containsAtRoom_ = newMentions.contains(u"@room");

        emit mentionsChanged();
    }

    bool containsInvalidCommand() const { return containsInvalidCommand_; }
    bool containsIncompleteCommand() const { return containsIncompleteCommand_; }
    QString currentCommand() const { return currentCommand_; }

    void send();
    bool tryPasteAttachment(bool fromMouse);
    bool insertMimeData(const QMimeData *data);
    void updateState(int selectionStart, int selectionEnd, int cursorPosition, const QString &text);
    void openFileSelection();
    [[nodiscard]] bool uploading() const { return uploading_; }
    void message(const QString &body,
                 MarkdownOverride useMarkdown = MarkdownOverride::NOT_SPECIFIED,
                 bool rainbowify              = false);
    void reaction(const QString &reactedEvent, const QString &reactionKey);
    void sticker(QStringList descriptor);

    void acceptUploads();
    void declineUploads();

private slots:
    void startTyping();
    void stopTyping();

    void finalizeUpload(MediaUpload *upload, const QString &url);
    void removeRunUpload(MediaUpload *upload);

signals:
    void textChanged(QString newText);
    void uploadingChanged(bool value);
    void containsInvalidCommandChanged();
    void mentionsChanged();
    void containsIncompleteCommandChanged();
    void currentCommandChanged();
    void uploadsChanged();

private:
    void emote(const QString &body, bool rainbowify);
    void notice(const QString &body, bool rainbowify);
    void confetti(const QString &body, bool rainbowify);
    void rainfall(const QString &body);
    void customMsgtype(const QString &msgtype, const QString &body);
    bool command(const QString &name, QString args);
    void image(const QString &filename,
               const std::optional<mtx::crypto::EncryptedFile> &file,
               const QString &url,
               const QString &mime,
               uint64_t dsize,
               const QSize &dimensions,
               const std::optional<mtx::crypto::EncryptedFile> &thumbnailEncryptedFile,
               const QString &thumbnailUrl,
               uint64_t thumbnailSize,
               const QSize &thumbnailDimensions,
               const QString &blurhash);
    void file(const QString &filename,
              const std::optional<mtx::crypto::EncryptedFile> &encryptedFile,
              const QString &url,
              const QString &mime,
              uint64_t dsize);
    void audio(const QString &filename,
               const std::optional<mtx::crypto::EncryptedFile> &file,
               const QString &url,
               const QString &mime,
               uint64_t dsize,
               uint64_t duration);
    void video(const QString &filename,
               const std::optional<mtx::crypto::EncryptedFile> &file,
               const QString &url,
               const QString &mime,
               uint64_t dsize,
               uint64_t duration,
               const QSize &dimensions,
               const std::optional<mtx::crypto::EncryptedFile> &thumbnailEncryptedFile,
               const QString &thumbnailUrl,
               uint64_t thumbnailSize,
               const QSize &thumbnailDimensions,
               const QString &blurhash);

    QPair<QString, QString> getCommandAndArgs() const { return getCommandAndArgs(text()); }
    QPair<QString, QString> getCommandAndArgs(const QString &currentText) const;
    mtx::common::Relations generateRelations() const;
    mtx::common::Mentions generateMentions() const;

    void startUploadFromPath(const QString &path);
    void startUploadFromMimeData(const QMimeData &source, const QString &format);
    void startUpload(std::unique_ptr<QIODevice> dev, const QString &orgPath, const QString &format);
    void setUploading(bool value)
    {
        if (value != uploading_) {
            uploading_ = value;
            emit uploadingChanged(value);
        }
    }

    void updateTextContentProperties(const QString &t, bool textDeleted = false);

    void toggleIgnore(const QString &user, const bool ignored);
    void toggleInvitePermission(const QString &id, bool block);

    QTimer typingRefresh_;
    QTimer typingTimeout_;
    TimelineModel *room;
    std::deque<QString> history_;
    std::size_t history_index_ = 0;
    int selectionStart = 0, selectionEnd = 0, cursorPosition = 0;
    bool uploading_                 = false;
    bool containsAtRoom_            = false;
    bool containsInvalidCommand_    = false;
    bool containsIncompleteCommand_ = false;
    QString currentCommand_;
    QStringList mentions_, mentionTexts_;
    // store stuff during edits
    QStringList mentionsBefore, mentionTextsBefore;
    QString textBeforeEdit;
    bool containsAtRoomBefore = false;

    using UploadHandle = std::unique_ptr<MediaUpload, DeleteLaterDeleter>;
    std::vector<UploadHandle> unconfirmedUploads;
    std::vector<UploadHandle> runningUploads;
};
