/*
    SPDX-FileCopyrightText: 2003 Jasem Mutlaq <mutlaqja@ikarustech.com>

    SPDX-License-Identifier: GPL-2.0-or-later
*/

#include "devicemanager.h"

#include "Options.h"
#include "indimenu.h"
#include "indiproperty.h"
#include "indigroup.h"
#include "indidevice.h"
#include "indistd.h"
#include "indidriver.h"
#include "kstars.h"
#include "kstarsdatetime.h"

#include <indicom.h>

#include <config-kstars.h>

//#include <stdlib.h>
//#include <unistd.h>

#include <QTcpSocket>
#include <QTextEdit>

#include <KProcess>
#include <KLocale>
#include <KDebug>
#include <KMessageBox>
#include <KStatusBar>

const int INDI_MAX_TRIES = 3;

/*******************************************************************
** The device manager contain devices running from one indiserver
** This allow KStars to control multiple devices distributed across
** multiple servers seemingly in a way that is completely transparent
** to devices and drivers alike.
** The device Manager can be thought of as the 'networking' parent
** of devices, while indimenu is 'GUI' parent of devices
*******************************************************************/
DeviceManager::DeviceManager(INDIMenu *INDIparent, QString inHost, uint inPort, ManagerMode inMode)
{
    parent        = INDIparent;
    serverProcess = nullptr;
    XMLParser     = nullptr;
    host          = inHost;
    port          = inPort;
    mode          = inMode;
}

DeviceManager::~DeviceManager()
{
    serverSocket.close();

    if (serverProcess)
        serverProcess->close();

    delete (serverProcess);

    if (XMLParser)
        delLilXML(XMLParser);

    XMLParser = nullptr;

    while (!indi_dev.isEmpty())
        delete indi_dev.takeFirst();
}

void DeviceManager::startServer()
{
    serverProcess = new KProcess;

    if (managed_devices.isEmpty())
    {
        kWarning() << "managed_devices was not set! Cannot start server!";
        return;
    }
#ifdef Q_OS_OSX
    if (Options::indiServerIsInternal())
        *serverProcess << QCoreApplication::applicationDirPath() + "/indiserver";
    else
#endif
        *serverProcess << Options::indiServer();
    *serverProcess << "-v"
                   << "-p" << QString::number(port);

    foreach (IDevice *device, managed_devices)
    {
        // JM: Temporary workaround for indiserver limit of client BLOBs for CCDs.
        if (device->type == KSTARS_CCD)
        {
            *serverProcess << "-m"
                           << "100";
            break;
        }
    }

    foreach (IDevice *device, managed_devices)
        *serverProcess << device->driver;

    if (mode == DeviceManager::M_LOCAL)
    {
        connect(serverProcess, SIGNAL(readyReadStandardError()), this, SLOT(processStandardError()));
        serverProcess->setOutputChannelMode(KProcess::SeparateChannels);
        serverProcess->setReadChannel(QProcess::StandardError);
    }

    serverProcess->start();

    serverProcess->waitForStarted();

    if (mode == DeviceManager::M_LOCAL)
        connectToServer();
}

void DeviceManager::stopServer()
{
    serverProcess->terminate();
}

void DeviceManager::connectToServer()
{
    connect(&serverSocket, SIGNAL(readyRead()), this, SLOT(dataReceived()));

    for (int i = 0; i < INDI_MAX_TRIES; i++)
    {
        serverSocket.connectToHost(host, port);
        if (serverSocket.waitForConnected(1000))
        {
            connect(&serverSocket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(connectionError()));
            connectionSuccess();
            return;
        }

        usleep(100000);
    }

    connectionError();
}

void DeviceManager::enableBLOB(bool enable, QString device, QString property)
{
    QTextStream serverFP(&serverSocket);
    QString openTag;

    if (device.isEmpty())
        return;

    if (property.isEmpty() == false)
        openTag = QString("<enableBLOB device='%1' name='%2'>").arg(device).arg(property);
    else
        openTag = QString("<enableBLOB device='%1'>").arg(device);

    if (enable)
    {
        serverFP << QString("%1Also</enableBLOB>\n").arg(openTag);
        kDebug() << QString("%1Also</enableBLOB>\n").arg(openTag);
    }
    else
    {
        serverFP << QString("%1Never</enableBLOB>\n").arg(openTag);
        kDebug() << QString("%1Never</enableBLOB>\n").arg(openTag);
    }
}

void DeviceManager::connectionSuccess()
{
    QTextStream serverFP(&serverSocket);

    if (XMLParser)
        delLilXML(XMLParser);
    XMLParser = newLilXML();

    serverFP << QString("<getProperties version='%1'/>\n").arg(INDIVERSION);
}

void DeviceManager::connectionError()
{
    QString errMsg = QString("Connection to INDI host at %1 on port %2 encountered an error: %3.")
                         .arg(host)
                         .arg(port)
                         .arg(serverSocket.errorString());
    KMessageBox::error(nullptr, errMsg);

    emit deviceManagerError(this);
}

void DeviceManager::appendManagedDevices(QList<IDevice *> &processed_devices)
{
    managed_devices = processed_devices;

    foreach (IDevice *device, managed_devices)
    {
        device->unique_label = parent->getUniqueDeviceLabel(device->tree_label);
        //device->mode		= mode;
        device->deviceManager = this;
    }
}

void DeviceManager::processStandardError()
{
    if (serverProcess == nullptr)
        return;

    serverBuffer.append(serverProcess->readAllStandardError());
    emit newServerInput();
}

void DeviceManager::dataReceived()
{
    char errmsg[ERRMSG_SIZE];
    int nr = 0, err_code = 0;
    QTextStream serverFP(&serverSocket);
    QString ibuf, err_cmd;

    ibuf = serverFP.readAll();
    nr   = ibuf.length();

    /* process each char */
    for (int i = 0; i < nr; i++)
    {
        if (!XMLParser)
            return;

        XMLEle *root = readXMLEle(XMLParser, ibuf[i].toAscii(), errmsg);
        if (root)
        {
            if ((err_code = dispatchCommand(root, err_cmd)) < 0)
            {
                // Silently ignore property duplication errors
                if (err_code != INDI_PROPERTY_DUPLICATED)
                {
                    //kDebug() << "Dispatch command error: " << err_cmd << endl;
                    fprintf(stderr, "Dispatch command error: %d for command %s\n", err_code, qPrintable(err_cmd));
                    prXMLEle(stderr, root, 0);
                }
            }

            delXMLEle(root);
        }
        else if (*errmsg)
        {
            kDebug() << "XML Root Error: " << errmsg;
        }
    }
}

int DeviceManager::dispatchCommand(XMLEle *root, QString &errmsg)
{
    if (!strcmp(tagXMLEle(root), "message"))
        return messageCmd(root, errmsg);
    else if (!strcmp(tagXMLEle(root), "delProperty"))
        return delPropertyCmd(root, errmsg);

    /* Get the device, if not available, create it */
    INDI_D *dp = findDev(root, 1, errmsg);

    if (dp == nullptr)
    {
        errmsg = "No device available and none was created";
        return INDI_DEVICE_NOT_FOUND;
    }

    if (!strcmp(tagXMLEle(root), "defTextVector"))
        return dp->buildTextGUI(root, errmsg);
    else if (!strcmp(tagXMLEle(root), "defNumberVector"))
        return dp->buildNumberGUI(root, errmsg);
    else if (!strcmp(tagXMLEle(root), "defSwitchVector"))
        return dp->buildSwitchesGUI(root, errmsg);
    else if (!strcmp(tagXMLEle(root), "defLightVector"))
        return dp->buildLightsGUI(root, errmsg);
    else if (!strcmp(tagXMLEle(root), "defBLOBVector"))
        return dp->buildBLOBGUI(root, errmsg);
    else if (!strcmp(tagXMLEle(root), "setTextVector") || !strcmp(tagXMLEle(root), "setNumberVector") ||
             !strcmp(tagXMLEle(root), "setSwitchVector") || !strcmp(tagXMLEle(root), "setLightVector") ||
             !strcmp(tagXMLEle(root), "setBLOBVector"))
        return dp->setAnyCmd(root, errmsg);
    // Ignore if we get NewXXX commands
    else if (QString(tagXMLEle(root)).startsWith("new"))
        return 0;

    return INDI_DISPATCH_ERROR;
}

/* delete the property in the given device, including widgets and data structs.
 * when last property is deleted, delete the device too.
 * if no property name attribute at all, delete the whole device regardless.
 * return 0 if ok, else -1 with reason in errmsg[].
 */
int DeviceManager::delPropertyCmd(XMLEle *root, QString &errmsg)
{
    XMLAtt *ap;
    INDI_D *dp;
    INDI_P *pp;

    /* dig out device and optional property name */
    dp = findDev(root, 0, errmsg);
    if (!dp)
        return INDI_DEVICE_NOT_FOUND;

    checkMsg(root, dp);

    ap = findXMLAtt(root, "name");

    /* Delete property if it exists, otherwise, delete the whole device */
    if (ap)
    {
        pp = dp->findProp(QString(valuXMLAtt(ap)));

        if (pp)
            return dp->removeProperty(pp);
        else
            return INDI_PROPERTY_INVALID;
    }
    // delete the whole device
    else
        return removeDevice(dp->name, errmsg);
}

int DeviceManager::removeDevice(const QString &devName, QString &errmsg)
{
    // remove all devices if devName == nullptr
    if (devName == nullptr)
    {
        while (!indi_dev.isEmpty())
            delete indi_dev.takeFirst();
        return (0);
    }

    for (int i = 0; i < indi_dev.size(); i++)
    {
        if (indi_dev[i]->name == devName)
        {
            delete indi_dev.takeAt(i);
            return (0);
        }
    }

    errmsg = QString("Device %1 not found").arg(devName);
    return INDI_DEVICE_NOT_FOUND;
}

INDI_D *DeviceManager::findDev(const QString &devName, QString &errmsg)
{
    /* search for existing */
    for (int i = 0; i < indi_dev.size(); i++)
    {
        if (indi_dev[i]->name == devName)
            return indi_dev[i];
    }

    errmsg = QString("INDI: no such device %1").arg(devName);

    return nullptr;
}

/* add new device to mainrc_w using info in dep.
- * if trouble return nullptr with reason in errmsg[]
- */
INDI_D *DeviceManager::addDevice(XMLEle *dep, QString &errmsg)
{
    INDI_D *dp;
    XMLAtt *ap;
    QString device_name, unique_label;
    IDevice *targetDevice = nullptr;

    /* allocate new INDI_D on indi_dev */
    ap = findAtt(dep, "device", errmsg);
    if (!ap)
    {
        errmsg = QString("Unable to find device attribute in XML tree. Cannot add device.");
        kDebug() << errmsg << endl;
        return nullptr;
    }

    device_name = QString(valuXMLAtt(ap));

    if (mode != M_CLIENT)
        foreach (IDevice *device, managed_devices)
        {
            // Each device manager has a list of managed_devices (IDevice). Each IDevice has the original constant name of the driver (driver_class)
            // Therefore, when a new device is discovered, we match the driver name (which never changes, it's always static from indiserver) against the driver_class
            // of IDevice because IDevice can have several names. It can have the tree_label which is the name it has in the local tree widget. Finally, the name that shows
            // up in the INDI control panel is the unique name of the driver, which is for most cases tree_label, but if that exists already then we get tree_label_1..etc

            if (device->name == device_name && device->state == IDevice::DEV_TERMINATE)
            {
                device->state = IDevice::DEV_START;
                unique_label = device->unique_label = parent->getUniqueDeviceLabel(device->tree_label);
                targetDevice                        = device;
                break;
            }
        }

    // For remote INDI drivers with no label attributes
    if (unique_label.isEmpty())
        unique_label = parent->getUniqueDeviceLabel(device_name);

    dp = new INDI_D(parent, this, device_name, unique_label, targetDevice);
    indi_dev.append(dp);
    emit newDevice(dp);

    enableBLOB(true, device_name);

    connect(dp->stdDev, SIGNAL(newTelescope()), parent->ksw->indiDriver(), SLOT(newTelescopeDiscovered()),
            Qt::QueuedConnection);

    /* ok */
    return dp;
}

INDI_D *DeviceManager::findDev(XMLEle *root, int create, QString &errmsg)
{
    XMLAtt *ap = findAtt(root, "device", errmsg);
    char *dn   = nullptr;

    /* get device name */
    if (!ap)
    {
        errmsg = QString("No device attribute found in element %1").arg(tagXMLEle(root));
        return nullptr;
    }
    dn = valuXMLAtt(ap);

    /* search for existing */
    for (int i = 0; i < indi_dev.size(); i++)
    {
        if (indi_dev[i]->name == QString(dn))
            return indi_dev[i];
    }

    /* not found, create if ok */
    if (create)
        return (addDevice(root, errmsg));

    errmsg = QString("INDI: <%1> no such device %2").arg(tagXMLEle(root)).arg(dn);
    return nullptr;
}

/* a general message command received from the device.
 * return 0 if ok, else -1 with reason in errmsg[].
 */
int DeviceManager::messageCmd(XMLEle *root, QString &errmsg)
{
    checkMsg(root, findDev(root, 0, errmsg));
    return (0);
}

/* display message attribute.
 * N.B. don't put carriage control in msg, we take care of that.
 */
void DeviceManager::checkMsg(XMLEle *root, INDI_D *dp)
{
    XMLAtt *ap;
    ap = findXMLAtt(root, "message");

    if (ap)
        doMsg(root, dp);
}

/* display valu of message and timestamp in dp's scrolled area, if any, else general.
 * prefix our time stamp if not included.
 * N.B. don't put carriage control in msg, we take care of that.
 */
void DeviceManager::doMsg(XMLEle *msg, INDI_D *dp)
{
    QTextEdit *txt_w;
    XMLAtt *message;
    XMLAtt *timestamp;

    if (dp == nullptr)
    {
        kDebug() << "Warning: dp is null.";
        return;
    }

    txt_w = dp->msgST_w;

    /* prefix our timestamp if not with msg */
    timestamp = findXMLAtt(msg, "timestamp");

    if (timestamp)
        txt_w->insertPlainText(QString(valuXMLAtt(timestamp)) + QString(" "));
    else
        txt_w->insertPlainText(KStarsDateTime::currentDateTime().toString("yyyy/mm/dd - h:m:s ap "));

    /* finally! the msg */
    message = findXMLAtt(msg, "message");

    if (!message)
        return;

    // Prepend to the log viewer
    txt_w->insertPlainText(QString(valuXMLAtt(message)) + QString("\n"));
    QTextCursor c = txt_w->textCursor();
    c.movePosition(QTextCursor::Start);
    txt_w->setTextCursor(c);

    if (Options::showINDIMessages())
        parent->ksw->statusBar()->changeItem(QString(valuXMLAtt(message)), 0);
}

void DeviceManager::sendNewText(INDI_P *pp)
{
    INDI_E *lp;

    QTextStream serverFP(&serverSocket);

    serverFP << QString("<newTextVector\n");
    serverFP << QString("  device='%1'\n").arg(qPrintable(pp->pg->dp->name));
    serverFP << QString("  name='%1'\n>").arg(qPrintable(pp->name));

    //for (lp = pp->el.first(); lp != nullptr; lp = pp->el.next())
    foreach (lp, pp->el)
    {
        serverFP << QString("  <oneText\n");
        serverFP << QString("    name='%1'>\n").arg(qPrintable(lp->name));
        serverFP << QString("      %1\n").arg(qPrintable(lp->text));
        serverFP << QString("  </oneText>\n");
    }
    serverFP << QString("</newTextVector>\n");
}

void DeviceManager::sendNewNumber(INDI_P *pp)
{
    INDI_E *lp;

    QTextStream serverFP(&serverSocket);

    serverFP << QString("<newNumberVector\n");
    serverFP << QString("  device='%1'\n").arg(qPrintable(pp->pg->dp->name));
    serverFP << QString("  name='%1'\n>").arg(qPrintable(pp->name));

    foreach (lp, pp->el)
    {
        serverFP << QString("  <oneNumber\n");
        serverFP << QString("    name='%1'>\n").arg(qPrintable(lp->name));
        if (lp->text.isEmpty() || lp->spin_w)
            serverFP << QString("      %1\n").arg(lp->targetValue);
        else
            serverFP << QString("      %1\n").arg(lp->text);
        serverFP << QString("  </oneNumber>\n");
    }
    serverFP << QString("</newNumberVector>\n");
}

void DeviceManager::sendNewSwitch(INDI_P *pp, INDI_E *lp)
{
    QTextStream serverFP(&serverSocket);

    serverFP << QString("<newSwitchVector\n");
    serverFP << QString("  device='%1'\n").arg(qPrintable(pp->pg->dp->name));
    serverFP << QString("  name='%1'>\n").arg(qPrintable(pp->name));
    serverFP << QString("  <oneSwitch\n");
    serverFP << QString("    name='%1'>\n").arg(qPrintable(lp->name));
    serverFP << QString("      %1\n").arg(lp->switch_state == ISS_ON ? "On" : "Off");
    serverFP << QString("  </oneSwitch>\n");

    serverFP << QString("</newSwitchVector>\n");
}

void DeviceManager::startBlob(const QString &devName, const QString &propName, const QString &timestamp)
{
    QTextStream serverFP(&serverSocket);

    serverFP << QString("<newBLOBVector\n");
    serverFP << QString("  device='%1'\n").arg(qPrintable(devName));
    serverFP << QString("  name='%1'\n").arg(qPrintable(propName));
    serverFP << QString("  timestamp='%1'>\n").arg(qPrintable(timestamp));
}

void DeviceManager::sendOneBlob(const QString &blobName, unsigned int blobSize, const QString &blobFormat,
                                unsigned char *blobBuffer)
{
    QTextStream serverFP(&serverSocket);

    serverFP << QString("  <oneBLOB\n");
    serverFP << QString("    name='%1'\n").arg(qPrintable(blobName));
    serverFP << QString("    size='%1'\n").arg(blobSize);
    serverFP << QString("    format='%1'>\n").arg(qPrintable(blobFormat));

    for (unsigned i = 0; i < blobSize; i += 72)
        serverFP << QString().sprintf("    %.72s\n", blobBuffer + i);

    serverFP << QString("   </oneBLOB>\n");
}

void DeviceManager::finishBlob()
{
    QTextStream serverFP(&serverSocket);

    serverFP << QString("</newBLOBVector>\n");
}

#include "devicemanager.moc"
