/*
  Q Light Controller Plus
  artnetcontroller.cpp

  Copyright (c) Massimo Callegari

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0.txt

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
*/

#include "artnetcontroller.h"

#include <QMutexLocker>
#include <QStringList>
#include <QDebug>

#define TRANSMIT_FULL    "Full"
#define TRANSMIT_PARTIAL "Partial"

#define _DEBUG_RECEIVED_PACKETS 0

ArtNetController::ArtNetController(QNetworkInterface const& interface, QNetworkAddressEntry const& address,
                                   QSharedPointer<QUdpSocket> const& udpSocket,
                                   quint32 line, QObject *parent)
    : QObject(parent)
    , m_interface(interface)
    , m_address(address)
    , m_ipAddr(address.ip())
    , m_packetSent(0)
    , m_packetReceived(0)
    , m_line(line)
    , m_udpSocket(udpSocket)
    , m_packetizer(new ArtNetPacketizer())
    , m_pollTimer(NULL)
{
    if (m_ipAddr == QHostAddress::LocalHost)
    {
        m_broadcastAddr = QHostAddress::LocalHost;
        m_MACAddress = "11:22:33:44:55:66";
    }
    else
    {
        m_broadcastAddr = address.broadcast();
        m_MACAddress = interface.hardwareAddress();
    }

    qDebug() << "[ArtNetController] IP Address:" << m_ipAddr.toString() << " Broadcast address:" << m_broadcastAddr.toString() << "(MAC:" << m_MACAddress << ")";
}

ArtNetController::~ArtNetController()
{
    qDebug() << Q_FUNC_INFO;
    qDeleteAll(m_dmxValuesMap);
}

ArtNetController::Type ArtNetController::type()
{
    int type = Unknown;
    foreach(UniverseInfo info, m_universeMap.values())
    {
        type |= info.type;
    }

    return Type(type);
}

quint32 ArtNetController::line()
{
    return m_line;
}

quint64 ArtNetController::getPacketSentNumber()
{
    return m_packetSent;
}

quint64 ArtNetController::getPacketReceivedNumber()
{
    return m_packetReceived;
}

bool ArtNetController::socketBound() const
{
    return m_udpSocket->state() == QAbstractSocket::BoundState;
}

QString ArtNetController::getNetworkIP()
{
    return m_ipAddr.toString();
}

QHash<QHostAddress, ArtNetNodeInfo> ArtNetController::getNodesList()
{
    return m_nodesList;
}

void ArtNetController::addUniverse(quint32 universe, ArtNetController::Type type)
{
    qDebug() << "[ArtNet] addUniverse - universe" << universe << ", type" << type;
    if (m_universeMap.contains(universe))
    {
        m_universeMap[universe].type |= (int)type;
    }
    else
    {
        UniverseInfo info;
        info.inputUniverse = universe;
        info.outputAddress = m_broadcastAddr;
        info.outputUniverse = universe;
        info.outputTransmissionMode = Full;
        info.type = type;
        m_universeMap[universe] = info;
    }

    // send Polls if we open an Output
    if (type == Output && m_pollTimer == NULL)
    {
        slotSendPoll();

        m_pollTimer = new QTimer(this);
        m_pollTimer->setInterval(5000);
        connect(m_pollTimer, SIGNAL(timeout()),
                this, SLOT(slotSendPoll()));
        m_pollTimer->start();
    }
}

void ArtNetController::removeUniverse(quint32 universe, ArtNetController::Type type)
{
    if (m_universeMap.contains(universe))
    {
        if (m_universeMap[universe].type == type)
            m_universeMap.take(universe);
        else
            m_universeMap[universe].type &= ~type;

        if (type == Output && ((this->type() & Output) == 0))
        {
            disconnect(m_pollTimer, SIGNAL(timeout()),
                       this, SLOT(slotSendPoll()));
            delete m_pollTimer;
            m_pollTimer = NULL;
        }
    }
}

bool ArtNetController::setInputUniverse(quint32 universe, quint32 artnetUni)
{
    if (!m_universeMap.contains(universe))
        return false;

    QMutexLocker locker(&m_dataMutex);
    m_universeMap[universe].inputUniverse = artnetUni;

    return universe == artnetUni;
}

bool ArtNetController::setOutputIPAddress(quint32 universe, QString address)
{
    if (!m_universeMap.contains(universe))
        return false;

    if (address.size() == 0)
    {
        m_universeMap[universe].outputAddress = m_broadcastAddr;
        return true;
    }

    QMutexLocker locker(&m_dataMutex);

    QHostAddress hostAddress(address);
    if (hostAddress.isNull() || !address.contains("."))
    {
        // IP addresses are now always fully saved
        qDebug() << "[setOutputIPAddress] Legacy IP style detected:" << address;
        QStringList iFaceIP = m_ipAddr.toString().split(".");
        QStringList addList = address.split(".");

        for (int i = 0; i < addList.count(); i++)
            iFaceIP.replace(4 - addList.count() + i , addList.at(i));

        QString newIP = iFaceIP.join(".");
        hostAddress = QHostAddress(newIP);
    }

    qDebug() << "[setOutputIPAddress] transmit to IP: " << hostAddress.toString();

    m_universeMap[universe].outputAddress = hostAddress;

    return hostAddress == m_broadcastAddr;
}

bool ArtNetController::setOutputUniverse(quint32 universe, quint32 artnetUni)
{
    if (!m_universeMap.contains(universe))
        return false;

    QMutexLocker locker(&m_dataMutex);
    m_universeMap[universe].outputUniverse = artnetUni;

    return universe == artnetUni;
}

bool ArtNetController::setTransmissionMode(quint32 universe, ArtNetController::TransmissionMode mode)
{
    if (!m_universeMap.contains(universe))
        return false;

    QMutexLocker locker(&m_dataMutex);
    m_universeMap[universe].outputTransmissionMode = int(mode);

    return mode == ArtNetController::Full;
}

QString ArtNetController::transmissionModeToString(ArtNetController::TransmissionMode mode)
{
    switch (mode)
    {
        default:
        case Full:
            return QString(TRANSMIT_FULL);
        break;
        case Partial:
            return QString(TRANSMIT_PARTIAL);
        break;
    }
}

ArtNetController::TransmissionMode ArtNetController::stringToTransmissionMode(const QString &mode)
{
    if (mode == QString(TRANSMIT_PARTIAL))
        return Partial;
    else
        return Full;
}

QList<quint32> ArtNetController::universesList()
{
    return m_universeMap.keys();
}

UniverseInfo *ArtNetController::getUniverseInfo(quint32 universe)
{
    if (m_universeMap.contains(universe))
        return &m_universeMap[universe];

    return NULL;
}

void ArtNetController::sendDmx(const quint32 universe, const QByteArray &data)
{
    QMutexLocker locker(&m_dataMutex);
    QByteArray dmxPacket;
    QHostAddress outAddress = m_broadcastAddr;
    quint32 outUniverse = universe;
    TransmissionMode transmitMode = Full;

    if (m_universeMap.contains(universe))
    {
        UniverseInfo info = m_universeMap[universe];
        outAddress = info.outputAddress;
        outUniverse = info.outputUniverse;
        transmitMode = TransmissionMode(info.outputTransmissionMode);
    }

    if (transmitMode == Full)
    {
        QByteArray wholeuniverse(512, 0);
        wholeuniverse.replace(0, data.length(), data);
        m_packetizer->setupArtNetDmx(dmxPacket, outUniverse, wholeuniverse);
    }
    else
        m_packetizer->setupArtNetDmx(dmxPacket, outUniverse, data);

    qint64 sent = m_udpSocket->writeDatagram(dmxPacket, outAddress, ARTNET_PORT);
    if (sent < 0)
    {
        qWarning() << "sendDmx failed";
        qWarning() << "Errno: " << m_udpSocket->error();
        qWarning() << "Errmgs: " << m_udpSocket->errorString();
    }
    else
        m_packetSent++;
}

bool ArtNetController::handleArtNetPollReply(QByteArray const& datagram, QHostAddress const& senderAddress)
{
    ArtNetNodeInfo newNode;
    if (!m_packetizer->fillArtPollReplyInfo(datagram, newNode))
    {
        qWarning() << "[ArtNet] Bad ArtPollReply received";
        return false;
    }

#if _DEBUG_RECEIVED_PACKETS
    qDebug() << "[ArtNet] ArtPollReply received";
#endif

    if (m_nodesList.contains(senderAddress) == false)
        m_nodesList[senderAddress] = newNode;
    ++m_packetReceived;
    return true;
}

bool ArtNetController::handleArtNetPoll(QByteArray const& datagram, QHostAddress const& senderAddress)
{
    Q_UNUSED(datagram);

#if _DEBUG_RECEIVED_PACKETS
    qDebug() << "[ArtNet] ArtPoll received";
#endif
    QByteArray pollReplyPacket;
    m_packetizer->setupArtNetPollReply(pollReplyPacket, m_ipAddr, m_MACAddress);
    m_udpSocket->writeDatagram(pollReplyPacket, senderAddress, ARTNET_PORT);
    ++m_packetSent;
    ++m_packetReceived;
    return true;
}

bool ArtNetController::handleArtNetDmx(QByteArray const& datagram, QHostAddress const& senderAddress)
{
    Q_UNUSED(senderAddress);

    QByteArray dmxData;
    quint32 artnetUniverse;
    if (!m_packetizer->fillDMXdata(datagram, dmxData, artnetUniverse))
    {
        qWarning() << "[ArtNet] Bad DMX packet received";
        return false;
    }

#if _DEBUG_RECEIVED_PACKETS
    qDebug() << "[ArtNet] DMX data received. Universe:" << artnetUniverse << ", Data size:" << dmxData.size()
        << ", data[0]=" << (int)dmxData[0]
        << ", from=" << senderAddress.toString();
#endif

    for (QMap<quint32, UniverseInfo>::iterator it = m_universeMap.begin(); it != m_universeMap.end(); ++it)
    {
        quint32 universe = it.key();
        UniverseInfo const& info = it.value();

        if ((info.type & Input) && info.inputUniverse == artnetUniverse)
        {
            QByteArray *dmxValues;
            if (m_dmxValuesMap.contains(universe) == false)
                m_dmxValuesMap[universe] = new QByteArray(512, 0);
            dmxValues = m_dmxValuesMap[universe];

#if _DEBUG_RECEIVED_PACKETS
            qDebug() << "[ArtNet] -> universe" << (universe + 1);
#endif

            for (int i = 0; i < dmxData.length(); i++)
            {
                if (dmxValues->at(i) != dmxData.at(i))
                {
#if _DEBUG_RECEIVED_PACKETS
                    qDebug() << "[ArtNet] a value differs";
#endif
                    dmxValues->replace(i, 1, (const char *)(dmxData.data() + i), 1);
                    emit valueChanged(universe, m_line, i, (uchar)dmxData.at(i));
                }
            }
            ++m_packetReceived;
            return true;
        }
    }
    return false;
}

bool ArtNetController::handlePacket(QByteArray const& datagram, QHostAddress const& senderAddress)
{
#if _DEBUG_RECEIVED_PACKETS
    qDebug() << "Received packet with size: " << datagram.size() << ", host: " << senderAddress.toString();
#endif
    int opCode = -1;
    if (m_packetizer->checkPacketAndCode(datagram, opCode) == true)
    {
        switch (opCode)
        {
            case ARTNET_POLLREPLY:
                return handleArtNetPollReply(datagram, senderAddress);
            case ARTNET_POLL:
                return handleArtNetPoll(datagram, senderAddress);
            case ARTNET_DMX:
                return handleArtNetDmx(datagram, senderAddress);
            default:
                qDebug() << "[ArtNet] opCode not supported yet (" << opCode << ")";
                break;
        }
    }
    else
        qWarning() << "[ArtNet] Malformed packet received";

    return true;
}

void ArtNetController::slotSendPoll()
{
#if 0
    QList<QHostAddress>addressList;

    /* first, retrieve a list of unique output addresses */
    foreach(quint32 universe, universesList())
    {
        UniverseInfo info = m_universeMap[universe];
        if (addressList.contains(info.outputAddress) == false)
            addressList.append(info.outputAddress);
    }

    /* then send a poll to each address collected */
    foreach (QHostAddress addr, addressList)
    {
        QByteArray pollPacket;
        m_packetizer->setupArtNetPoll(pollPacket);
        qint64 sent = m_udpSocket->writeDatagram(pollPacket, addr, ARTNET_PORT);
        if (sent < 0)
            qWarning() << "Unable to send Poll packet: errno=" << m_udpSocket->error() << "(" << m_udpSocket->errorString() << ")";
        else
            m_packetSent++;
    }
#else
    QByteArray pollPacket;
    m_packetizer->setupArtNetPoll(pollPacket);
    qint64 sent = m_udpSocket->writeDatagram(pollPacket, m_broadcastAddr, ARTNET_PORT);
    if (sent < 0)
        qWarning() << "Unable to send Poll packet: errno=" << m_udpSocket->error() << "(" << m_udpSocket->errorString() << ")";
    else
        m_packetSent++;
#endif
}
