/***************************************************************************
**
** Copyright (C) 2016 BlackBerry Limited. All rights reserved.
** Copyright (C) 2016 BasysKom GmbH.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtNfc module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#ifndef QNEARFIELDTARGET_NEARD_P_H
#define QNEARFIELDTARGET_NEARD_P_H

//
//  W A R N I N G
//  -------------
//
// This file is not part of the Qt API.  It exists purely as an
// implementation detail.  This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//

#include <QDBusObjectPath>
#include <QDBusVariant>

#include <qnearfieldtarget.h>
#include <qnearfieldtarget_p.h>
#include <qndefrecord.h>
#include <qndefmessage.h>

#include "neard/neard_helper_p.h"
#include "neard/dbusproperties_p.h"
#include "neard/dbusobjectmanager_p.h"
#include "neard/tag_p.h"

#include <qndefnfctextrecord.h>
#include <qndefnfcsmartposterrecord.h>
#include <qndefnfcurirecord.h>

QT_BEGIN_NAMESPACE

Q_DECLARE_LOGGING_CATEGORY(QT_NFC_NEARD)

template <typename T>
class NearFieldTarget : public T
{
public:

    NearFieldTarget(QObject *parent, QDBusObjectPath interfacePath)
        : T(parent),
          m_tagPath(interfacePath),
          m_readRequested(false)
    {
        m_readErrorTimer.setSingleShot(true);
        m_recordPathsCollectedTimer.setSingleShot(true);
        m_delayedWriteTimer.setSingleShot(true);

        qCDebug(QT_NFC_NEARD) << "tag found at path" << interfacePath.path();
        m_dbusProperties = new OrgFreedesktopDBusPropertiesInterface(QStringLiteral("org.neard"),
                                                                     interfacePath.path(),
                                                                     QDBusConnection::systemBus(),
                                                                     this);
        if (!m_dbusProperties->isValid()) {
            qCWarning(QT_NFC_NEARD) << "Could not connect to dbus property interface at path" << interfacePath.path();
            return;
        }

        QDBusPendingReply<QVariantMap> reply = m_dbusProperties->GetAll(QStringLiteral("org.neard.Tag"));
        reply.waitForFinished();
        if (reply.isError()) {
            qCWarning(QT_NFC_NEARD) << "Could not get properties of org.neard.Tag dbus interface";
            return;
        }

        const QString &type = reply.value().value(QStringLiteral("Type")).toString();
        m_type = QNearFieldTarget::ProprietaryTag;

        if (type == QStringLiteral("Type 1"))
            m_type = QNearFieldTarget::NfcTagType1;
        else if (type == QStringLiteral("Type 2"))
            m_type = QNearFieldTarget::NfcTagType2;
        else if (type == QStringLiteral("Type 3"))
            m_type = QNearFieldTarget::NfcTagType3;
        else if (type == QStringLiteral("Type 4"))
            m_type = QNearFieldTarget::NfcTagType4;

        qCDebug(QT_NFC_NEARD) << "tag type" << type;

        QObject::connect(&m_recordPathsCollectedTimer, &QTimer::timeout,
                         this,                         &NearFieldTarget::createNdefMessage);
        QObject::connect(&m_readErrorTimer, &QTimer::timeout,
                         this,              &NearFieldTarget::handleReadError);
        QObject::connect(&m_delayedWriteTimer, &QTimer::timeout,
                         this,                 &NearFieldTarget::handleWriteRequest);
        QObject::connect(NeardHelper::instance(), &NeardHelper::recordFound,
                         this,                    &NearFieldTarget::handleRecordFound);
    }

    ~NearFieldTarget()
    {
    }

    bool isValid()
    {
        return m_dbusProperties->isValid() && NeardHelper::instance()->dbusObjectManager()->isValid();
    }

    QByteArray uid() const
    {
        return QByteArray(); // TODO figure out a workaround because neard does not offer
                             // this property
    }

    QNearFieldTarget::Type type() const
    {
        return m_type;
    }

    QNearFieldTarget::AccessMethods accessMethods() const
    {
        return QNearFieldTarget::NdefAccess;
    }

    bool hasNdefMessage()
    {
        return !m_recordPaths.isEmpty();
    }

    QNearFieldTarget::RequestId readNdefMessages()
    {
        if (isValid()) {
            // if the user calls readNdefMessages before the previous request has been completed
            // return the current request id.
            if (m_currentReadRequestId.isValid())
                return m_currentReadRequestId;

            QNearFieldTarget::RequestId requestId = QNearFieldTarget::RequestId(new QNearFieldTarget::RequestIdPrivate());
            // save the id so it can be passed along with requestCompleted
            m_currentReadRequestId = requestId;
            // since the triggering of interfaceAdded will ultimately lead to createNdefMessage being called
            // we need to make sure that ndefMessagesRead will only be triggered when readNdefMessages has
            // been called before. In case readNdefMessages is called again after that we can directly call
            // call createNdefMessage.
            m_readRequested = true;
            if (hasNdefMessage())
                createNdefMessage();
            else
                m_readErrorTimer.start(1000);

            return requestId;
        } else {
            return QNearFieldTarget::RequestId();
        }
    }

    QNearFieldTarget::RequestId sendCommand(const QByteArray &command)
    {
        Q_UNUSED(command);
        return QNearFieldTarget::RequestId();
    }

    QNearFieldTarget::RequestId sendCommands(const QList<QByteArray> &commands)
    {
        Q_UNUSED(commands);
        return QNearFieldTarget::RequestId();
    }

    QNearFieldTarget::RequestId writeNdefMessages(const QList<QNdefMessage> &messages)
    {
        // disabling write due to neard crash (see QTBUG-43802)
        qWarning("QNearFieldTarget::WriteNdefMessages() disabled. See QTBUG-43802\n");
        return QNearFieldTarget::RequestId();


        // return old request id when previous write request hasn't completed
        if (m_currentWriteRequestId.isValid())
            return m_currentReadRequestId;

        qCDebug(QT_NFC_NEARD) << "writing messages";
        if (messages.isEmpty() || messages.first().isEmpty()) {
            qCWarning(QT_NFC_NEARD) << "No record specified";
            return QNearFieldTarget::RequestId();
        }
        if (messages.count() > 1 || messages.first().count() > 1) {
            // neard only supports one ndef record per tag
            qCWarning(QT_NFC_NEARD) << "Writing of only one NDEF record and message is supported";
            return QNearFieldTarget::RequestId();
        }
        QNdefRecord record = messages.first().first();

        if (record.typeNameFormat() == QNdefRecord::NfcRtd) {
            m_currentWriteRequestData.clear();
            if (record.isRecordType<QNdefNfcUriRecord>()) {
                m_currentWriteRequestData.insert(QStringLiteral("Type"), QStringLiteral("URI"));
                QNdefNfcUriRecord uriRecord = static_cast<QNdefNfcUriRecord>(record);
                m_currentWriteRequestData.insert(QStringLiteral("URI"), uriRecord.uri().toString());
            } else if (record.isRecordType<QNdefNfcSmartPosterRecord>()) {
                m_currentWriteRequestData.insert(QStringLiteral("Type"), QStringLiteral("SmartPoster"));
                QNdefNfcSmartPosterRecord spRecord = static_cast<QNdefNfcSmartPosterRecord>(record);
                m_currentWriteRequestData.insert(QStringLiteral("URI"), spRecord.uri().toString());
                // Currently neard only supports the uri property for writing
            } else if (record.isRecordType<QNdefNfcTextRecord>()) {
                m_currentWriteRequestData.insert(QStringLiteral("Type"), QStringLiteral("Text"));
                QNdefNfcTextRecord textRecord = static_cast<QNdefNfcTextRecord>(record);
                m_currentWriteRequestData.insert(QStringLiteral("Representation"), textRecord.text());
                m_currentWriteRequestData.insert(QStringLiteral("Encoding"),
                                        textRecord.encoding() == QNdefNfcTextRecord::Utf8 ?
                                              QStringLiteral("UTF-8") : QStringLiteral("UTF-16") );
                m_currentWriteRequestData.insert(QStringLiteral("Language"), textRecord.locale());
            } else {
                qCWarning(QT_NFC_NEARD) << "Record type not supported for writing";
                return QNearFieldTarget::RequestId();
            }

            m_currentWriteRequestId = QNearFieldTarget::RequestId(new QNearFieldTarget::RequestIdPrivate());
            // trigger delayed write
            m_delayedWriteTimer.start(100);

            return m_currentWriteRequestId;
        }

        return QNearFieldTarget::RequestId();
    }

private:
    QNdefRecord readRecord(const QDBusObjectPath &path)
    {
        qCDebug(QT_NFC_NEARD) << "reading record for path" << path.path();
        OrgFreedesktopDBusPropertiesInterface recordInterface(QStringLiteral("org.neard"),
                                                              path.path(),
                                                              QDBusConnection::systemBus());
        if (!recordInterface.isValid())
            return QNdefRecord();

        QDBusPendingReply<QVariantMap> reply = recordInterface.GetAll(QStringLiteral("org.neard.Record"));
        reply.waitForFinished();
        if (reply.isError())
            return QNdefRecord();

        const QString &value = reply.value().value(QStringLiteral("Representation")).toString();
        const QString &locale = reply.value().value(QStringLiteral("Language")).toString();
        const QString &encoding = reply.value().value(QStringLiteral("Encoding")).toString();
        const QString &uri = reply.value().value(QStringLiteral("URI")).toString();

//        const QString &mime = reply.value().value(QStringLiteral("MIME")).toString();
//        const QString &arr = reply.value().value(QStringLiteral("ARR")).toString();

        const QString type = reply.value().value(QStringLiteral("Type")).toString();
        if (type == QStringLiteral("Text")) {
            QNdefNfcTextRecord textRecord;
            textRecord.setText(value);
            textRecord.setLocale(locale);
            textRecord.setEncoding((encoding == QStringLiteral("UTF-8")) ? QNdefNfcTextRecord::Utf8
                                                                       : QNdefNfcTextRecord::Utf16);
            return textRecord;
        } else if (type == QStringLiteral("SmartPoster")) {
            QNdefNfcSmartPosterRecord spRecord;
            if (!value.isEmpty()) {
                spRecord.addTitle(value, locale, (encoding == QStringLiteral("UTF-8"))
                                                        ? QNdefNfcTextRecord::Utf8
                                                        : QNdefNfcTextRecord::Utf16);
            }

            if (!uri.isEmpty())
                spRecord.setUri(QUrl(uri));

            const QString &action = reply.value().value(QStringLiteral("Action")).toString();
            if (!action.isEmpty()) {
                if (action == QStringLiteral("Do"))
                    spRecord.setAction(QNdefNfcSmartPosterRecord::DoAction);
                else if (action == QStringLiteral("Save"))
                    spRecord.setAction(QNdefNfcSmartPosterRecord::SaveAction);
                else if (action == QStringLiteral("Edit"))
                    spRecord.setAction(QNdefNfcSmartPosterRecord::EditAction);
            }

            if (reply.value().contains(QStringLiteral("Size"))) {
                uint size = reply.value().value(QStringLiteral("Size")).toUInt();
                spRecord.setSize(size);
            }

            const QString &mimeType = reply.value().value(QStringLiteral("MIMEType")).toString();
            if (!mimeType.isEmpty()) {
                spRecord.setTypeInfo(mimeType.toUtf8());
            }


            return spRecord;
        } else if (type == QStringLiteral("URI")) {
            QNdefNfcUriRecord uriRecord;
            uriRecord.setUri(QUrl(uri));
            return uriRecord;
        } else if (type == QStringLiteral("MIME")) {

        } else if (type == QStringLiteral("AAR")) {

        }

        return QNdefRecord();
    }

    void handleRecordFound(const QDBusObjectPath &path)
    {
        m_recordPaths.append(path);
        // FIXME: this timer only exists because neard doesn't currently supply enough
        // information to let us know when all record interfaces have been added or
        // how many records are actually contained on a tag. We assume that when no
        // signal has been received for 100ms all record interfaces have been added.
        m_recordPathsCollectedTimer.start(100);
        // as soon as record paths have been added we can handle errors without the timer.
        m_readErrorTimer.stop();
    }

    void createNdefMessage()
    {
        if (m_readRequested) {
            qCDebug(QT_NFC_NEARD) << "creating Ndef message, reading" << m_recordPaths.length() << "record paths";
            QNdefMessage newNdefMessage;
            for (const QDBusObjectPath &recordPath : qAsConst(m_recordPaths))
                newNdefMessage.append(readRecord(recordPath));

            if (!newNdefMessage.isEmpty()) {
                QMetaObject::invokeMethod(this, "ndefMessageRead", Qt::QueuedConnection,
                                          Q_ARG(QNdefMessage, newNdefMessage));
                // the request id in requestCompleted has to match the one created in readNdefMessages
                QMetaObject::invokeMethod(this, [this]() {
                    Q_EMIT this->requestCompleted(this->m_currentReadRequestId);
                }, Qt::QueuedConnection);
            } else {
                this->reportError(QNearFieldTarget::UnknownError, m_currentReadRequestId);
            }

            m_readRequested = false;
            // invalidate the current request id
            m_currentReadRequestId = QNearFieldTarget::RequestId(0);
        }
    }

    void handleReadError()
    {
        emit QNearFieldTarget::error(QNearFieldTarget::UnknownError, m_currentReadRequestId);
        m_currentReadRequestId = QNearFieldTarget::RequestId(0);
    }

    void handleWriteRequest()
    {
        OrgNeardTagInterface tagInterface(QStringLiteral("org.neard"),
                                          m_tagPath.path(),
                                          QDBusConnection::systemBus());
        if (!tagInterface.isValid()) {
            qCWarning(QT_NFC_NEARD) << "tag interface invalid";
        } else {
            QDBusPendingReply<> reply;
            reply = tagInterface.Write(m_currentWriteRequestData);
            reply.waitForFinished();
            if (reply.isError()) {
                qCWarning(QT_NFC_NEARD) << "Error writing to NFC tag" << reply.error();
                this->reportError(QNearFieldTarget::UnknownError, m_currentWriteRequestId);
            }

            QMetaObject::invokeMethod(this, "ndefMessagesWritten", Qt::QueuedConnection);
            QMetaObject::invokeMethod(this, [this]() {
                Q_EMIT this->requestCompleted(this->m_currentWriteRequestId);
            }, Qt::QueuedConnection);
        }

        // invalidate current write request
        m_currentWriteRequestId = QNearFieldTarget::RequestId(0);
    }

protected:
    QDBusObjectPath m_tagPath;
    OrgFreedesktopDBusPropertiesInterface *m_dbusProperties;
    QList<QDBusObjectPath> m_recordPaths;
    QTimer m_recordPathsCollectedTimer;
    QTimer m_readErrorTimer;
    QTimer m_delayedWriteTimer;
    QNearFieldTarget::Type m_type;
    bool m_readRequested;
    QNearFieldTarget::RequestId m_currentReadRequestId;
    QNearFieldTarget::RequestId m_currentWriteRequestId;
    QVariantMap m_currentWriteRequestData;
};

QT_END_NAMESPACE

#endif // QNEARFIELDTARGET_NEARD_P_H
