// SPDX-License-Identifier: GPL-3.0-or-later

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright (C) 2013 - 2024, nymea GmbH
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
*
* This file is part of nymea-plugins-modbus.
*
* nymea-plugins-modbus 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, either version 3 of the License, or
* (at your option) any later version.
*
* nymea-plugins-modbus 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 for more details.
*
* You should have received a copy of the GNU General Public License
* along with nymea-plugins-modbus. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#include "unipi.h"
#include "extern-plugininfo.h"
#include <QProcess>
#include <QTimer>

UniPi::UniPi(I2CManager *i2cManager, UniPiType unipiType, QObject *parent) :
    QObject(parent),
    m_i2cManager(i2cManager),
    m_unipiType(unipiType)
{
    m_mcp23008 = new MCP23008("i2c-1", 0x20, this);
    m_analogInputChannel1 = new MCP342XChannel("i2c-1", 0x68, 0, MCP342XChannel::Gain_1, this);
    m_analogInputChannel2 = new MCP342XChannel("i2c-1", 0x68, 1, MCP342XChannel::Gain_1, this);

    m_analogOutput = new UniPiPwm(0, this);
}

UniPi::~UniPi()
{
    m_mcp23008->deleteLater();

    //m_i2cManager->close(m_analogInputChannel1);
    m_analogInputChannel1->deleteLater();
    //m_i2cManager->close(m_analogInputChannel2);
    m_analogInputChannel2->deleteLater();

    Q_FOREACH (GpioMonitor *gpio, m_monitorGpios.keys()) {
        gpio->disable();
        gpio->deleteLater();
    }

    m_analogOutput->disable();
    m_analogOutput->deleteLater();
}

bool UniPi::init()
{
    //init MCP23008 Outputs
    if (m_mcp23008->init()) {
        m_mcp23008->writeRegister(MCP23008::RegisterAddress::IODIR, 0x00); //set all pins as outputs
        m_mcp23008->writeRegister(MCP23008::RegisterAddress::IPOL, 0x00);  //set all pins to non inverted mode 1 = high
        m_mcp23008->writeRegister(MCP23008::RegisterAddress::GPPU, 0x00);  //disable all pull up resistors
        m_mcp23008->writeRegister(MCP23008::RegisterAddress::OLAT, 0x00);  //Set all outputs to low
    } else {
        qCWarning(dcUniPi()) << "Could not init MCP23008";
        return false;
    }

    // In case of re-init
    if (!m_monitorGpios.isEmpty()) {
        foreach (GpioMonitor *gpio, m_monitorGpios.keys()) {
            m_monitorGpios.remove(gpio);
            gpio->deleteLater();
        }
    }

    //Init Raspberry Pi Inputs
    foreach (QString circuit, digitalInputs()){

        int pin = getPinFromCircuit(circuit);

        GpioMonitor *gpioMonitor = new GpioMonitor(pin, this);
        if (!gpioMonitor->enable()) {
            qCWarning(dcUniPi()) << "Could not enable gpio monitor for pin" << pin;
            return false;
        } else {
            QProcess::execute(QString("gpio -g mode %1 up").arg(pin));
            QTimer::singleShot(1000, this, [gpioMonitor, circuit, this]() {
                emit digitalInputStatusChanged(circuit, gpioMonitor->value()); //set initial status
                connect(gpioMonitor, &GpioMonitor::valueChanged, this, &UniPi::onInputValueChanged);
                m_monitorGpios.insert(gpioMonitor, circuit);
            });
        }
    }
    //Init Raspberry Pi PWM output
    if (!m_analogOutput->exportPwm()) {
        qCWarning(dcUniPi()) << "Error could not export analog output";
        return false;
    }

    if (!m_analogOutput->enable()) {
        qCWarning(dcUniPi()) << "Error could not enable analog output";
        return false;
    }
    m_analogOutput->setPolarity(UniPiPwm::PolarityNormal);
    m_analogOutput->setFrequency(400);
    m_analogOutput->setPercentage(0);

    if (!m_i2cManager->open(m_analogInputChannel1)) {
        qCDebug(dcUniPi()) << "Failed to open analog channel 1";
        return false;
    }
    connect(m_analogInputChannel1, &MCP342XChannel::readingAvailable, this, [this] (const QByteArray &data){

        if (data.length() < 3) {
            qCWarning(dcUniPi()) << "Error reading data from analog channel 1" << data;
            return;
        }
        qint16 rawValue = (static_cast<quint16>(data[0]) << 8) | data[1];
        double voltage = (rawValue * 0.001 * 5.51)/2.00;
        emit analogInputStatusChanged("AI1", voltage);
    });
    m_i2cManager->startReading(m_analogInputChannel1, 5000);

    if (!m_i2cManager->open(m_analogInputChannel2)) {
        qCDebug(dcUniPi()) << "Failed to open analog channel 2";
        return false;
    }
    connect(m_analogInputChannel2, &MCP342XChannel::readingAvailable, this, [this] (const QByteArray &data){

        if (data.length() < 3) {
            qCWarning(dcUniPi()) << "Error reading data from analog channel 2" << data;
            return;
        }
        qint16 rawValue = (static_cast<quint16>(data[0]) << 8) | data[1];
        double voltage = (rawValue * 0.001 * 5.51)/2.00;
        emit analogInputStatusChanged("AI2", voltage);
    });
    m_i2cManager->startReading(m_analogInputChannel2, 5000);
    return true;
}

QString UniPi::type()
{
    QString type;
    switch (m_unipiType) {
    case UniPiType::UniPi1:
        type = "UniPi 1";
        break;
    case UniPiType::UniPi1Lite:
        type = "UniPi 1 Lite";
        break;
    }
    return type;
}

QList<QString> UniPi::digitalInputs()
{
    QList<QString> inputs;
    switch (m_unipiType) {
    case UniPiType::UniPi1:
        for (int i = 1; i < 15; ++i) {
            inputs.append(QString("DI%1").arg(i));
        }
        break;
    case UniPiType::UniPi1Lite:
        for (int i = 1; i < 7; ++i) {
            inputs.append(QString("DI%1").arg(i));
        }
        break;
    }
    return inputs;
}

QList<QString> UniPi::digitalOutputs()
{
    QList<QString> outputs;
    switch (m_unipiType) {
    case UniPiType::UniPi1:
        for (int i = 1; i < 9; ++i) {
            outputs.append(QString("DO%1").arg(i));
        }
        break;
    case UniPiType::UniPi1Lite:
        for (int i = 1; i < 7; ++i) {
            outputs.append(QString("DO%1").arg(i));
        }
        break;
    }
    return outputs;
}

QList<QString> UniPi::analogInputs()
{
    QList<QString> inputs;
    switch (m_unipiType) {
    case UniPiType::UniPi1:
        for (int i = 1; i < 3; ++i) {
            inputs.append(QString("AI%1").arg(i));
        }
        break;
    case UniPiType::UniPi1Lite:
        for (int i = 1; i < 3; ++i) {
            inputs.append(QString("AI%1").arg(i));
        }
        break;
    }
    return inputs;
}

QList<QString> UniPi::analogOutputs()
{
    QList<QString> outputs;
    outputs.append("AO");
    return outputs;
}

int UniPi::getPinFromCircuit(const QString &circuit)
{
    int pin = 0;
    if (circuit.startsWith("DI")) { //Raspberry Pi Input Pins
        switch (circuit.mid(2, 2).toInt()) {
        case 1: //DI01 GPIO04 Digital input
            pin = 4;
            break;
        case 2: //DI02 GPIO17 Digital input
            pin = 17;
            break;
        case 3: //DI03 GPIO27 Digital input
            pin = 27;
            break;
        case 4: //DI04 GPIO23 Digital input
            pin = 23;
            break;
        case 5: //DI05 GPIO22 Digital input
            pin = 22;
            break;
        case 6: //DI06 GPIO24 Digital input
            pin = 24;
            break;
        case 7: //I07 GPIO11 Digital Input
            pin = 11;
            break;
        case 8: //I08 GPIO07 Digital Input
            pin = 7;
            break;
        case 9: //I09 GPIO08 Digital Input
            pin = 8;
            break;
        case 10: //I10 GPIO09 Digital Input
            pin = 9;
            break;
        case 11: //I11 GPIO25 Digital Input
            pin = 25;
            break;
        case 12: //DI12 GPIO10 Digital input
            pin = 10;
            break;
        case 13: //DI13 GPIO31 Digital input
            pin = 31;
            break;
        case 14: //DI14 GPIO30 Digital input
            pin = 30;
            break;
        default:
            return 0;
        }
    }
    if (circuit.startsWith("DO")) { //MCP23008 Output Pins
        switch (circuit.mid(2, 2).toInt()) {
        case 01: //DO1 GP07 Digital Output
            pin = 7;
            break;
        case 02: //DO1 GP07 Digital Output
            pin = 6;
            break;
        case 03: //DO1 GP07 Digital Output
            pin = 5;
            break;
        case 04: //DO1 GP07 Digital Output
            pin = 4;
            break;
        case 05: //DO1 GP07 Digital Output
            pin = 3;
            break;
        case 06: //DO1 GP07 Digital Output
            pin = 2;
            break;
        default:
            return 0;
        }
    }

    if (circuit.startsWith("AO")) { //Raspberry Pi Analog Output
        switch (circuit.mid(2, 2).toInt()) {
        case 0: //AO GPIO18 PWM Analog Output 0-10V
            pin = 18;
            break;
        default:
            return 0;
        }
    }

    if (circuit.startsWith("AI")) { //MCP3422 analog input channels
        switch (circuit.mid(2, 2).toInt()) {
        case 1:
            pin = 1; //MCP3422 Channel 1
            break;
        case 2:
            pin = 2; //MCP3422 Channel 2
            break;
        default:
            return 0;
        }
    }
    return pin;
}

bool UniPi::setDigitalOutput(const QString &circuit, bool status)
{
    int pin = getPinFromCircuit(circuit);
    if (pin == 0) {
        qWarning(dcUniPi()) << "Out of range pin number";
        return false;
    }

    quint8 registerValue;
    if(!m_mcp23008->readRegister(MCP23008::RegisterAddress::OLAT, &registerValue))
        return false;
    if (status) {
        registerValue |= (1 << pin);
    } else {
        registerValue &= ~(1 << pin);
    }
    //write output register
    if(!m_mcp23008->writeRegister(MCP23008::RegisterAddress::OLAT, registerValue))
        return false;

    getDigitalOutput(circuit);
    return true;
}

bool UniPi::getDigitalOutput(const QString &circuit)
{
    int pin = getPinFromCircuit(circuit);
    if (pin > 7)
        return false;

    uint8_t registerValue;
    if(!m_mcp23008->readRegister(MCP23008::RegisterAddress::OLAT, &registerValue))
        return false;

    emit digitalOutputStatusChanged(circuit, ( registerValue & (1 << pin)));
    return  true;
}

bool UniPi::getDigitalInput(const QString &circuit)
{
    int pin = getPinFromCircuit(circuit);
    if (pin == 0) {
        qCWarning(dcUniPi()) << "Out of range pin number";
        return false;
    }
    if (!m_monitorGpios.values().contains(circuit)) {
        qCWarning(dcUniPi()) << "Could not read digital inpu, GPIO not initialized" << circuit;
        return false;
    }
    //Read RPi pins
    GpioMonitor *gpio = m_monitorGpios.key(circuit);
    digitalInputStatusChanged(circuit, gpio->value());
    return true;
}

bool UniPi::setAnalogOutput(double value)
{
    int percentage = value * 10;     //convert volt to percentage
    if(!m_analogOutput->setPercentage(percentage))
        return false;

    getAnalogOutput();
    return true;
}

bool UniPi::getAnalogOutput()
{
    double voltage = m_analogOutput->percentage()/10.0;
    emit analogOutputStatusChanged(voltage);

    return true;
}

void UniPi::onInputValueChanged(const bool &value)
{
    GpioMonitor *monitor = static_cast<GpioMonitor *>(sender());
    if (!m_monitorGpios.contains(monitor))
        return;

    QString circuit = m_monitorGpios.value(monitor);
    emit digitalInputStatusChanged(circuit, value);
}
