/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; version 3. This project 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 Lesser General Public License
* along with this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#include "doorbird.h"
#include "extern-plugininfo.h"

#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QAuthenticator>
#include <QTimer>
#include <QImage>
#include <QUrl>
#include <QUrlQuery>

Doorbird::Doorbird(const QHostAddress &address, QObject *parent) :
    QObject(parent),
    m_address(address)
{
    m_networkAccessManager = new QNetworkAccessManager(this);
}

QHostAddress Doorbird::address()
{
    return m_address;
}

void Doorbird::setAddress(const QHostAddress &address)
{
    m_address = address;
}

QUuid Doorbird::getSession(const QString &username, const QString &password)
{
    QUrl url;
    url.setHost(m_address.toString());
    url.setScheme("http");
    url.setPath("/bha-api/getsession.cgi");
    url.setUserName(username);
    url.setPassword(password);
    QNetworkRequest request(url);
    qCDebug(dcDoorBird) << "Sending request:" << request.url();
    QNetworkReply *reply = m_networkAccessManager->get(request);
    QUuid requestId = QUuid::createUuid();

    connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
    connect(reply, &QNetworkReply::finished, this, [this, reply, requestId](){

        if (reply->error() != QNetworkReply::NoError) {
            qCWarning(dcDoorBird) << "Error DoorBird thing:" << reply->errorString();
            emit requestSent(requestId, false);
            return;
        }
        emit requestSent(requestId, true);
        QByteArray data = reply->readAll();
        QJsonParseError error;
        QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
        if (error.error != QJsonParseError::NoError) {
            qCWarning(dcDoorBird()) << "Error parsing json:" << data;
            return;
        }
        QVariantMap map = jsonDoc.toVariant().toMap().value("BHA").toMap();
        if (map.contains("SESSIONID")) {
            QString sessionId = map.value("SESSIONID").toString();
            qCDebug(dcDoorBird) << "Got sessionId" << sessionId;
            emit sessionIdReceived(sessionId);
        }
    });

    return requestId;
}

QUuid Doorbird::openDoor(int value)
{
    QNetworkRequest request(QString("http://%1/bha-api/open-door.cgi?r=%2").arg(m_address.toString()).arg(QString::number(value)));
    qCDebug(dcDoorBird) << "Sending request:" << request.url();
    QNetworkReply *reply = m_networkAccessManager->get(request);
    QUuid requestId = QUuid::createUuid();
    connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
    connect(reply, &QNetworkReply::finished, this, [this, reply, requestId](){

        if (reply->error() != QNetworkReply::NoError) {
            qCWarning(dcDoorBird) << "Error opening DoorBird "  << reply->error() << reply->errorString();
            emit requestSent(requestId, false);
            return;
        }
        qCDebug(dcDoorBird) << "DoorBird unlatched:";
        emit requestSent(requestId, true);
    });
    return requestId;
}

QUuid Doorbird::lightOn()
{
    QNetworkRequest request(QString("http://%1/bha-api/light-on.cgi").arg(m_address.toString()));
    qCDebug(dcDoorBird) << "Sending request:" << request.url();
    QNetworkReply *reply = m_networkAccessManager->get(request);
    QUuid requestId = QUuid::createUuid();
    connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
    connect(reply, &QNetworkReply::finished, this, [this, reply, requestId](){

        if (reply->error() != QNetworkReply::NoError) {
            qCWarning(dcDoorBird) << "Error light on"  << reply->error() << reply->errorString();
            emit requestSent(requestId, false);
            return;
        }
        qCDebug(dcDoorBird) << "DoorBird light on:";
        emit requestSent(requestId, true);
    });
    return requestId;
}

QUuid Doorbird::liveVideoRequest()
{
    QNetworkRequest request(QString("http://%1/bha-api/video.cgi").arg(m_address.toString()));
    qCDebug(dcDoorBird) << "Sending request:" << request.url();
    QNetworkReply *reply = m_networkAccessManager->get(request);
    QUuid requestId = QUuid::createUuid();
    connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
    connect(reply, &QNetworkReply::finished, this, [this, reply, requestId](){

        if (reply->error() != QNetworkReply::NoError) {
            qCWarning(dcDoorBird) << "Error live video request" << reply->error() << reply->errorString();
            emit requestSent(requestId, false);
            return;
        }
        qCDebug(dcDoorBird) << "DoorBird live video request" ;
        emit requestSent(requestId, true);
    });
    return requestId;
}

QUuid Doorbird::liveImageRequest()
{
    QNetworkRequest request(QString("http://%1/bha-api/image.cgi").arg(m_address.toString()));
    qCDebug(dcDoorBird) << "Sending request:" << request.url();
    QNetworkReply *reply = m_networkAccessManager->get(request);
    QUuid requestId = QUuid::createUuid();
    connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
    connect(reply, &QNetworkReply::finished, this, [this, reply, requestId](){

        if (reply->error() != QNetworkReply::NoError) {
            qCWarning(dcDoorBird) << "Error live image request"  << reply->error() << reply->errorString();
            emit requestSent(requestId, false);
            return;
        }

        QByteArrayList tokens = reply->readAll().split('\n');
        QImage image = QImage::fromData(tokens.last());
        //image.save("testfile");
        emit liveImageReceived(image);
        qCDebug(dcDoorBird) << "DoorBird live image request:";
        emit requestSent(requestId, true);
    });
    return requestId;
}

QUuid Doorbird::historyImageRequest(int index)
{
    QUrl url(QString("http://%1/bha-api/history.cgi").arg(m_address.toString()));
    QUrlQuery query;
    query.addQueryItem("index", QString::number(index));
    url.setQuery(query);

    qCDebug(dcDoorBird) << "Sending request:" << url;
    QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url));
    QUuid requestId = QUuid::createUuid();
    connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
    connect(reply, &QNetworkReply::finished, this, [this, reply, requestId](){

        if (reply->error() != QNetworkReply::NoError) {
            qCWarning(dcDoorBird) << "Error history image request" << reply->error() << reply->errorString();
            emit requestSent(requestId, false);
            return;
        }
        qCDebug(dcDoorBird) << "DoorBird history image received:";
        emit requestSent(requestId, true);
    });
    return requestId;
}

QUuid Doorbird::liveAudioReceive()
{
    QNetworkRequest request(QString("http://%1/bha-api/audio-receive.cgi").arg(m_address.toString()));
    qCDebug(dcDoorBird) << "Sending request:" << request.url();
    QNetworkReply *reply = m_networkAccessManager->get(request);
    QUuid requestId = QUuid::createUuid();
    connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
    connect(reply, &QNetworkReply::finished, this, [this, reply, requestId](){

        if (reply->error() != QNetworkReply::NoError) {
            qCWarning(dcDoorBird) << "Error live audio receive";
            emit requestSent(requestId, false);
            return;
        }
        qCDebug(dcDoorBird) << "DoorBird live audio receive";
        emit requestSent(requestId, true);
    });
    return requestId;
}

QUuid Doorbird::liveAudioTransmit()
{
    return QUuid::createUuid();
}

QUuid Doorbird::infoRequest()
{
    QNetworkRequest request(QString("http://%1/bha-api/info.cgi").arg(m_address.toString()));
    qCDebug(dcDoorBird) << "Sending request:" << request.url();
    QNetworkReply *reply = m_networkAccessManager->get(request);
    QUuid requestId = QUuid::createUuid();
    connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
    connect(reply, &QNetworkReply::finished, this, [this, reply, requestId](){

        if (reply->error() != QNetworkReply::NoError) {
            qCWarning(dcDoorBird) << "Error DoorBird" << reply->error() << reply->errorString();
            emit requestSent(requestId, false);
            return;
        }
        qCDebug(dcDoorBird) << "DoorBird info:" << reply->readAll() ;
        emit requestSent(requestId, true);
    });
    return requestId;
}

QUuid Doorbird::listFavorites()
{
    QNetworkRequest request(QString("http://%1/bha-api/favorites.cgi").arg(m_address.toString()));
    qCDebug(dcDoorBird) << "Sending request:" << request.url();
    QNetworkReply *reply = m_networkAccessManager->get(request);
    QUuid requestId = QUuid::createUuid();
    connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
    connect(reply, &QNetworkReply::finished, this, [this, reply, requestId](){

        if (reply->error() != QNetworkReply::NoError) {
            qCWarning(dcDoorBird) << "Error DoorBird device" << reply->error() << reply->errorString();
            emit requestSent(requestId, false);
            return;
        }
        emit requestSent(requestId, true);
    });
    return requestId;
}

QUuid Doorbird::addFavorite(FavoriteType type, const QString &name, const QUrl &commandUrl, int id)
{
    QUrl url(QString("http://%1/bha-api/favorites.cgi").arg(m_address.toString()));
    QUrlQuery query;
    query.addQueryItem("action", "save");
    if (type == FavoriteType::Http) {
        query.addQueryItem("type", "http");
    } else {
        query.addQueryItem("type", "sip");
    }
    query.addQueryItem("title", name);
    query.addQueryItem("value", commandUrl.toString());
    query.addQueryItem("id", QString::number(id));
    url.setQuery(query);

    QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(url));
    QUuid requestId = QUuid::createUuid();
    connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
    connect(reply, &QNetworkReply::finished, this, [this, reply, requestId](){

        if (reply->error() != QNetworkReply::NoError) {
            qCWarning(dcDoorBird) << "Error DoorBird device" << reply->error() << reply->errorString();
            emit requestSent(requestId, false);
            return;
        }
        emit requestSent(requestId, true);
    });
    return requestId;
}

QUuid Doorbird::listSchedules()
{
    QNetworkRequest request(QString("http://%1/bha-api/schedule.cgi").arg(m_address.toString()));
    qCDebug(dcDoorBird) << "Sending request:" << request.url();
    QNetworkReply *reply = m_networkAccessManager->get(request);
    QUuid requestId = QUuid::createUuid();
    connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
    connect(reply, &QNetworkReply::finished, this, [this, reply, requestId](){

        if (reply->error() != QNetworkReply::NoError) {
            qCWarning(dcDoorBird) << "Error DoorBird device" << reply->error() << reply->errorString();
            emit requestSent(requestId, false);
            return;
        }
        emit requestSent(requestId, true);
    });
    return requestId;
}

QUuid Doorbird::restart()
{
    QNetworkRequest request(QString("http://%1/bha-api/restart.cgi").arg(m_address.toString()));
    qCDebug(dcDoorBird) << "Sending request:" << request.url();
    QNetworkReply *reply = m_networkAccessManager->get(request);
    QUuid requestId = QUuid::createUuid();
    connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
    connect(reply, &QNetworkReply::finished, this, [this, reply, requestId](){

        if (reply->error() != QNetworkReply::NoError) {
            qCWarning(dcDoorBird) << "Error restarting DoorBird device" << reply;
            emit requestSent(requestId, false);
            return;
        }
        qCDebug(dcDoorBird) << "DoorBird restarting";
        emit requestSent(requestId, true);
    });
    return requestId;
}

void Doorbird::connectToEventMonitor()
{
    qCDebug(dcDoorBird) << "Starting monitoring";

    QNetworkRequest request(QString("http://%1/bha-api/monitor.cgi?ring=doorbell,motionsensor").arg(m_address.toString()));
    QNetworkReply *reply = m_networkAccessManager->get(request);

    connect(reply, &QNetworkReply::downloadProgress, this, [this, reply](qint64 bytesReceived, qint64 bytesTotal){
        Q_UNUSED(bytesReceived)
        Q_UNUSED(bytesTotal);

        emit deviceConnected(true);
        m_readBuffer.append(reply->readAll());
        qCDebug(dcDoorBird) << "Event received" << m_readBuffer;

        // Input data looks like:
        // "--ioboundary\r\nContent-Type: text/plain\r\n\r\ndoorbell:H\r\n\r\n"

        while (!m_readBuffer.isEmpty()) {
            // find next --ioboundary
            QString boundary = QStringLiteral("--ioboundary");
            int startIndex = m_readBuffer.indexOf(boundary);
            if (startIndex == -1) {
                qCWarning(dcDoorBird) << "No meaningful data in buffer:" << m_readBuffer;
                if (m_readBuffer.size() > 1024) {
                    qCWarning(dcDoorBird) << "Buffer size > 1KB and still no meaningful data. Discarding buffer...";
                    m_readBuffer.clear();
                }
                // Assuming we don't have enough data yet...
                return;
            }

            QByteArray contentType = QByteArrayLiteral("Content-Type: text/plain");
            int contentTypeIndex = m_readBuffer.indexOf(contentType);
            if (contentTypeIndex == -1) {
                qCWarning(dcDoorBird) << "Cannot find Content-Type in buffer:" << m_readBuffer;
                if (m_readBuffer.size() > startIndex + 50) {
                    qCWarning(dcDoorBird) << boundary << "found but unexpected data follows. Skipping this element...";
                    m_readBuffer.remove(0, startIndex + boundary.length());
                    continue;
                }
                // Assuming we don't have enough data yet...
                return;
            }

            // At this point we have the boundary and Content-Type. Remove all of that and take the entire string to either end or next boundary
            m_readBuffer.remove(0, contentTypeIndex + contentType.length());
            int nextStartIndex = m_readBuffer.indexOf(boundary);
            QByteArray data;
            if (nextStartIndex == -1) {
                data = m_readBuffer;
                m_readBuffer.clear();
            } else {
                data = m_readBuffer.left(nextStartIndex);
                m_readBuffer.remove(0, nextStartIndex);
            }

            QString message = data.trimmed();
            QStringList parts = message.split(":");
            if (parts.count() != 2) {
                qCWarning(dcDoorBird) << "Message has invalid format:" << message << "Expected device:state";
                continue;
            }
            if (parts.first() == "doorbell") {
                if (parts.at(1) == "H") {
                    qCDebug(dcDoorBird) << "Doorbell ringing!";
                    emit eventReveiced(EventType::Doorbell, true);
                } else {
                    emit eventReveiced(EventType::Doorbell, false);
                }
            } else if (parts.first() == "motionsensor") {
                if (parts.at(1) == "H") {
                    qCDebug(dcDoorBird) << "Motion sensor detected a person";
                    emit eventReveiced(EventType::Motion, true);
                } else {
                    emit eventReveiced(EventType::Motion, false);
                }
            } else {
                qCWarning(dcDoorBird) << "Unhandled DoorBird data:" << message;
            }
        }
        qCDebug(dcDoorBird()) << "Event read buffer size" << m_readBuffer.size();
    });

    connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
    connect(reply, &QNetworkReply::finished, this, [this, reply]() {

        emit deviceConnected(false);
        m_readBuffer.clear();
        qCDebug(dcDoorBird) << "Monitor request finished:" << reply->error();
        qCDebug(dcDoorBird) << "    - Trying to reconnect in 5 seconds";
        QTimer::singleShot(2000, this, [this] {
            qCDebug(dcDoorBird) << "    - Reconnecting now";
            connectToEventMonitor();
        });
    });
}
