/****************************************************************************
**
** Copyright (C) 2017 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtKnx module.
**
** $QT_BEGIN_LICENSE:BSD$
** 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.
**
** BSD License Usage
** Alternatively, you may use this file under the terms of the BSD license
** as follows:
**
** "Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
**   * Redistributions of source code must retain the above copyright
**     notice, this list of conditions and the following disclaimer.
**   * Redistributions in binary form must reproduce the above copyright
**     notice, this list of conditions and the following disclaimer in
**     the documentation and/or other materials provided with the
**     distribution.
**   * Neither the name of The Qt Company Ltd nor the names of its
**     contributors may be used to endorse or promote products derived
**     from this software without specific prior written permission.
**
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "localdevicemanagement.h"
#include "ui_localdevicemanagement.h"

#include <QKnxByteArray>
#include <QKnxDeviceManagementFrameBuilder>
#include <QKnxInterfaceObjectPropertyDataType>
#include <QKnxNetIpSecureConfiguration>
#include <QMetaEnum>
#include <QMetaType>
#include <QTreeWidget>

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

    setupMessageCodeComboBox();

    const auto setTreeWidget = [](QComboBox *comboBox) {
        auto treeWidget = new QTreeWidget;
        treeWidget->setHeaderHidden(true);
        treeWidget->setUniformRowHeights(true);

        comboBox->setModel(treeWidget->model());
        comboBox->setView(treeWidget);
    };
    setTreeWidget(ui->property);
    setTreeWidget(ui->objectType);

    setupComboBox(ui->property, QKnxInterfaceObjectProperty::staticMetaObject);
    setupComboBox(ui->objectType, QKnxInterfaceObjectType::staticMetaObject);

    connect(ui->connectRequestDeviceManagement, &QPushButton::clicked, this, [&]() {
        m_management.setLocalPort(0);
        if (ui->secureSessionCheckBox->isChecked()) {
            auto config = m_configs.value(ui->secureSessionCb->currentIndex());
            config.setKeepSecureSessionAlive(true);
            m_management.setSecureConfiguration(config);
            m_management.connectToHostEncrypted(m_server.controlEndpointAddress(),
                m_server.controlEndpointPort());
        } else {
            m_management.connectToHost(m_server.controlEndpointAddress(),
                m_server.controlEndpointPort(), m_proto);
        }
    });

    connect(&m_management, &QKnxNetIpDeviceManagement::connected, this, [&] {
        ui->deviceManagementSendRequest->setEnabled(true);
        ui->connectRequestDeviceManagement->setEnabled(false);
        ui->disconnectRequestDeviceManagement->setEnabled(true);
        ui->textOuputDeviceManagement->append(tr("Successfully connected to: %1 on port: %2")
            .arg(m_server.controlEndpointAddress().toString()).arg(m_server.controlEndpointPort()));

        m_management.sendFrame(QKnxDeviceManagementFrame::propertyReadBuilder()
            .setObjectType(QKnxInterfaceObjectType::System::Device)
            .setObjectInstance(1)
            .setProperty(QKnxInterfaceObjectProperty::Device::IoList)
            .setNumberOfElements(1)
            .setStartIndex(0).createRequest());
    });

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

    connect(&m_management, &QKnxNetIpDeviceManagement::disconnected, this, [&] {
        if (!ui)
            return;
        m_awaitIoListResponse = true;
        ui->deviceManagementSendRequest->setEnabled(false);
        ui->connectRequestDeviceManagement->setEnabled(true);
        ui->disconnectRequestDeviceManagement->setEnabled(false);
        setupComboBox(ui->objectType, QKnxInterfaceObjectType::staticMetaObject);
        ui->textOuputDeviceManagement->append(tr("Successfully disconnected from: %1 on port: %2\n")
            .arg(m_server.controlEndpointAddress().toString()).arg(m_server.controlEndpointPort()));
    });

    connect(ui->deviceManagementSendRequest, &QPushButton::clicked, this, [&]() {
        ui->textOuputDeviceManagement->append(tr("Send device management frame with cEMI payload: ")
            + ui->cemiFrame->text());
        auto data = QKnxByteArray::fromHex(ui->cemiFrame->text().toUtf8());
        if (ui->cemiData->isEnabled())
            data.append(QKnxByteArray::fromHex(ui->cemiData->text().toUtf8()));
        m_management.sendFrame(QKnxDeviceManagementFrame::fromBytes(data, 0,
            data.size()));
    });

    connect(&m_management, &QKnxNetIpDeviceManagement::frameReceived,
        this, [&](QKnxDeviceManagementFrame frame) {
        ui->textOuputDeviceManagement->append(tr("Received device management frame with cEMI "
            "payload: " + frame.bytes().toHex().toByteArray()));

        if (m_awaitIoListResponse)
            handleIoListResponse(frame);
    });

    connect(&m_management, &QKnxNetIpDeviceManagement::errorOccurred, this,
        [&] (QKnxNetIpEndpointConnection::Error, QString errorString) {
            ui->textOuputDeviceManagement->append(errorString);
    });

    ui->cemiData->setValidator(new QRegExpValidator(QRegExp("[0-9a-fA-F]+")));
    ui->cemiFrame->setValidator(new QRegExpValidator(QRegExp("[0-9a-fA-F]+")));
}

LocalDeviceManagement::~LocalDeviceManagement()
{
    delete ui;
    ui = nullptr;
}

void LocalDeviceManagement::setNatAware(bool isNatAware)
{
    m_management.setNatAware(isNatAware);
}

void LocalDeviceManagement::setLocalAddress(const QHostAddress &address)
{
    m_management.disconnectFromHost();
    m_management.setLocalAddress(address);
}

void LocalDeviceManagement::setKnxNetIpServer(const QKnxNetIpServerInfo &server)
{
    m_management.disconnectFromHost();
    m_server = server;

    if (m_management.state() == QKnxNetIpEndpointConnection::State::Disconnected) {
        ui->connectRequestDeviceManagement->setEnabled(true);
        ui->disconnectRequestDeviceManagement->setEnabled(false);
    }

    updateSecureConfigCombo();
    ui->deviceManagementSendRequest->setEnabled(false);
}

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

void LocalDeviceManagement::clearLogging()
{
    ui->textOuputDeviceManagement->clear();
}

void LocalDeviceManagement::on_mc_currentIndexChanged(int index)
{
    if (ui->cemiFrame->text().size() > 2)
        m_fullCemiFrame = ui->cemiFrame->text();

    int maxLength = 10;
    bool dataEnabled = true;
    auto cemiFrame = m_fullCemiFrame;

    quint8 data = ui->mc->itemData(index).toUInt();
    switch (QKnxDeviceManagementFrame::MessageCode(data)) {
    case QKnxDeviceManagementFrame::MessageCode::PropertyReadRequest:
        dataEnabled = false;
    case QKnxDeviceManagementFrame::MessageCode::PropertyWriteRequest:
        maxLength = 14;
    case QKnxDeviceManagementFrame::MessageCode::FunctionPropertyCommandRequest:
    case QKnxDeviceManagementFrame::MessageCode::FunctionPropertyStateReadRequest:
        if (cemiFrame.size() < maxLength) {
            cemiFrame.append(QStringLiteral("%1").arg(ui->noe->value(), 1, 16, QLatin1Char('0')));
            cemiFrame.append(QStringLiteral("%1").arg(ui->startIndex->value(), 3, 16, QLatin1Char('0')));
        }
        cemiFrame = QStringLiteral("%1").arg(data, 2, 16, QLatin1Char('0')) + cemiFrame.mid(2);
        break;
    case QKnxDeviceManagementFrame::MessageCode::ResetRequest:
        maxLength = 2;
        dataEnabled = false;
        cemiFrame = QStringLiteral("%1").arg(data, 2, 16, QLatin1Char('0'));
        break;
    default:
        break;
    }
    ui->cemiFrame->setMaxLength(maxLength);
    ui->cemiFrame->setText(cemiFrame);
    ui->cemiData->setEnabled(dataEnabled);
}

void LocalDeviceManagement::on_objectType_currentTextChanged(const QString &type)
{
    bool keyExists = false;
    quint16 value = keyToValue(QKnxInterfaceObjectType::staticMetaObject, type, &keyExists);
    if (keyExists) {
        auto text = ui->cemiFrame->text();
        ui->cemiFrame->setText(text.left(2) + QStringLiteral("%1").arg(value, 4, 16,
            QLatin1Char('0')) + text.mid(6));
        updatePropertyTypeCombobox(type);
    }
}

void LocalDeviceManagement::on_property_currentTextChanged(const QString &property)
{
    bool keyExists = false;
    quint8 value = keyToValue(QKnxInterfaceObjectProperty::staticMetaObject, property, &keyExists);
    if (keyExists) {
        auto text = ui->cemiFrame->text();
        ui->cemiFrame->setText(text.left(8) + QStringLiteral("%1").arg(value, 2, 16,
            QLatin1Char('0')) + text.mid(10));
    }
}

void LocalDeviceManagement::on_noe_valueChanged(int value)
{
    auto text = ui->cemiFrame->text();
    ui->cemiFrame->setText(text.left(10) + QStringLiteral("%1").arg(value, 1,
        16, QLatin1Char('0')) + text.mid(11));
}

void LocalDeviceManagement::on_startIndex_valueChanged(int value)
{
    auto text = ui->cemiFrame->text();
    ui->cemiFrame->setText(text.left(11) + QStringLiteral("%1").arg(value, 3, 16,
        QLatin1Char('0')));
}

void LocalDeviceManagement::on_objectInstance_valueChanged(int value)
{
    auto text = ui->cemiFrame->text();
    ui->cemiFrame->setText(text.left(6) + QStringLiteral("%1").arg(value, 2, 16,
        QLatin1Char('0')) + text.mid(8));
}

void LocalDeviceManagement::on_manualInput_clicked(bool checked)
{
    ui->cemiData->setEnabled(checked);
    ui->cemiFrame->setReadOnly(!checked);
    ui->cemiFrame->setMaxLength(SHRT_MAX);
    ui->cemiFrame->setFocus();

    if (checked) {
        if (ui->mc->currentIndex() != ui->mc->count() -1)
            m_fullCemiFrame = ui->cemiFrame->text();
    } else {
        ui->cemiFrame->setText(m_fullCemiFrame);
        on_mc_currentIndexChanged(ui->mc->currentIndex());
    }
}

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

void LocalDeviceManagement::setupMessageCodeComboBox()
{
    ui->mc->addItem("M_PropRead.req",
        quint8(QKnxDeviceManagementFrame::MessageCode::PropertyReadRequest));
    ui->mc->addItem("M_PropWrite.req",
        quint8(QKnxDeviceManagementFrame::MessageCode::PropertyWriteRequest));
    ui->mc->addItem("M_FuncPropCommand.req",
        quint8(QKnxDeviceManagementFrame::MessageCode::FunctionPropertyCommandRequest));
    ui->mc->addItem("M_FuncPropStateRead.req",
        quint8(QKnxDeviceManagementFrame::MessageCode::FunctionPropertyStateReadRequest));
    ui->mc->addItem("M_Reset.req",
        quint8(QKnxDeviceManagementFrame::MessageCode::ResetRequest));
}

void LocalDeviceManagement::updatePropertyTypeCombobox(const QString &type)
{
    int index = QKnxInterfaceObjectProperty::staticMetaObject.indexOfEnumerator("General");
    if (index >= 0) {
        auto treeView = qobject_cast<QTreeWidget *>(ui->property->view());
        if (!treeView)
            return;

        auto topLevelItem = treeView->takeTopLevelItem(0);
        ui->property->clear();
        treeView->addTopLevelItem(topLevelItem);

        auto index = QKnxInterfaceObjectProperty::staticMetaObject.indexOfEnumerator(type.toLatin1());
        if (index >= 0) {
            auto typeEnum = QKnxInterfaceObjectProperty::staticMetaObject.enumerator(index);
            topLevelItem = new QTreeWidgetItem(treeView, { typeEnum.name() });
            for (auto a = 0; a < typeEnum.keyCount(); ++a)
                topLevelItem->addChild(new QTreeWidgetItem({ typeEnum.key(a) }));
            topLevelItem->setFlags(topLevelItem->flags() &~Qt::ItemIsSelectable);
        }
        treeView->expandItem(topLevelItem);
        selectFirstSubitem(treeView, topLevelItem, ui->property);
    }
}

void LocalDeviceManagement::handleIoListResponse(const QKnxDeviceManagementFrame &frame)
{
    if (frame.objectType() != QKnxInterfaceObjectType::System::Device
        || frame.property() != QKnxInterfaceObjectProperty::Device::IoList) {
            return;
    }

    if (frame.isNegativeConfirmation()) {
        auto metaEnum = QMetaEnum::fromType<QKnxNetIp::CemiServer::Error>();
        ui->textOuputDeviceManagement->append(tr("Received negative confirmation. Error code: %1")
            .arg(QString::fromLatin1(metaEnum.valueToKey(int(frame.error())))));
        m_awaitIoListResponse = false;
        return;
    }

    auto dataTypes = QKnxInterfaceObjectPropertyDataType::fromProperty(QKnxInterfaceObjectProperty
        ::Device::IoList);
    if (!dataTypes.value(0).isValid())
        return;

    auto data = frame.data().toByteArray();
    quint8 expectedDataSize = dataTypes[0].size();
    if (frame.startIndex() == 0) {
        if (data.size() == expectedDataSize) {
            m_management.sendFrame(QKnxDeviceManagementFrame::propertyReadBuilder()
                .setObjectType(QKnxInterfaceObjectType::System::Device)
                .setObjectInstance(1)
                .setProperty(QKnxInterfaceObjectProperty::Device::IoList)
                .setNumberOfElements(data.toHex().toUShort(nullptr, 16))
                .setStartIndex(1)
                .createRequest()
            );
        }
    } else {
        if ((data.size() % expectedDataSize) == 0) {
            QSet<int> values;
            for (int i = 0; i < data.size(); i += expectedDataSize)
                values.insert(data.mid(i, expectedDataSize).toHex().toUShort(nullptr, 16));

            m_awaitIoListResponse = false;
            setupComboBox(ui->objectType, QKnxInterfaceObjectType::staticMetaObject, values);
        }
    }
}

int LocalDeviceManagement::keyToValue(const QMetaObject &object, const QString &key, bool *ok)
{
    auto enumCount = object.enumeratorCount();
    for (auto i = 0; i < enumCount; ++i) {
        int value = object.enumerator(i).keyToValue(key.toLatin1(), ok);
        if (value != -1 && *ok)
            return value;
    }
    return -1;
}

void LocalDeviceManagement::setupComboBox(QComboBox *comboBox, const QMetaObject &object,
    const QSet<int> &values)
{
    comboBox->clear();

    auto treeWidget = qobject_cast<QTreeWidget *> (comboBox->view());
    if (!treeWidget)
        return;

    QTreeWidgetItem *rootItem = nullptr;
    auto enumCount = object.enumeratorCount();
    for (auto i = 0; i < enumCount; ++i) {
        auto typeEnum = object.enumerator(i);
        auto topLevelItem = new QTreeWidgetItem(treeWidget, { typeEnum.name() });
        if (!rootItem)
            rootItem = topLevelItem;
        for (auto a = 0; a < typeEnum.keyCount(); ++a) {
            auto subItem = new QTreeWidgetItem(topLevelItem, { typeEnum.key(a) });
            subItem->setDisabled(values.isEmpty() ? false : !values.contains(typeEnum.value(a)));
        }
        topLevelItem->setFlags(topLevelItem->flags() &~ Qt::ItemIsSelectable);
    }
    treeWidget->expandItem(rootItem);
    selectFirstSubitem(treeWidget, rootItem, comboBox);
}

void LocalDeviceManagement::selectFirstSubitem(QTreeWidget *treeWidget, QTreeWidgetItem *rootItem,
    QComboBox *comboBox)
{
    treeWidget->setCurrentItem(rootItem, 0);
    comboBox->setRootModelIndex(treeWidget->currentIndex());
    comboBox->setCurrentIndex(0);
    treeWidget->setCurrentItem(treeWidget->invisibleRootItem(), 0);
    comboBox->setRootModelIndex(treeWidget->currentIndex());
}

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

    ui->secureSessionCheckBox->setEnabled(!m_configs.isEmpty());
    ui->secureSessionCheckBox->setChecked(m_proto == 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;

        ui->secureSessionCb->addItem(tr("User ID: %1 (Individual Address: %2)").arg(config.userId())
            .arg(config.individualAddress().toString()), i);
    }
}
