///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2022-2023 Jon Beniston, M7RCE <jon@beniston.com>                //
//                                                                               //
// This program is free software; you can redistribute it and/or modify          //
// it under the terms of the GNU General Public License as published by          //
// the Free Software Foundation as version 3 of the License, or                  //
// (at your option) any later version.                                           //
//                                                                               //
// This program 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 General Public License V3 for more details.                               //
//                                                                               //
// You should have received a copy of the GNU General Public License             //
// along with this program. If not, see <http://www.gnu.org/licenses/>.          //
///////////////////////////////////////////////////////////////////////////////////

#include "feature/featureuiset.h"
#include "gui/basicfeaturesettingsdialog.h"
#include "gui/flowlayout.h"
#include "gui/scidoublespinbox.h"
#include "gui/dialogpositioner.h"

#include "ui_remotecontrolgui.h"
#include "remotecontrol.h"
#include "remotecontrolgui.h"
#include "remotecontrolsettingsdialog.h"

RemoteControlGUI* RemoteControlGUI::create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature)
{
    RemoteControlGUI* gui = new RemoteControlGUI(pluginAPI, featureUISet, feature);
    return gui;
}

void RemoteControlGUI::destroy()
{
    delete this;
}

void RemoteControlGUI::resetToDefaults()
{
    m_settings.resetToDefaults();
    displaySettings();
    applySettings(true);
}

QByteArray RemoteControlGUI::serialize() const
{
    return m_settings.serialize();
}

bool RemoteControlGUI::deserialize(const QByteArray& data)
{
    if (m_settings.deserialize(data))
    {
        m_feature->setWorkspaceIndex(m_settings.m_workspaceIndex);
        displaySettings();
        applySettings(true);
        on_update_clicked();

        return true;
    }
    else
    {
        resetToDefaults();
        return false;
    }
}

bool RemoteControlGUI::handleMessage(const Message& message)
{
    if (RemoteControl::MsgConfigureRemoteControl::match(message))
    {
        qDebug("RemoteControlGUI::handleMessage: RemoteControl::MsgConfigureRemoteControl");
        const RemoteControl::MsgConfigureRemoteControl& cfg = (RemoteControl::MsgConfigureRemoteControl&) message;
        m_settings = cfg.getSettings();
        blockApplySettings(true);
        displaySettings();
        blockApplySettings(false);

        return true;
    }
    else if (RemoteControl::MsgDeviceStatus::match(message))
    {
        const RemoteControl::MsgDeviceStatus& msg = (RemoteControl::MsgDeviceStatus&) message;
        deviceUpdated(msg.getProtocol(), msg.getDeviceId(), msg.getStatus());
        return true;
    }
    else if (RemoteControl::MsgDeviceError::match(message))
    {
        const RemoteControl::MsgDeviceError& msg = (RemoteControl::MsgDeviceError&) message;
        QMessageBox::critical(this,  "Remote Control Error", msg.getErrorMessage());
        return true;
    }
    else if (RemoteControl::MsgDeviceUnavailable::match(message))
    {
        const RemoteControl::MsgDeviceUnavailable& msg = (RemoteControl::MsgDeviceUnavailable&) message;
        deviceUnavailable(msg.getProtocol(), msg.getDeviceId());
        return true;
    }

    return false;
}

void RemoteControlGUI::handleInputMessages()
{
    Message* message;

    while ((message = getInputMessageQueue()->pop()))
    {
        if (handleMessage(*message)) {
            delete message;
        }
    }
}

void RemoteControlGUI::onWidgetRolled(QWidget* widget, bool rollDown)
{
    (void) widget;
    (void) rollDown;

    getRollupContents()->saveState(m_rollupState);
    applySettings();
}

RemoteControlGUI::RemoteControlGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent) :
    FeatureGUI(parent),
    ui(new Ui::RemoteControlGUI),
    m_pluginAPI(pluginAPI),
    m_featureUISet(featureUISet),
    m_doApplySettings(true)
{
    m_feature = feature;
    setAttribute(Qt::WA_DeleteOnClose, true);
    m_helpURL = "plugins/feature/remotecontrol/readme.md";
    RollupContents *rollupContents = getRollupContents();
	ui->setupUi(rollupContents);
    rollupContents->arrangeRollups();
	connect(rollupContents, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool)));

    ui->startStop->setStyleSheet("QToolButton { background-color : blue; }"
                                 "QToolButton:checked { background-color : green; }"
                                 "QToolButton:disabled { background-color : gray; }");

    m_startStopIcon.addFile(":/play.png", QSize(16, 16), QIcon::Normal, QIcon::Off);
    m_startStopIcon.addFile(":/stop.png", QSize(16, 16), QIcon::Normal, QIcon::On);

    m_remoteControl = reinterpret_cast<RemoteControl*>(feature);
    m_remoteControl->setMessageQueueToGUI(&m_inputMessageQueue);

    m_settings.setRollupState(&m_rollupState);

    connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &)));
    connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages()));

    displaySettings();
    applySettings(true);
    makeUIConnections();
    m_resizer.enableChildMouseTracking();
}

RemoteControlGUI::~RemoteControlGUI()
{
    qDeleteAll(m_deviceGUIs);
    m_deviceGUIs.clear();
    delete ui;
}

void RemoteControlGUI::setWorkspaceIndex(int index)
{
    m_settings.m_workspaceIndex = index;
    m_feature->setWorkspaceIndex(index);
}

void RemoteControlGUI::blockApplySettings(bool block)
{
    m_doApplySettings = !block;
}

void RemoteControlGUI::displaySettings()
{
    setTitleColor(m_settings.m_rgbColor);
    setWindowTitle(m_settings.m_title);
    setTitle(m_settings.m_title);
    createGUI();
    blockApplySettings(true);
    getRollupContents()->restoreState(m_rollupState);
    blockApplySettings(false);
    getRollupContents()->arrangeRollups();
}

void RemoteControlGUI::onMenuDialogCalled(const QPoint &p)
{
    if (m_contextMenuType == ContextMenuType::ContextMenuChannelSettings)
    {
        BasicFeatureSettingsDialog dialog(this);
        dialog.setTitle(m_settings.m_title);
        dialog.setUseReverseAPI(m_settings.m_useReverseAPI);
        dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress);
        dialog.setReverseAPIPort(m_settings.m_reverseAPIPort);
        dialog.setReverseAPIFeatureSetIndex(m_settings.m_reverseAPIFeatureSetIndex);
        dialog.setReverseAPIFeatureIndex(m_settings.m_reverseAPIFeatureIndex);
        dialog.setDefaultTitle(m_displayedName);

        dialog.move(p);
        new DialogPositioner(&dialog, false);
        dialog.exec();

        m_settings.m_title = dialog.getTitle();
        m_settings.m_useReverseAPI = dialog.useReverseAPI();
        m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress();
        m_settings.m_reverseAPIPort = dialog.getReverseAPIPort();
        m_settings.m_reverseAPIFeatureSetIndex = dialog.getReverseAPIFeatureSetIndex();
        m_settings.m_reverseAPIFeatureIndex = dialog.getReverseAPIFeatureIndex();

        setTitle(m_settings.m_title);
        setTitleColor(m_settings.m_rgbColor);

        applySettings();
    }

    resetContextMenuType();
}

void RemoteControlGUI::createControls(RemoteControlDeviceGUI *gui, QBoxLayout *vBox, FlowLayout *flow, int &widgetCnt)
{
    // Create buttons to control the device
    QGridLayout *controlsGrid = nullptr;

    if (gui->m_rcDevice->m_verticalControls)
    {
        controlsGrid = new QGridLayout();
        vBox->addLayout(controlsGrid);
    }
    else if (!flow)
    {
        flow = new FlowLayout(2, 6, 6);
        vBox->addItem(flow);
    }

    int row = 0;
    for (auto const &control : gui->m_rcDevice->m_controls)
    {
        if (!gui->m_rcDevice->m_verticalControls && (widgetCnt > 0))
        {
            QFrame *line = new QFrame();
            line->setFrameShape(QFrame::VLine);
            line->setFrameShadow(QFrame::Sunken);
            flow->addWidget(line);
        }

        DeviceDiscoverer::ControlInfo *info = gui->m_rcDevice->m_info.getControl(control.m_id);
        if (!info)
        {
            qDebug() << "RemoteControlGUI::createControls: Info missing for " << control.m_id;
            continue;
        }

        if (!control.m_labelLeft.isEmpty())
        {
            QLabel *controlLabelLeft = new QLabel(control.m_labelLeft);
            if (gui->m_rcDevice->m_verticalControls)
            {
                controlsGrid->addWidget(controlLabelLeft, row, 0);
                controlsGrid->setColumnStretch(row, 0);
            }
            else
            {
                flow->addWidget(controlLabelLeft);
            }
        }

        QList<QWidget *> widgets;
        QWidget *widget = nullptr;

        switch (info->m_type)
        {
        case DeviceDiscoverer::BOOL:
            {
                ButtonSwitch *button = new ButtonSwitch();
                button->setToolTip("Start/stop " + info->m_name);
                button->setIcon(m_startStopIcon);
                button->setStyleSheet("QToolButton { background-color : blue; }"
                                      "QToolButton:checked { background-color : green; }"
                                      "QToolButton:disabled { background-color : gray; }");
                connect(button, &ButtonSwitch::toggled,
                    [=] (bool toggled)
                    {
                        RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol,
                                                                                                 gui->m_rcDevice->m_info.m_id,
                                                                                                 control.m_id,
                                                                                                 toggled);
                        m_remoteControl->getInputMessageQueue()->push(message);
                    }
                );
                widgets.append(button);
                widget = button;
            }
            break;

        case DeviceDiscoverer::INT:
            {
                QSpinBox *spinBox = new QSpinBox();

                spinBox->setToolTip("Set value for " + info->m_name);
                spinBox->setMinimum((int)info->m_min);
                spinBox->setMaximum((int)info->m_max);
                connect(spinBox, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged),
                    [=] (int value)
                    {
                        RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol,
                                                                                                 gui->m_rcDevice->m_info.m_id,
                                                                                                 control.m_id,
                                                                                                 value);
                        m_remoteControl->getInputMessageQueue()->push(message);
                    }
                );
                widgets.append(spinBox);
                widget = spinBox;
            }
            break;

        case DeviceDiscoverer::FLOAT:
            {
                switch (info->m_widgetType)
                {
                case DeviceDiscoverer::SPIN_BOX:
                    {
                        QDoubleSpinBox *spinBox = new SciDoubleSpinBox();

                        spinBox->setToolTip("Set value for " + info->m_name);
                        spinBox->setMinimum(info->m_min);
                        spinBox->setMaximum(info->m_max);
                        spinBox->setDecimals(info->m_precision);
                        connect(spinBox, static_cast<void (QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged),
                            [=] (double value)
                            {
                                RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol,
                                                                                                         gui->m_rcDevice->m_info.m_id,
                                                                                                         control.m_id,
                                                                                                         (float)value * info->m_scale);
                                m_remoteControl->getInputMessageQueue()->push(message);
                            }
                        );
                        widgets.append(spinBox);
                        widget = spinBox;
                    }
                    break;

                case DeviceDiscoverer::DIAL:
                    {
                        widget = new QWidget();
                        QHBoxLayout *layout = new QHBoxLayout();
                        layout->setContentsMargins(0, 0, 0, 0);
                        widget->setLayout(layout);

                        QDial *dial = new QDial();
                        dial->setMaximumSize(24, 24);
                        dial->setToolTip("Set value for " + info->m_name);
                        dial->setMinimum(info->m_min);
                        dial->setMaximum(info->m_max);

                        connect(dial, static_cast<void (QDial::*)(int)>(&QDial::valueChanged),
                            [=] (int value)
                            {
                                RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol,
                                                                                                         gui->m_rcDevice->m_info.m_id,
                                                                                                         control.m_id,
                                                                                                         ((float)value) * info->m_scale);
                                m_remoteControl->getInputMessageQueue()->push(message);
                            }
                        );
                        widgets.append(dial);
                        layout->addWidget(dial);

                        QLabel *label = new QLabel(QString::number(dial->value()));
                        label->setToolTip("Value for " + info->m_name);
                        widgets.append(label);
                        layout->addWidget(label);
                    }
                    break;

                case DeviceDiscoverer::SLIDER:
                    {
                        widget = new QWidget();
                        QHBoxLayout *layout = new QHBoxLayout();
                        layout->setContentsMargins(0, 0, 0, 0);
                        widget->setLayout(layout);

                        QSlider *slider = new QSlider(Qt::Horizontal);
                        slider->setToolTip("Set value for " + info->m_name);
                        slider->setMinimum(info->m_min);
                        slider->setMaximum(info->m_max);

                        connect(slider, static_cast<void (QSlider::*)(int)>(&QSlider::valueChanged),
                            [=] (int value)
                            {
                                RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol,
                                                                                                         gui->m_rcDevice->m_info.m_id,
                                                                                                         control.m_id,
                                                                                                         ((float)value) * info->m_scale);
                                m_remoteControl->getInputMessageQueue()->push(message);
                            }
                        );
                        widgets.append(slider);
                        layout->addWidget(slider);

                        QLabel *label = new QLabel(QString::number(slider->value()));
                        label->setToolTip("Value for " + info->m_name);
                        widgets.append(label);
                        layout->addWidget(label);
                    }
                    break;

                }
            }
            break;

        case DeviceDiscoverer::STRING:
            {
                QLineEdit *lineEdit = new QLineEdit();

                lineEdit->setToolTip("Set value for " + info->m_name);

                connect(lineEdit, &QLineEdit::editingFinished,
                    [=] ()
                    {
                        QString text = lineEdit->text();
                        RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol,
                                                                                                 gui->m_rcDevice->m_info.m_id,
                                                                                                 control.m_id,
                                                                                                 text);
                        m_remoteControl->getInputMessageQueue()->push(message);
                    }
                );
                widgets.append(lineEdit);
                widget = lineEdit;
            }
            break;

        case DeviceDiscoverer::LIST:
            {
                QComboBox *combo = new QComboBox();

                combo->setToolTip("Set value for " + info->m_name);
                combo->insertItems(0, info->m_values);
                connect(combo, &QComboBox::currentTextChanged,
                    [=] (const QString &text)
                    {
                        RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol,
                                                                                                 gui->m_rcDevice->m_info.m_id,
                                                                                                 control.m_id,
                                                                                                 text);
                        m_remoteControl->getInputMessageQueue()->push(message);
                    }
                );
                widgets.append(combo);
                widget = combo;
            }
            break;

        case DeviceDiscoverer::BUTTON:
            {
                QString label = info->m_name;
                if (info->m_values.size() > 0) {
                    label = info->m_values[0];
                }
                QToolButton *button = new QToolButton();
                button->setText(label);
                button->setToolTip("Trigger " + info->m_name);

                connect(button, &QToolButton::clicked,
                    [=] (bool checked)
                    {
                        (void) checked;
                        RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol,
                                                                                                 gui->m_rcDevice->m_info.m_id,
                                                                                                 control.m_id,
                                                                                                 1);
                        m_remoteControl->getInputMessageQueue()->push(message);
                    }
                );
                widgets.append(button);
                widget = button;
            }
            break;

        default:
            qDebug() << "RemoteControlGUI::createControls: Unexpected type for control.";
            break;

        }
        gui->m_controls.insert(control.m_id, widgets);
        if (gui->m_rcDevice->m_verticalControls) {
            controlsGrid->addWidget(widget, row, 1);
        } else {
            flow->addWidget(widget);
        }

        if (!control.m_labelRight.isEmpty())
        {
            QLabel *controlLabelRight = new QLabel(control.m_labelRight);
            if (gui->m_rcDevice->m_verticalControls)
            {
                controlsGrid->addWidget(controlLabelRight, row, 2);
                controlsGrid->setColumnStretch(row, 2);
            }
            else
            {
                flow->addWidget(controlLabelRight);
            }
        }

        widgetCnt++;
        row++;
    }
}

void RemoteControlGUI::createChart(RemoteControlDeviceGUI *gui, QVBoxLayout *vBox, const QString &id, const QString &units)
{
    if (gui->m_chart == nullptr)
    {
        // Create a chart to plot the sensor data
        gui->m_chart = new QChart();
        gui->m_chart->setTitle("");
        gui->m_chart->legend()->hide();
        gui->m_chart->layout()->setContentsMargins(0, 0, 0, 0);
        gui->m_chart->setMargins(QMargins(1, 1, 1, 1));
        gui->m_chart->setTheme(QChart::ChartThemeDark);
        QLineSeries *series = new QLineSeries();
        gui->m_series.insert(id, series);
        QLineSeries *onePointSeries = new QLineSeries();
        gui->m_onePointSeries.insert(id, onePointSeries);
        gui->m_chart->addSeries(series);
        QValueAxis *yAxis = new QValueAxis();
        QDateTimeAxis *xAxis = new QDateTimeAxis();
        xAxis->setFormat(QString("hh:mm:ss"));
        yAxis->setTitleText(units);
        gui->m_chart->addAxis(xAxis, Qt::AlignBottom);
        gui->m_chart->addAxis(yAxis, Qt::AlignLeft);
        series->attachAxis(xAxis);
        series->attachAxis(yAxis);
        gui->m_chartView = new QChartView();
        gui->m_chartView->setChart(gui->m_chart);
        if (m_settings.m_chartHeightFixed)
        {
            gui->m_chartView->setMinimumSize(300, m_settings.m_chartHeightPixels);
            gui->m_chartView->setMaximumSize(16777215, m_settings.m_chartHeightPixels);
            gui->m_chartView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
        }
        else
        {
            gui->m_chartView->setMinimumSize(300, 130); // 130 is enough to display axis labels
            gui->m_chartView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
            gui->m_chartView->setSceneRect(0, 0, 300, 130); // This determines m_chartView->sizeHint() - default is 640x480, which is a bit big
        }
        QBoxLayout *chartLayout = new QVBoxLayout();
        gui->m_chartView->setLayout(chartLayout);

        vBox->addWidget(gui->m_chartView);
    }
    else
    {
        // Add new series
        QLineSeries *series = new QLineSeries();
        gui->m_series.insert(id, series);
        QLineSeries *onePointSeries = new QLineSeries();
        gui->m_onePointSeries.insert(id, onePointSeries);
        gui->m_chart->addSeries(series);
        if (!gui->m_rcDevice->m_commonYAxis)
        {
            // Use per series Y axis
            QValueAxis *yAxis = new QValueAxis();
            yAxis->setTitleText(units);
            gui->m_chart->addAxis(yAxis, Qt::AlignRight);
            series->attachAxis(yAxis);
        }
        else
        {
            // Use common y axis
            QAbstractAxis *yAxis = gui->m_chart->axes(Qt::Vertical)[0];
            // Only display units if all the same
            if (yAxis->titleText() != units) {
                yAxis->setTitleText("");
            }
            series->attachAxis(yAxis);
        }
        series->attachAxis(gui->m_chart->axes(Qt::Horizontal)[0]);
    }
}

void RemoteControlGUI::createSensors(RemoteControlDeviceGUI *gui, QVBoxLayout *vBox, FlowLayout *flow, int &widgetCnt, bool &hasCharts)
{
    // Table doesn't seem to expand in a QHBoxLayout, so we have to use a GridLayout
    QGridLayout *grid = nullptr;
    QTableWidget *table = nullptr;
    if (gui->m_rcDevice->m_verticalSensors)
    {
        grid = new QGridLayout();
        grid->setColumnStretch(0, 1);
        vBox->addLayout(grid);
        table = new QTableWidget(gui->m_rcDevice->m_sensors.size(), 3);
        table->verticalHeader()->setVisible(false);
        table->horizontalHeader()->setVisible(false);
        table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
        table->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
        table->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
        table->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
        table->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);  // Needed so table->sizeHint matches minimumSize set below
    }
    else if (!flow)
    {
        flow = new FlowLayout(2, 6, 6);
        vBox->addItem(flow);
    }

    int row = 0;
    bool hasUnits = false;
    for (auto const &sensor : gui->m_rcDevice->m_sensors)
    {
        // For vertical layout, we use a table
        // For horizontal, we use HBox of labels separated with bars
        if (gui->m_rcDevice->m_verticalSensors)
        {
            if (!sensor.m_labelLeft.isEmpty())
            {
                QTableWidgetItem *sensorLabel = new QTableWidgetItem(sensor.m_labelLeft);
                sensorLabel->setFlags(Qt::ItemIsEnabled);
                table->setItem(row, COL_LABEL, sensorLabel);
            }
            QTableWidgetItem *valueItem = new QTableWidgetItem("-");
            table->setItem(row, COL_VALUE, valueItem);
            valueItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
            valueItem->setFlags(Qt::ItemIsEnabled);
            if (!sensor.m_labelRight.isEmpty())
            {
                QTableWidgetItem *unitsItem = new QTableWidgetItem(sensor.m_labelRight);
                unitsItem->setFlags(Qt::ItemIsEnabled);
                table->setItem(row, COL_UNITS, unitsItem);
                hasUnits = true;
            }
            gui->m_sensorValueItems.insert(sensor.m_id, valueItem);
            grid->addWidget(table, 0, 0);
        }
        else
        {
            if (widgetCnt > 0)
            {
                QFrame *line = new QFrame();
                line->setFrameShape(QFrame::VLine);
                line->setFrameShadow(QFrame::Sunken);
                flow->addWidget(line);
            }
            if (!sensor.m_labelLeft.isEmpty())
            {
                QLabel *sensorLabel = new QLabel(sensor.m_labelLeft);
                flow->addWidget(sensorLabel);
            }
            QLabel *sensorValue = new QLabel("-");
            flow->addWidget(sensorValue);
            if (!sensor.m_labelRight.isEmpty())
            {
                QLabel *sensorUnits = new QLabel(sensor.m_labelRight);
                flow->addWidget(sensorUnits);
            }
            gui->m_sensorValueLabels.insert(sensor.m_id, sensorValue);
        }

        if (sensor.m_plot)
        {
            createChart(gui, vBox, sensor.m_id, sensor.m_labelRight);
            hasCharts = true;
        }

        widgetCnt++;
        row++;
    }

    if (table)
    {
        table->resizeColumnToContents(COL_LABEL);
        if (hasUnits) {
            table->resizeColumnToContents(COL_UNITS);
        } else {
            table->hideColumn(COL_UNITS);
        }

        int tableWidth = 0;
        for (int i = 0; i < table->columnCount(); i++){
           tableWidth += table->columnWidth(i);
        }
        int tableHeight = 0;
        for (int i = 0; i < table->rowCount(); i++){
           tableHeight += table->rowHeight(i);
        }
        table->setMinimumWidth(tableWidth);
        table->setMinimumHeight(tableHeight+2);
    }
}

RemoteControlGUI::RemoteControlDeviceGUI *RemoteControlGUI::createDeviceGUI(RemoteControlDevice *rcDevice)
{
    // Create the UI for the device
    RemoteControlDeviceGUI *gui = new RemoteControlDeviceGUI(rcDevice);

    bool hasCharts = false;

    gui->m_container = new QWidget(getRollupContents());
    gui->m_container->setWindowTitle(gui->m_rcDevice->m_label);
    bool vertical = gui->m_rcDevice->m_verticalControls || gui->m_rcDevice->m_verticalSensors;
    QVBoxLayout *vBox = new QVBoxLayout();
    vBox->setContentsMargins(2, 2, 2, 2);
    FlowLayout *flow = nullptr;

    if (!vertical)
    {
        flow = new FlowLayout(2, 6, 6);
        vBox->addItem(flow);
    }
    int widgetCnt = 0;

    // Create buttons to control the device
    createControls(gui, vBox, flow, widgetCnt);

    if (gui->m_rcDevice->m_verticalControls) {
        widgetCnt = 0;
    }

    // Create widgets to display the sensor label and its value
    createSensors(gui, vBox, flow, widgetCnt, hasCharts);

    gui->m_container->setLayout(vBox);

    if (hasCharts && !m_settings.m_chartHeightFixed) {
        gui->m_container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
    }

    gui->m_container->show();
    return gui;
}

void RemoteControlGUI::createGUI()
{
    // Delete existing elements
    for (auto gui : m_deviceGUIs)
    {
        delete gui->m_container;
        gui->m_container = nullptr;
    }
    qDeleteAll(m_deviceGUIs);
    m_deviceGUIs.clear();

    // Create new GUIs for each device
    bool expanding = false;
    for (auto device : m_settings.m_devices)
    {
        RemoteControlDeviceGUI *gui = createDeviceGUI(device);
        m_deviceGUIs.append(gui);
        if (gui->m_container->sizePolicy().verticalPolicy() == QSizePolicy::Expanding) {
            expanding = true;
        }
    }
    if (expanding)
    {
        getRollupContents()->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
        setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
    }
    else
    {
        getRollupContents()->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
        setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
    }

    // FIXME: Why are these three steps needed to get the window
    // to resize to the newly added widgets?
    getRollupContents()->arrangeRollups(); // Recalc rollup size
    layout()->activate(); // Get QMdiSubWindow to recalc its sizeHint
    resize(sizeHint());

    // Need to do it twice when FlowLayout is used!
    getRollupContents()->arrangeRollups();
    layout()->activate();
    resize(sizeHint());
}

void RemoteControlGUI::on_startStop_toggled(bool checked)
{
    if (m_doApplySettings)
    {
        RemoteControl::MsgStartStop *message = RemoteControl::MsgStartStop::create(checked);
        m_remoteControl->getInputMessageQueue()->push(message);
    }
}

void RemoteControlGUI::on_update_clicked()
{
    RemoteControl::MsgDeviceGetState *message = RemoteControl::MsgDeviceGetState::create();
    m_remoteControl->getInputMessageQueue()->push(message);
}

void RemoteControlGUI::on_settings_clicked()
{
    // Display settings dialog
    RemoteControlSettingsDialog dialog(&m_settings);
    if (dialog.exec() == QDialog::Accepted)
    {
        createGUI();
        applySettings();
        on_update_clicked();
    }
}

void RemoteControlGUI::on_clearData_clicked()
{
    // Clear data in all charts
    for (auto deviceGUI : m_deviceGUIs)
    {
        for (auto series : deviceGUI->m_series) {
            series->clear();
        }
        for (auto series : deviceGUI->m_onePointSeries) {
            series->clear();
        }
    }
}

// Update a control widget with latest state value
void RemoteControlGUI::updateControl(QWidget *widget, const DeviceDiscoverer::ControlInfo *controlInfo, const QString &key, const QVariant &value)
{
    if (ButtonSwitch *button = qobject_cast<ButtonSwitch *>(widget))
    {
        if ((QMetaType::Type)value.type() == QMetaType::QString)
        {
            if (value.toString() == "unavailable")
            {
                button->setStyleSheet("QToolButton { background-color : gray; }"
                                      "QToolButton:checked { background-color : gray; }"
                                      "QToolButton:disabled { background-color : gray; }");
            }
            else if (value.toString() == "error")
            {
                button->setStyleSheet("QToolButton { background-color : red; }"
                                      "QToolButton:checked { background-color : red; }"
                                      "QToolButton:disabled { background-color : red; }");
            }
            else
            {
                qDebug() << "RemoteControlGUI::updateControl: String value for button " << key << value;
            }
        }
        else
        {
            int state = value.toInt();
            int prev = button->blockSignals(true);
            button->setChecked(state != 0);
            button->blockSignals(prev);
            button->setStyleSheet("QToolButton { background-color : blue; }"
                                  "QToolButton:checked { background-color : green; }"
                                  "QToolButton:disabled { background-color : gray; }");
        }
    }
    else if (QSpinBox *spinBox = qobject_cast<QSpinBox *>(widget))
    {
        int prev = spinBox->blockSignals(true);
        if (value.toString() == "unavailable")
        {
            spinBox->setStyleSheet("QSpinBox { background-color : gray; }");
        }
        else if (value.toString() == "error")
        {
            spinBox->setStyleSheet("QSpinBox { background-color : red; }");
        }
        else
        {
            int state = value.toInt();
            bool outOfRange = (state < spinBox->minimum()) || (state > spinBox->maximum());
            spinBox->setValue(state);
            if (outOfRange) {
                spinBox->setStyleSheet("QSpinBox { background-color : red; }");
            } else {
                spinBox->setStyleSheet("");
            }
        }
        spinBox->blockSignals(prev);
    }
    else if (QDoubleSpinBox *spinBox = qobject_cast<QDoubleSpinBox *>(widget))
    {
        int prev = spinBox->blockSignals(true);
        if (value.toString() == "unavailable")
        {
            spinBox->setStyleSheet("QDoubleSpinBox { background-color : gray; }");
        }
        else if (value.toString() == "error")
        {
            spinBox->setStyleSheet("QDoubleSpinBox { background-color : red; }");
        }
        else
        {
            double state = value.toDouble();
            if (controlInfo) {
                state = state / controlInfo->m_scale;
            }
            bool outOfRange = (state < spinBox->minimum()) || (state > spinBox->maximum());
            spinBox->setValue(state);
            if (outOfRange) {
                spinBox->setStyleSheet("QDoubleSpinBox { background-color : red; }");
            } else {
                spinBox->setStyleSheet("");
            }
        }
        spinBox->blockSignals(prev);
    }
    else if (QDial *dial = qobject_cast<QDial *>(widget))
    {
        int prev = dial->blockSignals(true);
        if (value.toString() == "unavailable")
        {
            dial->setStyleSheet("QDial { background-color : gray; }");
        }
        else if (value.toString() == "error")
        {
            dial->setStyleSheet("QDial { background-color : red; }");
        }
        else
        {
            double state = value.toDouble();
            if (controlInfo) {
                state = state / controlInfo->m_scale;
            }
            bool outOfRange = (state < dial->minimum()) || (state > dial->maximum());
            dial->setValue(state);
            if (outOfRange) {
                dial->setStyleSheet("QDial { background-color : red; }");
            } else {
                dial->setStyleSheet("");
            }
        }
        dial->blockSignals(prev);
    }
    else if (QSlider *slider = qobject_cast<QSlider *>(widget))
    {
        int prev = slider->blockSignals(true);
        if (value.toString() == "unavailable")
        {
            slider->setStyleSheet("QSlider { background-color : gray; }");
        }
        else if (value.toString() == "error")
        {
            slider->setStyleSheet("QSlider { background-color : red; }");
        }
        else
        {
            double state = value.toDouble();
            if (controlInfo) {
                state = state / controlInfo->m_scale;
            }
            bool outOfRange = (state < slider->minimum()) || (state > slider->maximum());
            slider->setValue(state);
            if (outOfRange) {
                slider->setStyleSheet("QSlider { background-color : red; }");
            } else {
                slider->setStyleSheet("");
            }
        }
        slider->blockSignals(prev);
    }
    else if (QComboBox *comboBox = qobject_cast<QComboBox *>(widget))
    {
        int prev = comboBox->blockSignals(true);
        QString string = value.toString();
        int index = comboBox->findText(string);
        if (index != -1)
        {
            comboBox->setCurrentIndex(index);
            comboBox->setStyleSheet("");
        }
        else
        {
            comboBox->setStyleSheet("QComboBox { background-color : red; }");
        }
        comboBox->blockSignals(prev);
    }
    else if (QLineEdit *lineEdit = qobject_cast<QLineEdit *>(widget))
    {
        lineEdit->setText(value.toString());
    }
    else if (QLabel *label = qobject_cast<QLabel *>(widget))
    {
        label->setText(value.toString());
    }
    else
    {
        qDebug() << "RemoteControlGUI::updateControl: Unexpected widget type";
    }
}

void RemoteControlGUI::updateChart(RemoteControlDeviceGUI *deviceGUI, const QString &key, const QVariant &value)
{
    // Format the value for display
    bool ok = false;
    double d = value.toDouble(&ok);
    bool iOk = false;
    int iValue = value.toInt(&iOk);
    QString formattedValue;
    RemoteControlSensor *sensor = deviceGUI->m_rcDevice->getSensor(key);
    QString format = sensor->m_format.trimmed();
    if (format.contains("%s"))
    {
        formattedValue = QString::asprintf(format.toUtf8(), value.toString().toUtf8().data());
    }
    else if (format.contains("%d") || format.contains("%u") || format.contains("%x") || format.contains("%X"))
    {
        formattedValue = QString::asprintf(format.toUtf8(), value.toInt());
    }
    else if (((QMetaType::Type)value.type() == QMetaType::Double) || ((QMetaType::Type)value.type() == QMetaType::Float))
    {
        if (format.isEmpty()) {
            format = "%.1f";
        }
        formattedValue = QString::asprintf(format.toUtf8(), value.toDouble());
    }
    else if (iOk)
    {
        formattedValue = QString::asprintf("%d", iValue);
    }
    else
    {
        formattedValue = value.toString();
    }

    // Update sensor value widget to display the latest value
    if (deviceGUI->m_sensorValueLabels.contains(key)) {
        deviceGUI->m_sensorValueLabels.value(key)->setText(formattedValue);
    } else {
        deviceGUI->m_sensorValueItems.value(key)->setText(formattedValue);
    }

    // Plot value on chart
    if (deviceGUI->m_series.contains(key))
    {
        QLineSeries *onePointSeries = deviceGUI->m_onePointSeries.value(key);
        QLineSeries *series = deviceGUI->m_series.value(key);
        QDateTime dt = QDateTime::currentDateTime();
        if (ok)
        {
            // Charts aren't displayed properly if series has only one point,
            // so we save the first point in an additional series: onePointSeries
            if (onePointSeries->count() == 0)
            {
                onePointSeries->append(dt.toMSecsSinceEpoch(), d);
            }
            else
            {
                if (series->count() == 0) {
                    series->append(onePointSeries->at(0));
                }
                series->append(dt.toMSecsSinceEpoch(), d);
                QList<QAbstractAxis *> axes = deviceGUI->m_chart->axes(Qt::Horizontal, series);
                QDateTimeAxis *dtAxis = (QDateTimeAxis *)axes[0];
                QDateTime start = QDateTime::fromMSecsSinceEpoch(series->at(0).x());
                QDateTime end = QDateTime::fromMSecsSinceEpoch(series->at(series->count() - 1).x());
                if (start.date() == end.date())
                {
                    if (start.secsTo(end) < 60*5) {
                        dtAxis->setFormat(QString("hh:mm:ss"));
                    } else {
                        dtAxis->setFormat(QString("hh:mm"));
                    }
                }
                else
                {
                    dtAxis->setFormat(QString("%1 hh:mm").arg(QLocale::system().dateFormat(QLocale::ShortFormat)));
                }
                dtAxis->setRange(start, end);
                axes = deviceGUI->m_chart->axes(Qt::Vertical, series);
                QValueAxis *yAxis = (QValueAxis *)axes[0];
                if (series->count() == 2)
                {
                    double y1 = series->at(0).y();
                    double y2 = series->at(1).y();
                    double yMin = std::min(y1, y2);
                    double yMax = std::max(y1, y2);
                    double min = (yMin >= 0.0) ? yMin * 0.9 : yMin * 1.1;
                    double max = (yMax >= 0.0) ? yMax * 1.1 : yMax * 0.9;
                    yAxis->setRange(min, max);
                }
                else
                {
                    double min = (d >= 0.0) ? d * 0.9 : d * 1.1;
                    double max = (d >= 0.0) ? d * 1.1 : d * 0.9;
                    if (min < yAxis->min()) {
                        yAxis->setMin(min);
                    }
                    if (max > yAxis->max()) {
                        yAxis->setMax(max);
                    }
                }
            }
        }
        else
        {
            qDebug() << "RemoteControlGUI::deviceUpdated: Error converting " << key << value;
        }
    }
}

void RemoteControlGUI::deviceUpdated(const QString &protocol, const QString &deviceId, const QHash<QString, QVariant> &status)
{
    for (auto deviceGUI : m_deviceGUIs)
    {
        if (   (protocol == deviceGUI->m_rcDevice->m_protocol)
            && (deviceId == deviceGUI->m_rcDevice->m_info.m_id))
        {
            deviceGUI->m_container->setEnabled(true);

            QHashIterator<QString, QVariant> itr(status);

            while (itr.hasNext())
            {
                itr.next();
                QString key = itr.key();
                QVariant value = itr.value();

                if (deviceGUI->m_controls.contains(key))
                {
                    // Update control(s) to display latest state
                    QList<QWidget *> widgets = deviceGUI->m_controls.value(key);
                    DeviceDiscoverer::ControlInfo *control = deviceGUI->m_rcDevice->m_info.getControl(key);

                    for (auto widget : widgets) {
                        updateControl(widget, control, key, value);
                    }
                }
                else if (deviceGUI->m_sensorValueLabels.contains(key) || deviceGUI->m_sensorValueItems.contains(key))
                {
                    // Plot on chart
                    updateChart(deviceGUI, key, value);
                }
                else
                {
                    qDebug() << "RemoteControlGUI::deviceUpdated: Unexpected status key " << key << value;
                }
            }
        }
    }
}

void RemoteControlGUI::deviceUnavailable(const QString &protocol, const QString &deviceId)
{
    for (auto deviceGUI : m_deviceGUIs)
    {
        if (   (protocol == deviceGUI->m_rcDevice->m_protocol)
            && (deviceId == deviceGUI->m_rcDevice->m_info.m_id))
        {
            deviceGUI->m_container->setEnabled(false);
        }
    }
}

void RemoteControlGUI::applySettings(bool force)
{
    if (m_doApplySettings)
    {
        RemoteControl::MsgConfigureRemoteControl* message = RemoteControl::MsgConfigureRemoteControl::create(m_settings, force);
        m_remoteControl->getInputMessageQueue()->push(message);
    }
}

void RemoteControlGUI::makeUIConnections()
{
    QObject::connect(ui->startStop, &ButtonSwitch::toggled, this, &RemoteControlGUI::on_startStop_toggled);
    QObject::connect(ui->update, &QToolButton::clicked, this, &RemoteControlGUI::on_update_clicked);
    QObject::connect(ui->settings, &QToolButton::clicked, this, &RemoteControlGUI::on_settings_clicked);
    QObject::connect(ui->clearData, &QToolButton::clicked, this, &RemoteControlGUI::on_clearData_clicked);
}
