// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

#include "explorer.h"
#include <QTimer>
#include <QDebug>
#include <qsensor.h>
#include <QMetaObject>
#include <QMetaProperty>

Explorer::Explorer(QWidget *parent)
    : QMainWindow(parent)
    , m_sensor(0)
    , ignoreItemChanged(false)
{
    ui.setupUi(this);
    // Clear out example data from the .ui file
    ui.sensors->clear();
    clearSensorProperties();
    clearReading();

    // Force types to be registered
    (void)QSensor::sensorTypes();
    // Listen for changes to the registered types
    QSensor *sensor = new QSensor(QByteArray(), this);
    connect(sensor, SIGNAL(availableSensorsChanged()), this, SLOT(loadSensors()));
}

Explorer::~Explorer()
{
}

void Explorer::loadSensors()
{
    qDebug() << "Explorer::loadSensors";

    // Clear out anything that's in there now
    ui.sensors->clear();

    for (const QByteArray &type : QSensor::sensorTypes()) {
        qDebug() << "Found type" << type;
        for (const QByteArray &identifier : QSensor::sensorsForType(type)) {
            qDebug() << "Found identifier" << identifier;
            // Don't put in sensors we can't connect to
            QSensor sensor(type);
            sensor.setIdentifier(identifier);
            if (!sensor.connectToBackend()) {
                qDebug() << "Couldn't connect to" << identifier;
                continue;
            }

            qDebug() << "Adding identifier" << identifier;
            QTreeWidgetItem *item = new QTreeWidgetItem(QStringList() << QString::fromLatin1(identifier));
            item->setData(0, Qt::UserRole, QString::fromLatin1(type));
            ui.sensors->addTopLevelItem(item);
        }
    }

    if (ui.sensors->topLevelItemCount() == 0) {
        QTreeWidgetItem *item = new QTreeWidgetItem(QStringList() << tr("No Sensors Found"));
        item->setData(0, Qt::UserRole, QString());
        ui.sensors->addTopLevelItem(item);
    }

    ui.sensors->setCurrentItem(0);
    ui.scrollArea->hide();

    resizeSensors();
}

void Explorer::resizeSensors()
{
    ui.sensors->resizeColumnToContents(0);
    int length = ui.sensors->header()->length() + 4;
    ui.sensors->setFixedWidth(length);
}

void Explorer::on_sensors_currentItemChanged()
{
    qDebug() << "Explorer::sensorSelected";

    // Clear out anything that's in there now
    if (m_sensor) {
        delete m_sensor;
        m_sensor = 0;
    }
    clearSensorProperties();
    clearReading();
    ui.scrollArea->hide();

    // Check that we've selected an item
    QTreeWidgetItem *item = ui.sensors->currentItem();
    if (!item) {
        qWarning() << "Didn't select an item!";
        return;
    }

    QByteArray type = item->data(0, Qt::UserRole).toString().toLatin1();
    QByteArray identifier = item->data(0, Qt::DisplayRole).toString().toLatin1();

    if (type.isEmpty()) {
        // Uh oh, there aren't any sensors.
        // The user has clicked the dummy list entry so just ignore it.
        return;
    }

    // Connect to the sensor so we can probe it
    m_sensor = new QSensor(type, this);
    connect(m_sensor, SIGNAL(readingChanged()), this, SLOT(sensor_changed()));
    m_sensor->setIdentifier(identifier);
    if (!m_sensor->connectToBackend()) {
        delete m_sensor;
        m_sensor = 0;
        qWarning() << "Can't connect to the sensor!";
        return;
    }

    ui.scrollArea->show();
    loadSensorProperties();
    loadReading();

    adjustTableColumns(ui.sensorprops);
    adjustTableColumns(ui.reading);
    QTimer::singleShot(100, this, SLOT(adjustSizes()));
}

void Explorer::clearReading()
{
    ui.reading->setRowCount(0);
}

void Explorer::loadReading()
{
    // Probe the reading using Qt's meta-object facilities
    QSensorReading *reading = m_sensor->reading();
    if (!reading)
        return;
    const QMetaObject *mo = reading->metaObject();
    int firstProperty = QSensorReading::staticMetaObject.propertyOffset();

    ui.reading->setRowCount(mo->propertyCount() - firstProperty);

    for (int i = firstProperty; i < mo->propertyCount(); ++i) {
        int row = i - firstProperty;
        QTableWidgetItem *index;
        if (row == 0)
            // timestamp is not available via index
            index = new QTableWidgetItem(QLatin1String("N/A"));
        else
            index = new QTableWidgetItem(QVariant(row - 1).toString());
        QTableWidgetItem *prop = new QTableWidgetItem(mo->property(i).name());
        QString typeName = QLatin1String(mo->property(i).typeName());
        int delimiter = typeName.lastIndexOf("::");
        if (delimiter != -1)
            typeName = typeName.mid(delimiter + 2);
        QTableWidgetItem *type = new QTableWidgetItem(typeName);
        QTableWidgetItem *value = new QTableWidgetItem();

        index->setFlags(value->flags() ^ Qt::ItemIsEditable);
        prop->setFlags(value->flags() ^ Qt::ItemIsEditable);
        type->setFlags(value->flags() ^ Qt::ItemIsEditable);
        value->setFlags(value->flags() ^ Qt::ItemIsEditable);

        ui.reading->setItem(row, 0, index);
        ui.reading->setItem(row, 1, prop);
        ui.reading->setItem(row, 2, type);
        ui.reading->setItem(row, 3, value);
    }
}

void Explorer::clearSensorProperties()
{
    ui.sensorprops->setRowCount(0);
}

void Explorer::loadSensorProperties()
{
    ignoreItemChanged = true;

    // Probe the sensor using Qt's meta-object facilities
    const QMetaObject *mo = m_sensor->metaObject();
    int firstProperty = QSensor::staticMetaObject.propertyOffset();

    int rows = mo->propertyCount() - firstProperty;
    ui.sensorprops->setRowCount(rows);

    int offset = 0;
    for (int i = firstProperty; i < mo->propertyCount(); ++i) {
        int row = i - firstProperty - offset;
        QLatin1String name(mo->property(i).name());
        if (name == "identifier" ||
            //name == "type" ||
            name == "reading" ||
            name == "connected" ||
            name == "running" ||
            name == "supportsPolling") {
            ++offset;
            continue;
        }
        QTableWidgetItem *prop = new QTableWidgetItem(name);
        QString typeName = QLatin1String(mo->property(i).typeName());
        int delimiter = typeName.lastIndexOf("::");
        if (delimiter != -1)
            typeName = typeName.mid(delimiter + 2);
        QTableWidgetItem *type = new QTableWidgetItem(typeName);
        QVariant v = mo->property(i).read(m_sensor);
        QString val;
        if (typeName == "qrangelist") {
            qrangelist rl = v.value<qrangelist>();
            QStringList out;
            for (const qrange &r : rl) {
                if (r.first == r.second)
                    out << QString("%1 Hz").arg(r.first);
                else
                    out << QString("%1-%2 Hz").arg(r.first).arg(r.second);
            }
            val = out.join(", ");
        } else if (typeName == "qoutputrangelist") {
            qoutputrangelist rl = v.value<qoutputrangelist>();
            QStringList out;
            for (const qoutputrange &r : rl)
                out << QString("(%1, %2) += %3").arg(r.minimum).arg(r.maximum).arg(r.accuracy);
            val = out.join(", ");
        } else {
            val = v.toString();
        }
        QTableWidgetItem *value = new QTableWidgetItem(val);

        prop->setFlags(value->flags() ^ Qt::ItemIsEditable);
        type->setFlags(value->flags() ^ Qt::ItemIsEditable);
        if (!mo->property(i).isWritable()) {
            // clear the editable flag
            value->setFlags(value->flags() ^ Qt::ItemIsEditable);
        }

        ui.sensorprops->setItem(row, 0, prop);
        ui.sensorprops->setItem(row, 1, type);
        ui.sensorprops->setItem(row, 2, value);
    }

    // We don't add all properties
    ui.sensorprops->setRowCount(rows - offset);

    ignoreItemChanged = false;
}

void Explorer::showEvent(QShowEvent *event)
{
    // Once we're visible, load the sensors
    // (don't delay showing the UI while we load plugins, connect to backends, etc.)
    QTimer::singleShot(0, this, SLOT(loadSensors()));

    QMainWindow::showEvent(event);
}

// Resize columns to fit the space.
// This shouldn't be so hard!
void Explorer::adjustTableColumns(QTableWidget *table)
{
    if (table->rowCount() == 0) {
        table->setFixedHeight(0);
        return;
    }

    // At least this is easy to do
    table->resizeColumnsToContents();
    int length = table->verticalHeader()->length();
    length += (length / static_cast<qreal>(table->verticalHeader()->count())); // Add 1 more (the header itself)
    table->setFixedHeight(length);

    int columns = table->columnCount();
    QList<int> width;
    int suggestedWidth = 0;
    for (int i = 0; i < columns; ++i) {
        int cwidth = table->columnWidth(i);
        width << cwidth;
        suggestedWidth += cwidth;
    }

    int actualWidth = table->size().width();
    //qDebug() << "suggestedWidth" << suggestedWidth << "actualWidth" << actualWidth;

    // We only scale the columns up, we don't scale down
    if (actualWidth <= suggestedWidth)
        return;

    qreal multiplier = actualWidth / static_cast<qreal>(suggestedWidth);
    int currentSpace = 4;
    for (int i = 0; i < columns; ++i) {
        width[i] = multiplier * width[i];
        currentSpace += width[i];
    }

    // It ends up too big due to cell decorations or something.
    // Make things smaller one pixel at a time in round robin fashion until we're good.
    int i = 0;
    while (currentSpace > actualWidth) {
        --width[i];
        --currentSpace;
        i = (i + 1) % columns;
    }

    for (int i = 0; i < columns; ++i) {
        table->setColumnWidth(i, width[i]);
    }

    table->setMinimumWidth(suggestedWidth);
}

void Explorer::adjustSizes()
{
    adjustTableColumns(ui.reading);
    adjustTableColumns(ui.sensorprops);
}

void Explorer::resizeEvent(QResizeEvent *event)
{
    resizeSensors();
    adjustSizes();

    QMainWindow::resizeEvent(event);
}

void Explorer::on_start_clicked()
{
    m_sensor->start();
    QTimer::singleShot(0, this, SLOT(loadSensorProperties()));
}

void Explorer::on_stop_clicked()
{
    m_sensor->stop();
    QTimer::singleShot(0, this, SLOT(loadSensorProperties()));
}

void Explorer::sensor_changed()
{
    QSensorReading *reading = m_sensor->reading();
    filter(reading);
}

bool Explorer::filter(QSensorReading *reading)
{
    const QMetaObject *mo = reading->metaObject();
    int firstProperty = QSensorReading::staticMetaObject.propertyOffset();

    for (int i = firstProperty; i < mo->propertyCount(); ++i) {
        int row = i - firstProperty;
        QString typeName = QLatin1String(mo->property(i).typeName());
        int delimiter = typeName.lastIndexOf("::");
        if (delimiter != -1)
            typeName = typeName.mid(delimiter + 2);
        QLatin1String name(mo->property(i).name());
        QTableWidgetItem *value = ui.reading->item(row, 3);
        QVariant val = mo->property(i).read(reading);
        if (typeName == "LightLevel") {
            QString text;
            switch (val.toInt()) {
            case 1:
                text = "Dark";
                break;
            case 2:
                text = "Twilight";
                break;
            case 3:
                text = "Light";
                break;
            case 4:
                text = "Bright";
                break;
            case 5:
                text = "Sunny";
                break;
            default:
                text = "Undefined";
                break;
            }
            value->setText(text);
        } else {
            value->setText(val.toString());
        }
    }

    adjustTableColumns(ui.reading);
    //QTimer::singleShot(0, this, SLOT(adjustSizes()));

    return false;
}

void Explorer::on_sensorprops_itemChanged(QTableWidgetItem *item)
{
    if (ignoreItemChanged)
        return;
    if (!(item->flags() & Qt::ItemIsEditable))
        return;

    int row = item->row();
    QString name = ui.sensorprops->item(row, 0)->text();
    QVariant value = item->text();

    qDebug() << "setProperty" << name << value;
    m_sensor->setProperty(name.toLatin1().constData(), QVariant(value));

    QTimer::singleShot(0, this, SLOT(loadSensorProperties()));
}

