/****************************************************************************
**
** 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 "mainwindow.h"
#include "ui_mainwindow.h"

#include <QElapsedTimer>
#include <QFileDialog>
#include <QInputDialog>
#include <QNetworkInterface>
#include <QStandardItem>
#include <QStandardPaths>

Ui::MainWindow *MainWindow::s_ui { nullptr };

static QString familyToString(QKnxNetIp::ServiceFamily id)
{
    switch (id) {
    case QKnxNetIp::ServiceFamily::Core:
        return MainWindow::tr("Core");
    case QKnxNetIp::ServiceFamily::DeviceManagement:
        return MainWindow::tr("Device Management");
    case QKnxNetIp::ServiceFamily::IpTunneling:
        return MainWindow::tr("Tunnel");
    case QKnxNetIp::ServiceFamily::IpRouting:
        return MainWindow::tr("Routing");
    case QKnxNetIp::ServiceFamily::RemoteLogging:
        return MainWindow::tr("Remote Logging");
    case QKnxNetIp::ServiceFamily::RemoteConfigAndDiagnosis:
        return MainWindow::tr("Remote Configuration");
    case QKnxNetIp::ServiceFamily::ObjectServer:
        return MainWindow::tr("Object Server");
    case QKnxNetIp::ServiceFamily::Security:
        return MainWindow::tr("Security");
    default:
        break;
    }
    return MainWindow::tr("Unknown");
}

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

    ui->tunneling->setEnabled(false);
    ui->deviceManagement->setEnabled(false);
    ui->tunnelingFeatures->setEnabled(false);

    ui->serverBox->addItem(tr("Press Scan button to discover KNX server(s)"));

    m_discoveryAgent.setTimeout(5000);
    connect(&m_discoveryAgent, &QKnxNetIpServerDiscoveryAgent::started, this, [&] {
        if (!ui)
            return;

        ui->scanButton->setEnabled(false);
        ui->checkboxNat->setEnabled(false);

        ui->serverDescription->clear();

        ui->serverBox->clear();
        ui->serverBox->setEnabled(false);

        auto firstItem = new QStandardItem(tr("Select a KNX server(s)"));
        qobject_cast<QStandardItemModel*>(ui->serverBox->model())->appendRow(firstItem);
        firstItem->setSelectable(false);
    });
    connect(&m_discoveryAgent, &QKnxNetIpServerDiscoveryAgent::finished, this, [&] {
        if (!ui)
            return;

        ui->scanButton->setEnabled(true);
        ui->checkboxNat->setEnabled(true);

        if (ui->serverBox->count() <= 1)
            ui->serverBox->setItemText(0, tr("Press Scan button to discover KNX server(s)"));
        else if (ui->serverBox->count() == 2)
            ui->serverBox->setCurrentIndex(1);

        ui->serverBox->setEnabled(true);
        newServerSelected(ui->serverBox->currentIndex());
    });
    connect(&m_discoveryAgent, &QKnxNetIpServerDiscoveryAgent::deviceDiscovered, this,
        &MainWindow::showServerAndServices);

    fillLocalIpBox();

    connect(ui->scanButton, &QPushButton::clicked, &m_discoveryAgent,
        QOverload<>::of(&QKnxNetIpServerDiscoveryAgent::start));

    connect(ui->checkboxNat, &QCheckBox::toggled, this, [&](bool checked) {
        if (!ui)
            return;

        ui->tunneling->setNatAware(checked);
        m_discoveryAgent.setNatAware(checked);
        ui->deviceManagement->setNatAware(checked);
        ui->tunnelingFeatures->setNatAware(checked);
    });

    connect(ui->localIpBox, QOverload<int>::of(&QComboBox::activated), this,
        &MainWindow::newIPAddressSelected);
    connect(ui->serverBox, QOverload<int>::of(&QComboBox::activated), this,
        &MainWindow::newServerSelected);

    connect(ui->actionExit, &QAction::triggered, this, &QApplication::quit);
    connect(ui->actionClear_Output, &QAction::triggered, ui->outputEdit, &QTextEdit::clear);

    connect(ui->actionClear_All, &QAction::triggered, ui->deviceManagement,
        &LocalDeviceManagement::clearLogging);
    connect(ui->actionClear_All, &QAction::triggered, ui->outputEdit, &QTextEdit::clear);
    connect(ui->actionClear_All, &QAction::triggered, ui->tunneling, &Tunneling::clearLogging);
    if (ui->localIpBox->count() == 2) {
        ui->localIpBox->setCurrentIndex(1);
        newIPAddressSelected(ui->localIpBox->currentIndex());
    }

    s_ui = ui;
    qInstallMessageHandler(MainWindow::messageHandler);
}

MainWindow::~MainWindow()
{
    s_ui = nullptr;
    delete ui;
    ui = nullptr;
}

void MainWindow::newServerSelected(int serverBoxIndex)
{
    if (serverBoxIndex < 1)
        return;

    auto info = ui->serverBox->itemData(serverBoxIndex).value<QKnxNetIpServerInfo>();
    bool version2Supported = false;
    ui->serverDescription->setText(tr("<html><head><style> th { text-align: left; } td.padding { "
        "padding-left: 10px; } </style></head> <body>"
        "   <table style=\"width:100%\">"
        "       <th>Device Information</th>"
        "           <tr><td class=\"padding\">Individual address: %1</td></tr>"
        "           <tr><td class=\"padding\">Server control endpoint: %2:%3</td></tr>"
        "           <tr></tr>"
        "       <tr><th>Supported services:</th></tr>"
        "           %4"
        "       </table>"
        "   </table>"
        "</body></html>")
        .arg(info.individualAddress().toString())
        .arg(info.controlEndpointAddress().toString()).arg(info.controlEndpointPort())
        .arg([&info, &version2Supported]() -> QString {
            QString value;
            const auto services = info.supportedServices();
            for (const auto &service : services) {
                value.append(tr("<tr><td class=\"padding\">%1</td></th>")
                    .arg(tr("KNXnet/IP %1, Version: %2").arg(familyToString(service.ServiceFamily))
                        .arg(service.ServiceFamilyVersion)));
                if (service.ServiceFamilyVersion >= 2)
                     version2Supported = true;
            }
            return value;
        }())
    );

    const auto &hpai = info.endpoint();
    const QKnxNetIpHpaiProxy endpoint(hpai);
    if (endpoint.isValid() && m_server != info) {
        m_server = info;

        ui->tunneling->setEnabled(true);
        ui->tunneling->setKnxNetIpServer(m_server);

        ui->deviceManagement->setEnabled(true);
        ui->deviceManagement->setKnxNetIpServer(m_server);

        ui->tunnelingFeatures->setEnabled(true);
        ui->tunnelingFeatures->setKnxNetIpServer(m_server);
    }
    ui->radioButtonTCP->setEnabled(version2Supported);
}

void MainWindow::newIPAddressSelected(int localIpBoxIndex)
{
    if (localIpBoxIndex < 1)
        return;

    auto newAddress = QHostAddress(ui->localIpBox->itemData(localIpBoxIndex).toString());
    if (newAddress.isNull()) {
        ui->outputEdit->append(tr("Selected IP address is not valid"));
        return;
    }

    if (m_discoveryAgent.localAddress() == newAddress)
        return;

    ui->scanButton->setEnabled(true);
    ui->outputEdit->append(tr("Selected IP address: ") + newAddress.toString());

    m_discoveryAgent.stop();
    m_discoveryAgent.setLocalAddress(newAddress);

    ui->tunneling->setLocalAddress(newAddress);
    ui->deviceManagement->setLocalAddress(newAddress);
    ui->tunnelingFeatures->setLocalAddress(newAddress);
}

void MainWindow::showServerAndServices(const QKnxNetIpServerInfo &info)
{
    ui->outputEdit->append(tr("Server Endpoint found"));
    ui->outputEdit->append(tr("Server's Multicast Address"));
    ui->outputEdit->append(info.controlEndpointAddress().toString());
    ui->outputEdit->append(tr("Server's Port"));
    ui->outputEdit->append(QString::number(info.controlEndpointPort()));
    ui->outputEdit->append(tr("The following services are supported:"));
    const auto services = info.supportedServices();
    for (const auto service : services) {
        ui->outputEdit->append(tr("    KNXnet/IP %1, Version: %2")
            .arg(familyToString(service.ServiceFamily)).arg(service.ServiceFamilyVersion));
    }

    ui->serverBox->addItem(tr("%1 (%2:%3)").arg(info.deviceName(), info.controlEndpointAddress()
        .toString()).arg(info.controlEndpointPort()), QVariant::fromValue(info));
}

void MainWindow::on_radioButtonTCP_toggled(bool checked)
{
    ui->tunneling->setTcpEnable(checked);
    ui->deviceManagement->setTcpEnable(checked);
    ui->tunnelingFeatures->setTcpEnable(checked);
}

void MainWindow::on_actionEtsKeyringImport_triggered()
{
    auto fileName = QFileDialog::getOpenFileName(this, tr("Import keyring file"),
        QStandardPaths::standardLocations(QStandardPaths::DesktopLocation).value(0),
        tr("KNX keyring file (*.knxkeys)"));

    if (fileName.isEmpty())
        return;

    bool ok;
    auto password = QInputDialog::getText(this, tr("Import keyring file"),
        tr("Keyring file password:"), QLineEdit::Normal, {}, &ok);
    if (!ok || password.isEmpty())
        return;

    auto mgmtConfigs = QKnxNetIpSecureConfiguration::fromKeyring(QKnxNetIpSecureConfiguration
        ::Type::DeviceManagement, fileName, password.toUtf8(), true);
    ui->deviceManagement->onKeyringChanged(mgmtConfigs);

    for (auto &config : mgmtConfigs)
        config.setIndividualAddress({});

    auto tunnelConfigs = QKnxNetIpSecureConfiguration::fromKeyring(QKnxNetIpSecureConfiguration
        ::Type::Tunneling, fileName, password.toUtf8(), true);
    ui->tunneling->onKeyringChanged(mgmtConfigs + tunnelConfigs);
    ui->tunnelingFeatures->onKeyringChanged(mgmtConfigs + tunnelConfigs);
}

void MainWindow::fillLocalIpBox()
{
    auto firstItem = new QStandardItem(tr("Interface: IP address --Select One--"));
    qobject_cast<QStandardItemModel*>(ui->localIpBox->model())->appendRow(firstItem);
    firstItem->setSelectable(false);

    auto networkInterfaces = QNetworkInterface::allInterfaces();
    for (int i = 0; i < networkInterfaces.size(); i++) {
        auto addressList = networkInterfaces[i].addressEntries();
        for (int j = 0; j < addressList.size(); j++) {
            auto address = addressList[j].ip();
            if (address.isLoopback() || address.toIPv4Address() == 0)
                continue;
            auto ipAddress = address.toString();
            ui->localIpBox->addItem(networkInterfaces[i].name() + ": " + ipAddress, ipAddress);
        }
    }
}

void MainWindow::messageHandler(QtMsgType type, const QMessageLogContext &ctx, const QString &msg)
{
    class Uptime : public QElapsedTimer
    {
    public:
        Uptime() { start(); }
        quint64 lastEvent { 0 };
    };
    static Uptime uptime;

    auto msec = uptime.elapsed();
    if (s_ui) {
        s_ui->outputEdit->append(QStringLiteral("[elapsed: %1 msec, diff: %2 msec]\n    %3")
            .arg(msec) .arg(msec - uptime.lastEvent).arg(msg));
    }
    uptime.lastEvent = msec;

    if (type == QtFatalMsg) {
        auto oldMsgHandler = qInstallMessageHandler(0);
        qt_message_output(type, ctx, msg);
        qInstallMessageHandler(oldMsgHandler);
    }
}
