/******************************************************************************
**
** Copyright (C) 2018 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtKnx module.
**
** $QT_BEGIN_LICENSE:GPL$
** 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 General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 or (at your option) 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.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-3.0.html.
**
** $QT_END_LICENSE$
**
******************************************************************************/

#include "tunnelingfeatures.h"
#include "ui_tunnelingfeatures.h"

#include <QtCore/QMetaEnum>
#include <QtCore/QMetaType>
#include <QtKnx/QKnx1Bit>
#include <QtKnx/QKnx>
#include <QtKnx/QKnxUtils>

bool validFeature(const QKnx::NetIp::ServiceType &frameType, const QKnx::InterfaceFeature &feature,
                  const QKnxByteArray &value)
{
    using ServType = QKnx::NetIp::ServiceType;
    if (frameType != ServType::TunnelingFeatureSet)
        return true;

    using FeatureType = QKnx::InterfaceFeature;
    switch (feature) {
        case FeatureType::SupportedEmiType:
        case FeatureType::HostDeviceDescriptorType0:
        case FeatureType::KnxManufacturerCode:
        case FeatureType::IndividualAddress:
        case FeatureType::MaximumApduLength:
            return value.size() == 2;
        case FeatureType::BusConnectionStatus:
        case FeatureType::InterfaceFeatureInfoServiceEnable:
            return value.size() == 1 && ((value.at(0) == 0x01) || (value.at(0) == 0x00));
        case FeatureType::ActiveEmiType:
            return value.size() == 1;
        default:
            break;
    }

    return false;
}

TunnelingFeatures::TunnelingFeatures(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::TunnelingFeatures)
{
    ui->setupUi(this);

    ui->tunnelServiceType->setEnabled(false);
    ui->featureIdentifier->setEnabled(false);
    ui->featureValue->setEnabled(false);

    connect(ui->connectTunneling, &QPushButton::clicked, this, [&]() {
        ui->textOuputTunneling->append(tr("Connecting to: %1 on port: %2 protocol: %3")
            .arg(m_server.controlEndpointAddress().toString())
            .arg(m_server.controlEndpointPort()).arg(int(m_protocol)));

        m_tunnel.setLocalPort(0);
        if (ui->secureSessionCheckBox->isChecked()) {
            auto config = m_configs.value(ui->secureSessionCb->currentIndex());
            config.setKeepSecureSessionAlive(true);
            m_tunnel.setSecureConfiguration(config);
            m_tunnel.connectToHostEncrypted(m_server.controlEndpointAddress(),
                m_server.controlEndpointPort());
        } else {
            m_tunnel.connectToHost(m_server.controlEndpointAddress(),
                m_server.controlEndpointPort(), m_protocol);
        }
    });

    connect(ui->tunnelingSend, &QPushButton::clicked, this, [&]() {
        using ServType = QKnx::NetIp::ServiceType;
        ServType type = ServType(quint16(ServType::TunnelingFeatureGet));
        if (ui->tunnelServiceType->currentIndex() == 1)
            type = ServType(quint16(ServType::TunnelingFeatureSet));

        using FeatureType = QKnx::InterfaceFeature;
        FeatureType featureType = FeatureType(quint8(FeatureType::SupportedEmiType)
            + ui->featureIdentifier->currentIndex());

        QKnxByteArray bytes = QKnxByteArray::fromHex(ui->featureValue->text().toUtf8());
        QKnxNetIpFrame frame;

        if (type == ServType::TunnelingFeatureGet)
            m_tunnel.sendTunnelingFeatureGet(featureType);
        else if (type == ServType::TunnelingFeatureSet)
            m_tunnel.sendTunnelingFeatureSet(featureType, bytes);

        ui->textOuputTunneling->append(tr("Status: (%1) Messages sent.").arg(m_tunnel
            .sequenceCount(QKnxNetIpEndpointConnection::SequenceType::Send) + 1));
    });

    connect(&m_tunnel, &QKnxNetIpTunnel::tunnelingFeatureInfoReceived, this,
        [&](QKnx::InterfaceFeature feature, const QKnxByteArray &value) {

        auto metaEnum = QMetaEnum::fromType<QKnx::InterfaceFeature>();
        if (feature == QKnx::InterfaceFeature::InterfaceFeatureInfoServiceEnable) {
            auto state = QKnxSwitch::State(value.at(0));
            auto metaEnumState = QMetaEnum::fromType<QKnxSwitch::State>();
            ui->textOuputTunneling->append(tr("Received Tunneling Feature Info: Feature (%1), "
                "Value (%2)")
            .arg(QString::fromLatin1(metaEnum.valueToKey(int(feature))))
            .arg(QString::fromLatin1(metaEnumState.valueToKey(int(state)))));
        } else {
            tr("Received Tunneling Feature Info: Feature (%1), Value: ")
                .arg(metaEnum.valueToKey(int(feature)))
                + QLatin1String(value.toByteArray(), value.size());
        }
    });

    connect(&m_tunnel, &QKnxNetIpTunnel::tunnelingFeatureResponseReceived, this,
        [&](QKnx::InterfaceFeature feature, QKnx::ReturnCode code, const QKnxByteArray &value) {

        auto metaEnum = QMetaEnum::fromType<QKnx::InterfaceFeature>();
        auto metaReturnCode = QMetaEnum::fromType<QKnx::ReturnCode>();
        if (feature == QKnx::InterfaceFeature::InterfaceFeatureInfoServiceEnable
            || feature == QKnx::InterfaceFeature::BusConnectionStatus) {
            auto state = QKnxSwitch::State(value.at(0));
            auto metaEnumState = QMetaEnum::fromType<QKnxSwitch::State>();
            ui->textOuputTunneling->append(tr("Received Tunneling Feature Response: Feature (%1), "
                "Return Code (%2), Value (%3)")
            .arg(QString::fromLatin1(metaEnum.valueToKey(int(feature))))
            .arg(QString::fromLatin1(metaReturnCode.valueToKey(int(code))))
            .arg(QString::fromLatin1(metaEnumState.valueToKey(int(state)))));
        } else if (feature == QKnx::InterfaceFeature::IndividualAddress) {
            ui->textOuputTunneling->append(tr("Received Tunneling Feature Response: Feature (%1), "
                "Return Code (%2), Individual Address Value (%3)")
            .arg(QString::fromLatin1(metaEnum.valueToKey(int(feature))))
            .arg(QString::fromLatin1(metaReturnCode.valueToKey(int(code))))
            .arg(QKnxAddress(QKnxAddress::Type::Individual, value).toString()));
        } else if (feature == QKnx::InterfaceFeature::KnxManufacturerCode
                   || feature == QKnx::InterfaceFeature::MaximumApduLength) {
            ui->textOuputTunneling->append(tr("Received Tunneling Feature Response: Feature (%1), "
                "Return Code (%2), Value (%3)")
            .arg(QString::fromLatin1(metaEnum.valueToKey(int(feature))))
            .arg(QString::fromLatin1(metaReturnCode.valueToKey(int(code))))
            .arg(QKnxUtils::QUint16::fromBytes(value)));
        } else if (feature == QKnx::InterfaceFeature::ActiveEmiType
                   || feature == QKnx::InterfaceFeature::SupportedEmiType) {
            QString str;
            auto  types = QKnx::EmiTypes(value.at(0));
            auto metaEmiType =  QMetaEnum::fromType<QKnx::EmiType>();
            if (types.testFlag(QKnx::EmiType::EMI1))
                str = QLatin1String(metaEmiType.valueToKey(int(QKnx::EmiType::EMI1)));
            if (types.testFlag(QKnx::EmiType::EMI2))
                str += "|" + QLatin1String(metaEmiType.valueToKey(int(QKnx::EmiType::EMI2)));
            if (types.testFlag(QKnx::EmiType::cEMI))
                str += "|" + QLatin1String(metaEmiType.valueToKey(int(QKnx::EmiType::cEMI)));
            ui->textOuputTunneling->append(tr("Received Tunneling Feature Response: Feature (%1), "
                "Return Code (%2), EMI (%3)")
            .arg(QString::fromLatin1(metaEnum.valueToKey(int(feature))))
            .arg(QString::fromLatin1(metaReturnCode.valueToKey(int(code))))
            .arg(str.isEmpty() ? QLatin1String(metaEmiType.valueToKey(int(QKnx::EmiType::Unknown)))
                : str));
        } else {
            ui->textOuputTunneling->append(tr("Received Tunneling Feature Response: Feature (%1), "
                    "Return Code (%2), Value: ")
                .arg(metaEnum.valueToKey(int(feature)))
                .arg(QString::fromLatin1(metaReturnCode.valueToKey(int(code))))
                + QLatin1String(value.toByteArray(), value.size()));
        }
    });

    connect(&m_tunnel, &QKnxNetIpTunnel::connected, this, [&] {
        ui->connectTunneling->setEnabled(false);
        ui->disconnectTunneling->setEnabled(true);
        ui->tunnelServiceType->setEnabled(true);
        ui->featureIdentifier->setEnabled(true);
        if (ui->tunnelServiceType->currentText() == "TunnelingFeatureSet") {
            ui->featureValue->setEnabled(true);
            ui->tunnelingSend->setEnabled(!ui->featureValue->text().isEmpty());
        } else {
            ui->featureValue->setEnabled(false);
            ui->tunnelingSend->setEnabled(true);
        }
        ui->textOuputTunneling->append(tr("Successfully connected to: %1 on port: %2").arg(m_server
            .controlEndpointAddress().toString()).arg(m_server.controlEndpointPort()));

        ui->textOuputTunneling->append("Status: Connected.");
    });

    connect(ui->disconnectTunneling, &QPushButton::clicked, this, [&]() {
        m_tunnel.disconnectFromHost();
    });

    connect(&m_tunnel, &QKnxNetIpTunnel::disconnected, this, [&] {
        ui->connectTunneling->setEnabled(true);
        ui->disconnectTunneling->setEnabled(false);
        ui->tunnelingSend->setEnabled(false);
        ui->tunnelServiceType->setEnabled(false);
        ui->featureIdentifier->setEnabled(false);
        ui->featureValue->setEnabled(false);
        ui->textOuputTunneling->append(tr("Successfully disconnected from: %1 on port: %2\n")
            .arg(m_server.controlEndpointAddress().toString()).arg(m_server.controlEndpointPort()));
        ui->textOuputTunneling->append("Status: Disconnected.");
    });

    connect(&m_tunnel, &QKnxNetIpTunnel::errorOccurred, this,
        [&] (QKnxNetIpEndpointConnection::Error, QString errorString) {
            ui->textOuputTunneling->append(errorString);
    });

    connect(ui->tunnelServiceType, &QComboBox::currentTextChanged, this, [&](const QString &text) {
        if (text == QString("TunnelingFeatureSet")) {
            ui->featureValue->setEnabled(true);
            ui->textOuputTunneling->append("Status: Fill in the feature type and value fields.");
        } else {
            ui->featureValue->setEnabled(false);
            ui->textOuputTunneling->append("");
        }
        checkFeatureValue();
    });

    connect(ui->featureValue, &QLineEdit::textChanged, this, [&](const QString &) {
        checkFeatureValue();
    });

    connect(ui->featureIdentifier, &QComboBox::currentTextChanged, this, [&](const QString &) {
        checkFeatureValue();
    });
}

TunnelingFeatures::~TunnelingFeatures()
{
    delete ui;
}

void TunnelingFeatures::setNatAware(bool isNatAware)
{
    m_tunnel.setNatAware(isNatAware);
}

void TunnelingFeatures::setLocalAddress(const QHostAddress &address)
{
    m_tunnel.disconnectFromHost();
    m_tunnel.setLocalAddress(address);
}

void TunnelingFeatures::setKnxNetIpServer(const QKnxNetIpServerInfo &server)
{
    m_tunnel.disconnectFromHost();
    m_server = server;

    if (m_tunnel.state() == QKnxNetIpEndpointConnection::State::Disconnected) {
        ui->connectTunneling->setEnabled(true);
        ui->disconnectTunneling->setEnabled(false);
    }

    updateSecureConfigCombo();
    ui->tunnelingSend->setEnabled(false);
    ui->textOuputTunneling->append("Status: Start by clicking connect.");
}

void TunnelingFeatures::setTcpEnable(bool value)
{
    m_protocol = (value ? QKnxNetIp::HostProtocol::TCP_IPv4 : QKnxNetIp::HostProtocol::UDP_IPv4);
}

void TunnelingFeatures::onKeyringChanged(const QVector<QKnxNetIpSecureConfiguration> &configs)
{
    m_configs = configs;
    updateSecureConfigCombo();
}

void TunnelingFeatures::checkFeatureValue()
{
    if (!ui->featureValue->isEnabled()) {
        ui->textOuputTunneling->append("");
        ui->tunnelingSend->setEnabled(m_tunnel.state() == QKnxNetIpEndpointConnection::State::Connected);
        return;
    }

    using ServType = QKnx::NetIp::ServiceType;
    ServType type = ServType(quint16(ServType::TunnelingFeatureGet));
    if (ui->tunnelServiceType->currentIndex() == 1)
        type = ServType(quint16(ServType::TunnelingFeatureSet));

    using FeatureType = QKnx::InterfaceFeature;
    FeatureType featureType = FeatureType(quint8(FeatureType::SupportedEmiType)
        + ui->featureIdentifier->currentIndex());

    QKnxByteArray bytes = QKnxByteArray::fromHex(ui->featureValue->text().toUtf8());
    auto text = ui->featureValue->text();
    if (text.isEmpty() || !validFeature(type, featureType, bytes)
        || ((text.size() % 2) != 0)) {
        ui->textOuputTunneling->append("Status: Invalid value entered");
        ui->tunnelingSend->setEnabled(false);
        return;
    }
    ui->textOuputTunneling->append("Status: Valid value entered, click send.");
    ui->tunnelingSend->setEnabled(m_tunnel.state() == QKnxNetIpEndpointConnection::State::Connected);
}

void TunnelingFeatures::updateSecureConfigCombo()
{
    ui->secureSessionCb->clear();

    ui->secureSessionCheckBox->setEnabled(!m_configs.isEmpty());
    ui->secureSessionCheckBox->setChecked(m_protocol == QKnxNetIp::HostProtocol::TCP_IPv4);

    for (int i = 0; i < m_configs.size(); ++i) {
        const auto &config = m_configs[i];
        if (m_server.individualAddress() != config.host())
            continue;

        const auto ia = config.individualAddress();
        ui->secureSessionCb->addItem(tr("User ID: %1 (Individual Address: %2)")
            .arg(config.userId())
            .arg(ia.isValid() ? ia.toString() : tr("No specific address")), i);
    }
}
