/*
 * Copyright (C) 2015-2020 by Savoir-faire Linux
 * Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>
 * 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

#include "version.h"

#include <string>

#include <QClipboard>
#include <QCryptographicHash>
#include <QDir>
#include <QApplication>
#include <QImage>
#include <QItemDelegate>
#include <QLabel>
#include <QListWidget>
#include <QMessageBox>
#include <QQmlEngine>
#include <QSettings>
#include <QStackedWidget>
#include <QStandardPaths>
#include <QString>
#include <QTextDocument>
#include <QtGlobal>
#include <QPainterPath>

#ifdef Q_OS_WIN
#include <ciso646>
#include <windows.h>
#undef ERROR
#else
#define LPCWSTR char *
#endif

#include "api/account.h"
#include "api/contact.h"
#include "api/contactmodel.h"
#include "api/conversationmodel.h"

static const QSize IMAGE_SIZE{128, 128};
static float CURRENT_SCALING_RATIO{1.0};

#ifdef BETA
static constexpr bool isBeta = true;
#else
static constexpr bool isBeta = false;
#endif

namespace Utils {

/*
 * Qml type register.
 */
#define QML_REGISTERSINGLETONTYPE(T, MAJ, MIN) \
    qmlRegisterSingletonType<T>("net.jami.Models", \
                                MAJ, \
                                MIN, \
                                #T, \
                                [](QQmlEngine *e, QJSEngine *se) -> QObject * { \
                                    Q_UNUSED(e); \
                                    Q_UNUSED(se); \
                                    T *obj = new T(); \
                                    return obj; \
                                });
#define QML_REGISTERSINGLETONTYPE_WITH_INSTANCE(T, MAJ, MIN) \
    qmlRegisterSingletonType<T>("net.jami.Models", \
                                MAJ, \
                                MIN, \
                                #T, \
                                [](QQmlEngine *e, QJSEngine *se) -> QObject * { \
                                    Q_UNUSED(e); \
                                    Q_UNUSED(se); \
                                    return &(T::instance()); \
                                });

#define QML_REGISTERSINGLETONTYPE_URL(URL, T, MAJ, MIN) \
    qmlRegisterSingletonType(QUrl(URL), "net.jami.Models", MAJ, MIN, #T);

#define QML_REGISTERTYPE(T, MAJ, MIN) qmlRegisterType<T>("net.jami.Models", MAJ, MIN, #T);

#define QML_REGISTERNAMESPACE(T, NAME, MAJ, MIN) \
    qmlRegisterUncreatableMetaObject(T, "net.jami.Models", MAJ, MIN, NAME, "")

#define QML_REGISTERUNCREATABLE(T, MAJ, MIN) \
    qmlRegisterUncreatableType<T>("net.jami.Models", \
                                  MAJ, \
                                  MIN, \
                                  #T, \
                                  "Don't try to add to a qml definition of " #T);

#define QML_REGISTERUNCREATABLE_IN_NAMESPACE(T, NAMESPACE, MAJ, MIN) \
    qmlRegisterUncreatableType<NAMESPACE::T>("net.jami.Models", \
                                             MAJ, \
                                             MIN, \
                                             #T, \
                                             "Don't try to add to a qml definition of " #T);

/*
 * System.
 */
bool CreateStartupLink(const std::wstring &wstrAppName);
void DeleteStartupLink(const std::wstring &wstrAppName);
bool CreateLink(LPCWSTR lpszPathObj, LPCWSTR lpszPathLink);
bool CheckStartupLink(const std::wstring &wstrAppName);
void removeOldVersions();
const char *WinGetEnv(const char *name);
QString GetRingtonePath();
QString GenGUID();
QString GetISODate();
void InvokeMailto(const QString &subject,
                  const QString &body,
                  const QString &attachement = QString());
void setStackWidget(QStackedWidget *stack, QWidget *widget);
void showSystemNotification(QWidget *widget,
                            const QString &message,
                            long delay = 5000,
                            const QString &triggeredAccountId = "");
void showSystemNotification(QWidget *widget,
                            const QString &sender,
                            const QString &message,
                            long delay = 5000,
                            const QString &triggeredAccountId = "");
QSize getRealSize(QScreen *screen);
void forceDeleteAsync(const QString &path);
QString getChangeLog();
QString getProjectCredits();
float getCurrentScalingRatio();
void setCurrentScalingRatio(float ratio);

/*
 * Updates.
 */
void cleanUpdateFiles();
void checkForUpdates(bool withUI, QWidget *parent = nullptr);
void applyUpdates(bool updateToBeta, QWidget *parent = nullptr);

/*
 * Names.
 */
QString bestIdForConversation(const lrc::api::conversation::Info &conv,
                              const lrc::api::ConversationModel &model);
QString bestIdForAccount(const lrc::api::account::Info &account);
QString bestNameForAccount(const lrc::api::account::Info &account);
QString bestIdForContact(const lrc::api::contact::Info &contact);
QString bestNameForContact(const lrc::api::contact::Info &contact);
QString bestNameForConversation(const lrc::api::conversation::Info &conv,
                                const lrc::api::ConversationModel &model);
/*
 * Returns empty string if only infoHash is available.
 */
QString secondBestNameForAccount(const lrc::api::account::Info &account);
lrc::api::profile::Type profileType(const lrc::api::conversation::Info &conv,
                                    const lrc::api::ConversationModel &model);

/*
 * Interactions.
 */
std::string formatTimeString(const std::time_t &timestamp);
bool isInteractionGenerated(const lrc::api::interaction::Type &interaction);
bool isContactValid(const QString &contactUid, const lrc::api::ConversationModel &model);
bool getReplyMessageBox(QWidget *widget, const QString &title, const QString &text);

/*
 * Image.
 */
QString getContactImageString(const QString &accountId, const QString &uid);
QImage getCirclePhoto(const QImage original, int sizePhoto);
QImage conversationPhoto(const QString &convUid,
                         const lrc::api::account::Info &accountInfo,
                         bool filtered = false);
QColor getAvatarColor(const QString &canonicalUri);
QImage fallbackAvatar(const QSize size,
                      const QString &canonicalUriStr,
                      const QString &letterStr = QString());
QImage fallbackAvatar(const QSize size, const std::string &alias, const std::string &uri);
QByteArray QImageToByteArray(QImage image);
QByteArray QByteArrayFromFile(const QString &filename);
QPixmap generateTintedPixmap(const QString &filename, QColor color);
QPixmap generateTintedPixmap(const QPixmap &pix, QColor color);
QImage scaleAndFrame(const QImage photo, const QSize &size = IMAGE_SIZE);
QImage accountPhoto(const lrc::api::account::Info &accountInfo, const QSize &size = IMAGE_SIZE);
QImage cropImage(const QImage &img);
QPixmap pixmapFromSvg(const QString &svg_resource, const QSize &size);
QImage setupQRCode(QString ringID, int margin);

/*
 * Rounded corner.
 */
template<typename T>
void
fillRoundRectPath(QPainter &painter,
                  const T &brushType,
                  const QRect &rectToDraw,
                  qreal cornerRadius,
                  int xTransFormOffset = 0,
                  int yTransFormOffset = 0)
{
    QBrush brush(brushType);
    brush.setTransform(QTransform::fromTranslate(rectToDraw.x() + xTransFormOffset,
                                                 rectToDraw.y() + yTransFormOffset));
    QPainterPath painterPath;
    painterPath.addRoundRect(rectToDraw, cornerRadius);
    painter.fillPath(painterPath, brush);
}

/*
 * Time.
 */
QString formattedTime(int seconds);

/*
 * Byte to human readable size.
 */
QString humanFileSize(qint64 fileSize);

/*
 * Device plug or unplug enum.
 */
enum class DevicePlugStatus { Plugged, Unplugged, Unchanged };

class OneShotDisconnectConnection : public QObject
{
    Q_OBJECT

public:
    explicit OneShotDisconnectConnection(const QObject *sender,
                                         const char *signal,
                                         QMetaObject::Connection *connection,
                                         QObject *parent = nullptr)
        : QObject(parent)
    {
        connection_ = connection;
        disconnectConnection_ = new QMetaObject::Connection;
        *disconnectConnection_ = QObject::connect(sender,
                                                  signal,
                                                  this,
                                                  SLOT(slotOneShotDisconnectConnection()));
    }
    ~OneShotDisconnectConnection()
    {
        if (!connection_) {
            delete connection_;
        }
        if (!disconnectConnection_) {
            delete disconnectConnection_;
        }
        if (!this) {
            delete this;
        }
    }

public slots:
    void
    slotOneShotDisconnectConnection()
    {
        if (connection_) {
            QObject::disconnect(*connection_);
            delete connection_;
        }
        if (disconnectConnection_) {
            QObject::disconnect(*disconnectConnection_);
            delete disconnectConnection_;
        }
        delete this;
    }

private:
    QMetaObject::Connection *connection_;
    QMetaObject::Connection *disconnectConnection_;
};

template<typename Func1, typename Func2>
void
oneShotConnect(const typename QtPrivate::FunctionPointer<Func1>::Object *sender,
               Func1 signal,
               Func2 slot)
{
    QMetaObject::Connection *const connection = new QMetaObject::Connection;
    *connection = QObject::connect(sender, signal, slot);
    QMetaObject::Connection *const disconnectConnection = new QMetaObject::Connection;
    *disconnectConnection = QObject::connect(sender, signal, [connection, disconnectConnection] {
        if (connection) {
            QObject::disconnect(*connection);
            delete connection;
        }
        if (disconnectConnection) {
            QObject::disconnect(*disconnectConnection);
            delete disconnectConnection;
        }
    });
}

template<typename Func1, typename Func2>
void
oneShotConnect(const typename QtPrivate::FunctionPointer<Func1>::Object *sender,
               Func1 signal,
               const typename QtPrivate::FunctionPointer<Func2>::Object *receiver,
               Func2 slot)
{
    QMetaObject::Connection *const connection = new QMetaObject::Connection;
    *connection = QObject::connect(sender, signal, receiver, slot);
    QMetaObject::Connection *const disconnectConnection = new QMetaObject::Connection;
    *disconnectConnection = QObject::connect(sender, signal, [connection, disconnectConnection] {
        if (connection) {
            QObject::disconnect(*connection);
            delete connection;
        }
        if (disconnectConnection) {
            QObject::disconnect(*disconnectConnection);
            delete disconnectConnection;
        }
    });
}

inline void
oneShotConnect(const QObject *sender, const char *signal, const QObject *receiver, const char *slot)
{
    QMetaObject::Connection *const connection = new QMetaObject::Connection;
    *connection = QObject::connect(sender, signal, receiver, slot);
    OneShotDisconnectConnection *disconnectConnection = new OneShotDisconnectConnection(sender,
                                                                                        signal,
                                                                                        connection);
    Q_UNUSED(disconnectConnection)
}

template<class T>
class Blocker
{
    T *blocked;
    bool previous;

public:
    Blocker(T *blocked)
        : blocked(blocked)
        , previous(blocked->blockSignals(true))
    {}
    ~Blocker() { blocked->blockSignals(previous); }
    T *
    operator->()
    {
        return blocked;
    }
};

template<class T>
inline Blocker<T>
whileBlocking(T *blocked)
{
    return Blocker<T>(blocked);
}

template<typename T>
void
setElidedText(T *object,
              const QString &text,
              Qt::TextElideMode mode = Qt::ElideMiddle,
              int padding = 32)
{
    QFontMetrics metrics(object->font());
    QString clippedText = metrics.elidedText(text, mode, object->width() - padding);
    object->setText(clippedText);
}

template<typename E>
constexpr inline
    typename std::enable_if<std::is_enum<E>::value, typename std::underlying_type<E>::type>::type
    toUnderlyingValue(E e) noexcept
{
    return static_cast<typename std::underlying_type<E>::type>(e);
}

template<typename E, typename T>
constexpr inline
    typename std::enable_if<std::is_enum<E>::value && std::is_integral<T>::value, E>::type
    toEnum(T value) noexcept
{
    return static_cast<E>(value);
}
} // namespace Utils

class UtilsAdapter : public QObject
{
    Q_OBJECT
public:
    explicit UtilsAdapter(QObject *parent = nullptr)
        : QObject(parent)
    {
        clipboard_ = QApplication::clipboard();
    }
    ~UtilsAdapter() {}

    ///Singleton
    static UtilsAdapter &instance();

    Q_INVOKABLE const QString
    getChangeLog()
    {
        return Utils::getChangeLog();
    }

    Q_INVOKABLE const QString
    getProjectCredits()
    {
        return Utils::getProjectCredits();
    }

    Q_INVOKABLE const QString
    getVersionStr()
    {
        return QString(VERSION_STRING);
    }

    Q_INVOKABLE void
    setText(QString text)
    {
        clipboard_->setText(text, QClipboard::Clipboard);
    }

    Q_INVOKABLE const QString
    qStringFromFile(const QString &filename)
    {
        return Utils::QByteArrayFromFile(filename);
    }

    Q_INVOKABLE const QString
    getStyleSheet(const QString &name, const QString &source)
    {
        auto simplifiedCSS = source.simplified().replace("'", "\"");
        QString s = QString::fromLatin1("(function() {"
                                        "    var node = document.createElement('style');"
                                        "    node.id = '%1';"
                                        "    node.innerHTML = '%2';"
                                        "    document.head.appendChild(node);"
                                        "})()")
                        .arg(name)
                        .arg(simplifiedCSS);
        return s;
    }

    Q_INVOKABLE const QString
    getCachePath()
    {
        QDir dataDir(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation));
        dataDir.cdUp();
        return dataDir.absolutePath() + "/jami";
    }
    Q_INVOKABLE bool
    createStartupLink()
    {
        return Utils::CreateStartupLink(L"Jami");
    }
    Q_INVOKABLE QString
    GetRingtonePath()
    {
        return Utils::GetRingtonePath();
    }
    Q_INVOKABLE bool
    checkStartupLink()
    {
        return Utils::CheckStartupLink(L"Jami");
    }

    Q_INVOKABLE const QString
    getContactImageString(const QString &accountId, const QString &uid)
    {
        return Utils::getContactImageString(accountId, uid);
    }

    Q_INVOKABLE void removeConversation(const QString &accountId,
                                        const QString &uid,
                                        bool banContact = false);
    Q_INVOKABLE void clearConversationHistory(const QString &accountId, const QString &uid);
    Q_INVOKABLE void setConversationFilter(const QString &filter);
    Q_INVOKABLE int getTotalUnreadMessages();
    Q_INVOKABLE int getTotalPendingRequest();
    Q_INVOKABLE const QString getBestName(const QString &accountId, const QString &uid);
    Q_INVOKABLE const QString getBestId(const QString &accountId, const QString &uid);

    Q_INVOKABLE const QString getCurrAccId();
    Q_INVOKABLE const QStringList getCurrAccList();
    Q_INVOKABLE int getAccountListSize();
    Q_INVOKABLE void setCurrentCall(const QString &accountId, const QString &convUid);
    Q_INVOKABLE void startPreviewing(bool force);
    Q_INVOKABLE void stopPreviewing();
    Q_INVOKABLE bool hasVideoCall();
    Q_INVOKABLE const QString getCallId(const QString &accountId, const QString &convUid);
    Q_INVOKABLE QString getStringUTF8(QString string);
    Q_INVOKABLE bool validateRegNameForm(const QString &regName);
    Q_INVOKABLE QString getRecordQualityString(int value);
    Q_INVOKABLE QString getCurrentPath();
    Q_INVOKABLE QString
    stringSimplifier(QString input)
    {
        return input.simplified();
    }

    Q_INVOKABLE QString
    toNativeSeparators(QString inputDir)
    {
        return QDir::toNativeSeparators(inputDir);
    }

    Q_INVOKABLE QString
    toFileInfoName(QString inputFileName)
    {
        QFileInfo fi(inputFileName);
        return fi.fileName();
    }

    Q_INVOKABLE QString
    toFileAbsolutepath(QString inputFileName)
    {
        QFileInfo fi(inputFileName);
        return fi.absolutePath();
    }

    Q_INVOKABLE QString
    getAbsPath(QString path)
    {
#ifdef Q_OS_WIN
        return path.replace("file:///", "").replace("\n", "").replace("\r", "");
#else
        return path.replace("file:///", "/").replace("\n", "").replace("\r", "");
#endif
    }

    Q_INVOKABLE QString
    getCroppedImageBase64FromFile(QString fileName, int size)
    {
        auto image = Utils::cropImage(QImage(fileName));
        auto croppedImage = image.scaled(size,
                                         size,
                                         Qt::KeepAspectRatioByExpanding,
                                         Qt::SmoothTransformation);
        return QString::fromLatin1(Utils::QImageToByteArray(croppedImage).toBase64().data());
    }

private:
    QClipboard *clipboard_;
};
#if QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)
Q_DECLARE_METATYPE(UtilsAdapter *)
#endif
