/*
  Q Light Controller Plus
  avolitesd4parser.cpp

  Copyright (C) Rui Barreiros
                Heikki Junnila
                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 <QXmlStreamReader>
#include <QStringList>
#include <QDebug>

#include "avolitesd4parser.h"
#include "qlcfixturemode.h"
#include "qlcfixturedef.h"
#include "qlccapability.h"
#include "qlcphysical.h"
#include "qlcchannel.h"
#include "qlcfile.h"

// Channel groups
#define KD4GroupSpecial   QStringLiteral("S")
#define KD4GroupIntensity QStringLiteral("I")
#define KD4GroupPanTilt   QStringLiteral("P")
#define KD4GroupColour    QStringLiteral("C")
#define KD4GroupGobo      QStringLiteral("G")
#define KD4GroupBeam      QStringLiteral("B")
#define KD4GroupEffect    QStringLiteral("E")

// Channels
#define KD4TagFixture   QStringLiteral("Fixture")
#define KD4TagName      QStringLiteral("Name")
#define KD4TagShortName QStringLiteral("ShortName")
#define KD4TagCompany   QStringLiteral("Company")
#define KD4TagControl   QStringLiteral("Control")
#define KD4TagID        QStringLiteral("ID")
#define KD4TagGroup     QStringLiteral("Group")
#define KD4TagSpeed     QStringLiteral("Speed")
#define KD4TagMacro     QStringLiteral("Macro")
#define KD4TagReserved  QStringLiteral("Reserved")
#define KD4TagShutter   QStringLiteral("Shutter")
#define KD4TagPan       QStringLiteral("Pan")
#define KD4TagTilt      QStringLiteral("Tilt")
#define KD4TagCyan      QStringLiteral("Cyan")
#define KD4TagMagenta   QStringLiteral("Magenta")
#define KD4TagYellow    QStringLiteral("Yellow")
#define KD4TagRed       QStringLiteral("Red")
#define KD4TagBlue      QStringLiteral("Blue")
#define KD4TagGreen     QStringLiteral("Green")
#define KD4TagPrism     QStringLiteral("Prism")
#define KD4TagEffect    QStringLiteral("Effect")
#define KD4TagAttribute QStringLiteral("Attribute")
#define KD4TagUpdate    QStringLiteral("Update")

// Capabilities
#define KD4TagFunction                  QStringLiteral("Function")
#define KD4TagFunctionName              QStringLiteral("Name")
#define KD4TagFunctionDmx               QStringLiteral("Dmx")
#define KD4TagFunctionDmxValueSeparator QStringLiteral("~")

// Mode section
#define KD4TagMode                 QStringLiteral("Mode")
#define KD4TagModeName             QStringLiteral("Name")
#define KD4TagModeInclude          QStringLiteral("Include")
#define KD4TagModeAttribute        QStringLiteral("Attribute")
#define KD4TagModeChannelOffset    QStringLiteral("ChannelOffset")
#define KD4TagModeID               QStringLiteral("ID")
#define KD4TagModeChannelSeparator QStringLiteral(",")

// Palettes section
#define KD4TagPalettes          QStringLiteral("Palettes")
#define KD4TagPalette           QStringLiteral("Palette")

// Physical section
#define KD4TagPhysical                     QStringLiteral("Physical")
#define KD4TagPhysicalBulb                 QStringLiteral("Bulb")
#define KD4TagPhysicalBulbType             QStringLiteral("Type")
#define KD4TagPhysicalBulbLumens           QStringLiteral("Lumens")
#define KD4TagPhysicalBulbColourTemp       QStringLiteral("ColourTemp")
#define KD4TagPhysicalLens                 QStringLiteral("Lens")
#define KD4TagPhysicalLensName             QStringLiteral("Name")
#define KD4TagPhysicalLensDegrees          QStringLiteral("Degrees")
#define KD4TagPhysicalLensDegreesSeparator QStringLiteral("~")
#define KD4TagPhysicalWeight               QStringLiteral("Weight")
#define KD4TagPhysicalWeightKg             QStringLiteral("Kg")
#define KD4TagPhysicalSize                 QStringLiteral("Size")
#define KD4TagPhysicalSizeHeight           QStringLiteral("Height")
#define KD4TagPhysicalSizeWidth            QStringLiteral("Width")
#define KD4TagPhysicalSizeDepth            QStringLiteral("Depth")
#define KD4TagPhysicalFocus                QStringLiteral("Focus")
#define KD4TagPhysicalFocusType            QStringLiteral("Type")
#define KD4TagPhysicalFocusPanMax          QStringLiteral("PanMax")
#define KD4TagPhysicalFocusTiltMax         QStringLiteral("TiltMax")

// Static attibute map shared between instances of the parser, initialized only
// once per application.
AvolitesD4Parser::StringToEnumMap AvolitesD4Parser::s_attributesMap;

AvolitesD4Parser::AvolitesD4Parser()
{
    if (s_attributesMap.isEmpty() == true)
    {
        // Setup our attribute mapping map helper
        s_attributesMap.insert(KD4GroupSpecial, AvolitesD4Parser::SPECIAL);
        s_attributesMap.insert(KD4GroupIntensity, AvolitesD4Parser::INTENSITY);
        s_attributesMap.insert(KD4GroupPanTilt, AvolitesD4Parser::PANTILT);
        s_attributesMap.insert(KD4GroupColour, AvolitesD4Parser::COLOUR);
        s_attributesMap.insert(KD4GroupGobo, AvolitesD4Parser::GOBO);
        s_attributesMap.insert(KD4GroupBeam, AvolitesD4Parser::BEAM);
        s_attributesMap.insert(KD4GroupEffect, AvolitesD4Parser::EFFECT);
    }
}

AvolitesD4Parser::~AvolitesD4Parser()
{
}

bool AvolitesD4Parser::loadXML(const QString& path, QLCFixtureDef *fixtureDef)
{
    m_lastError = QString();
    m_channels.clear();

    if (path.isEmpty())
    {
        m_lastError = "filename not specified";
        return false;
    }

    QXmlStreamReader *doc = QLCFile::getXMLReader(path);
    if (doc == NULL || doc->device() == NULL || doc->hasError())
    {
        m_lastError = QString("Unable to read from %1").arg(path);
        return false;
    }

    // check if the document has <Fixture></Fixture> if not then it's not a valid file
    if (doc->readNextStartElement() == false || doc->name() != KD4TagFixture)
    {
        m_lastError = "wrong document format";
        return false;
    }

    QXmlStreamAttributes attrs = doc->attributes();
    if ((!attrs.hasAttribute(KD4TagName)) || (!attrs.hasAttribute(KD4TagCompany)))
    {
        m_lastError = "the document doesn't have the required attributes";
        return false;
    }

    fixtureDef->setManufacturer(doc->attributes().value(KD4TagCompany).toString());
    fixtureDef->setModel(doc->attributes().value(KD4TagName).toString());
    fixtureDef->setAuthor("Avolites");

    while (doc->readNextStartElement())
    {
        if (doc->name() == KD4TagControl)
        {
            // Parse a channel
            if (parseChannel(doc, fixtureDef) == false)
                return false;
        }
        else if (doc->name() == KD4TagMode)
        {
            // Parse mode tag
            parseMode(doc, fixtureDef);
        }
        else if (doc->name() == KD4TagPalettes)
        {
            // TODO TODO TODO
            // Maybe also import preset palettes and macros ?!?!?!?!
            /**
                Can't be done for now, as qxf files don't have any information on preset palettes or macros
                for fixtures, they are automatically generated on the main application maybe in future... **/
            doc->skipCurrentElement();
        }
        else
        {
            qWarning() << Q_FUNC_INFO << "Unknown D4 tag:" << doc->name().toString();
            doc->skipCurrentElement();
        }
    }

    fixtureDef->setType(guessType(fixtureDef));

    QLCFile::releaseXMLReader(doc);

    return true;
}

QString AvolitesD4Parser::lastError() const
{
    return m_lastError;
}

QLCChannel::Group AvolitesD4Parser::getGroup(const QString& ID, const QString& name, const QString& group) const
{
    if (name.isEmpty() && group.isEmpty())
        return QLCChannel::NoGroup;

    switch (stringToAttributeEnum(group))
    {
        case AvolitesD4Parser::SPECIAL:
            if (ID.contains(KD4TagSpeed, Qt::CaseInsensitive) ||
                name.contains(KD4TagSpeed, Qt::CaseInsensitive))
                    return QLCChannel::Speed;
            else if (ID.contains(KD4TagMacro, Qt::CaseInsensitive) ||
                     name.contains(KD4TagMacro, Qt::CaseInsensitive))
                        return QLCChannel::Effect;
            else if (ID.contains(KD4TagReserved, Qt::CaseInsensitive) ||
                     name.contains(KD4TagReserved, Qt::CaseInsensitive))
                        return QLCChannel::NoGroup;
            else
                return QLCChannel::Maintenance;
        break;

        default:
        case AvolitesD4Parser::INTENSITY:
            if (ID.contains(KD4TagShutter, Qt::CaseInsensitive) ||
                name.contains(KD4TagShutter, Qt::CaseInsensitive))
                    return QLCChannel::Shutter;
            else
                return QLCChannel::Intensity;
        break;

        case AvolitesD4Parser::PANTILT:
            if (ID.contains(KD4TagPan, Qt::CaseInsensitive) ||
                name.contains(KD4TagPan, Qt::CaseInsensitive))
                    return QLCChannel::Pan;
            else if (ID.contains(KD4TagTilt, Qt::CaseInsensitive) ||
                    name.contains(KD4TagTilt, Qt::CaseInsensitive))
                        return QLCChannel::Tilt;
            else
                return QLCChannel::NoGroup;
        break;

        case AvolitesD4Parser::COLOUR:
            if (ID.contains(KD4TagCyan, Qt::CaseInsensitive) ||
                name.contains(KD4TagCyan, Qt::CaseInsensitive))
                    return QLCChannel::Intensity;
            else if (ID.contains(KD4TagMagenta, Qt::CaseInsensitive) ||
                    name.contains(KD4TagMagenta, Qt::CaseInsensitive))
                        return QLCChannel::Intensity;
            else if (ID.contains(KD4TagYellow, Qt::CaseInsensitive) ||
                    name.contains(KD4TagYellow, Qt::CaseInsensitive))
                        return QLCChannel::Intensity;
            else if (ID.contains(KD4TagRed, Qt::CaseInsensitive) ||
                    name.contains(KD4TagRed, Qt::CaseInsensitive))
                        return QLCChannel::Intensity;
            else if (ID.contains(KD4TagGreen, Qt::CaseInsensitive) ||
                    name.contains(KD4TagGreen, Qt::CaseInsensitive))
                        return QLCChannel::Intensity;
            else if (ID.contains(KD4TagBlue, Qt::CaseInsensitive) ||
                    name.contains(KD4TagBlue, Qt::CaseInsensitive))
                        return QLCChannel::Intensity;
            else
                return QLCChannel::Colour;
        break;

        case AvolitesD4Parser::GOBO:
            return QLCChannel::Gobo;
        break;

        case AvolitesD4Parser::BEAM:
            return QLCChannel::Beam;
        break;

        case AvolitesD4Parser::EFFECT:
            if (ID.contains(KD4TagPrism, Qt::CaseInsensitive) ||
                name.contains(KD4TagPrism, Qt::CaseInsensitive))
                    return QLCChannel::Prism;
            else if (ID.contains(KD4TagEffect, Qt::CaseInsensitive) ||
                     name.contains(KD4TagEffect, Qt::CaseInsensitive))
                        return QLCChannel::Effect;
            else if (ID.contains(KD4TagMacro, Qt::CaseInsensitive) ||
                     name.contains(KD4TagMacro, Qt::CaseInsensitive))
                        return QLCChannel::Effect;
            else
                return QLCChannel::NoGroup;
        break;
    }

    return QLCChannel::NoGroup;
}

QLCChannel::PrimaryColour AvolitesD4Parser::getColour(const QString& ID, const QString& name, const QString& group) const
{
    if (group.compare(KD4GroupColour, Qt::CaseInsensitive) != 0)
        return QLCChannel::NoColour;

    if (ID.contains(KD4TagCyan, Qt::CaseInsensitive) ||
        name.contains(KD4TagCyan, Qt::CaseInsensitive))
            return QLCChannel::Cyan;
    else if (ID.contains(KD4TagMagenta, Qt::CaseInsensitive) ||
            name.contains(KD4TagMagenta, Qt::CaseInsensitive))
                return QLCChannel::Magenta;
    else if (ID.contains(KD4TagYellow, Qt::CaseInsensitive) ||
             name.contains(KD4TagYellow, Qt::CaseInsensitive))
                return QLCChannel::Yellow;
    else if (ID.contains(KD4TagRed, Qt::CaseInsensitive) ||
            name.contains(KD4TagRed, Qt::CaseInsensitive))
                return QLCChannel::Red;
    else if (ID.contains(KD4TagGreen, Qt::CaseInsensitive) ||
            name.contains(KD4TagGreen, Qt::CaseInsensitive))
                return QLCChannel::Green;
    else if (ID.contains(KD4TagBlue, Qt::CaseInsensitive) ||
            name.contains(KD4TagBlue, Qt::CaseInsensitive))
                return QLCChannel::Blue;
    else
        return QLCChannel::NoColour;
}
/*
bool AvolitesD4Parser::isFunction(const QDomElement& elem) const
{
    QDomElement el = elem.firstChildElement(KD4TagFunction);
    for (; !el.isNull(); el = el.nextSiblingElement(KD4TagFunction))
    {
        if (!el.attribute(KD4TagUpdate).isEmpty())
            return true;
    }

    return false;
}
*/
bool AvolitesD4Parser::is16Bit(const QString& dmx) const
{
    QStringList dmxValues = dmx.split(KD4TagFunctionDmxValueSeparator);

    if (dmxValues.isEmpty())
        return false;

    // I remember avolites sometimes switches the sides on dmx values, sometimes, the left of the ~
    // is not always the lowest, better check both sides

    if (dmxValues.value(0).toInt() > 256)
        return true;

    // Is there a right side ? (there should always be something in the right side of the ~,
    // or avolites desks won't parse the file, anyway, there should be a check, if some smart
    // dude will complain this crashes with his D4 file)

    if (dmxValues.size() > 1)
    {
        if (dmxValues.value(1).toInt() > 256)
            return true;
    }

    return false;
}

QLCCapability *AvolitesD4Parser::getCapability(const QString& dmx, const QString& name, bool isFine) const
{
    if (dmx.isEmpty())
        return NULL;

    QStringList dmxValues = dmx.split(KD4TagFunctionDmxValueSeparator);

    // Here, instead of checking all the time for both dmxValues, it's more efficient to
    // set a default value if it's missing
    if (dmxValues.size() == 0)
        dmxValues << QString("0") << QString("0");
    else if (dmxValues.size() == 1)
        dmxValues << QString("0");

    // if were trying to get capabilities from a 16bit channel, we need to change them to 8 bit
    int minValue = 0, maxValue = 0;

    if (dmxValues.value(0).toInt() > 256)
        minValue = 0xFF & (dmxValues.value(0).toInt() >> 8);
    else
        minValue = dmxValues.value(0).toInt();

    if (dmxValues.value(1).toInt() > 256)
        maxValue = 0xFF & (dmxValues.value(1).toInt() >> 8);
    else
        maxValue = dmxValues.value(1).toInt();

    // Guess what, I seen this happen, it seems min value is not always on the left of the ~
    // sometimes they're switched!
    if (minValue > maxValue)
    {
        int tmp = maxValue;
        maxValue = minValue;
        minValue = tmp;
    }

    QLCCapability *cap = new QLCCapability(minValue, maxValue, isFine ? (name + " Fine") : name);

    return cap;
}

bool AvolitesD4Parser::parseChannel(QXmlStreamReader *doc, QLCFixtureDef *fixtureDef)
{
    if (doc->name() != KD4TagControl)
        return false;

    while (doc->readNextStartElement())
    {
        if (doc->name() == KD4TagAttribute)
        {
            QString ID = doc->attributes().value(KD4TagID).toString();
            if (ID.isEmpty())
            {
                doc->skipCurrentElement();
                continue;
            }

            parseAttribute(doc, fixtureDef);
        }
        else
        {
            qWarning() << Q_FUNC_INFO << "Unknown control tag:" << doc->name().toString();
            doc->skipCurrentElement();
        }
    }

    return true;
}

bool AvolitesD4Parser::parseFunction(QXmlStreamReader *doc, QLCFixtureDef *fixtureDef,
                                     QLCChannel *channel, QString ID, QString group)
{
    QXmlStreamAttributes attrs = doc->attributes();
    QString name = attrs.value(KD4TagFunctionName).toString();
    if (name.isEmpty())
    {
        doc->skipCurrentElement();
        return true;
    }

    QString dmx = attrs.value(KD4TagFunctionDmx).toString();
    QLCCapability *cap = getCapability(dmx, name);

    if (cap != NULL)
    {
        // We just ignore capability adding errors, because avolites often repeats attributes due to conditionals
        // so we just add the first one we get, the repeating ones are ignored naturally and
        // obviously further human verification is needed on the fixture definition to fix this issues
        channel->addCapability(cap);
    }

    if (is16Bit(dmx))
    {
        QLCChannel *fineChan = new QLCChannel();
        fineChan->setName(name + " Fine");
        fineChan->setGroup(getGroup(ID, name, group));
        fineChan->setColour(getColour(ID, name, group));
        fineChan->setControlByte(QLCChannel::LSB);
        QLCCapability *fineCap = getCapability(dmx, name, true);
        if (fineCap != NULL)
            fineChan->addCapability(fineCap);
        fixtureDef->addChannel(fineChan);
        m_channels.insert(ID + " Fine", fineChan);
    }
    //qDebug() << "Capability found" << cap->name() << cap->min() << cap->max();
    doc->skipCurrentElement();

    return true;
}

bool AvolitesD4Parser::parseAttribute(QXmlStreamReader *doc, QLCFixtureDef *fixtureDef)
{
    if (doc->name() != KD4TagAttribute)
        return false;

    QXmlStreamAttributes attrs = doc->attributes();
    QString ID = doc->attributes().value(KD4TagID).toString();
    QString name = attrs.value(KD4TagName).toString();
    QString group = attrs.value(KD4TagGroup).toString();

    QLCChannel *chan = new QLCChannel();
    chan->setName(name);
    chan->setGroup(getGroup(ID, name, group));
    chan->setColour(getColour(ID, name, group));
    chan->setControlByte(QLCChannel::MSB);

    // add channel to fixture definition
    fixtureDef->addChannel(chan);
    m_channels.insert(ID, chan);

    // if this channel is a NoGroup then we don't need to continue
    // no capabilities nor 16 bit channel
    if (chan->group() == QLCChannel::NoGroup)
    {
        doc->skipCurrentElement();
        return true;
    }

    while (doc->readNextStartElement())
    {
        if (doc->name() == KD4TagFunction)
        {
            parseFunction(doc, fixtureDef, chan, ID, group);
        }
        else
        {
            qWarning() << Q_FUNC_INFO << "Unknown attribute tag:" << doc->name().toString();
            doc->skipCurrentElement();
        }
    }
    chan->sortCapabilities();

    return true;
}

bool AvolitesD4Parser::parseMode(QXmlStreamReader *doc, QLCFixtureDef *fixtureDef) const
{
    if (doc->name() != KD4TagMode)
        return false;

    QString name = doc->attributes().value(KD4TagModeName).toString();

    if (name.isEmpty())
        return false;

    QLCFixtureMode *mode = new QLCFixtureMode(fixtureDef);
    mode->setName(name);

    while (doc->readNextStartElement())
    {
        if (doc->name() == KD4TagModeInclude)
        {
            parseInclude(doc, mode);
        }
        else if (doc->name() == KD4TagPhysical)
        {
            // Parse physical
            parsePhysical(doc, fixtureDef, mode);
        }
        else
        {
            qWarning() << Q_FUNC_INFO << "Unknown mode tag:" << doc->name().toString();
            doc->skipCurrentElement();
        }
    }

    // Add the mode
    fixtureDef->addMode(mode);

    return true;
}

bool AvolitesD4Parser::comparePhysical(const QLCPhysical &globalPhy, const QLCPhysical &modePhy) const
{
    if (globalPhy.isEmpty())
        return true;

    if (globalPhy.bulbLumens() != modePhy.bulbLumens() ||
        globalPhy.bulbColourTemperature() != modePhy.bulbColourTemperature() ||
        globalPhy.weight() != modePhy.weight() ||
        globalPhy.width() != modePhy.width() ||
        globalPhy.height() != modePhy.height() ||
        globalPhy.depth() != modePhy.depth() ||
        globalPhy.lensDegreesMin() != modePhy.lensDegreesMin() ||
        globalPhy.lensDegreesMax() != modePhy.lensDegreesMax() ||
        globalPhy.focusPanMax() != modePhy.focusPanMax() ||
        globalPhy.focusTiltMax() != modePhy.focusTiltMax() ||
        globalPhy.powerConsumption() != modePhy.powerConsumption())
            return false;

    return true;
}

void AvolitesD4Parser::parsePhysical(QXmlStreamReader *doc, QLCFixtureDef *fixtureDef, QLCFixtureMode *mode) const
{
    if (doc->name() != KD4TagPhysical)
        return;

    QLCPhysical phys;

    while (doc->readNextStartElement())
    {
        QXmlStreamAttributes attrs = doc->attributes();

        if (doc->name() == KD4TagPhysicalBulb)
        {
            phys.setBulbType(attrs.value(KD4TagPhysicalBulbType).toString());
            phys.setBulbLumens(attrs.value(KD4TagPhysicalBulbLumens).toString().toInt());
            // this is kind of wrong cause a ColourTemp tag can be "0, 0, 0"
            phys.setBulbColourTemperature(attrs.value(KD4TagPhysicalBulbColourTemp).toString().toInt());
        }
        else if (doc->name() == KD4TagPhysicalLens)
        {
            phys.setLensName(attrs.value(KD4TagName).toString());

            QString degrees = attrs.value(KD4TagPhysicalLensDegrees).toString();
            if (degrees.contains(KD4TagPhysicalLensDegreesSeparator))
            {
                QStringList deg = degrees.split(KD4TagPhysicalLensDegreesSeparator);
                if (deg.size() == 2)
                {
                    if (deg.value(0).toInt() > deg.value(1).toInt())
                    {
                        phys.setLensDegreesMin(deg.value(1).toInt());
                        phys.setLensDegreesMax(deg.value(0).toInt());
                    }
                    else
                    {
                        phys.setLensDegreesMin(deg.value(0).toInt());
                        phys.setLensDegreesMax(deg.value(1).toInt());
                    }
                } else if (deg.size() == 1)
                {
                    phys.setLensDegreesMax(deg.value(0).toInt());
                    phys.setLensDegreesMin(deg.value(0).toInt());
                }
            }
            else if (!degrees.isEmpty())
            {
                phys.setLensDegreesMax(degrees.toInt());
                phys.setLensDegreesMin(degrees.toInt());
            }
        }
        else if (doc->name() == KD4TagPhysicalWeight)
        {
            phys.setWeight(attrs.value(KD4TagPhysicalWeightKg).toString().toDouble());
        }
        else if (doc->name() == KD4TagPhysicalSize)
        {
            phys.setHeight(attrs.value(KD4TagPhysicalSizeHeight).toString().toDouble() * 1000);
            phys.setWidth(attrs.value(KD4TagPhysicalSizeWidth).toString().toDouble() * 1000);
            phys.setDepth(attrs.value(KD4TagPhysicalSizeDepth).toString().toDouble() * 1000);
        }
        else if (doc->name() == KD4TagPhysicalFocus)
        {
            phys.setFocusType(attrs.value(KD4TagPhysicalFocusType).toString());
            phys.setFocusPanMax(attrs.value(KD4TagPhysicalFocusPanMax).toString().toInt());
            phys.setFocusTiltMax(attrs.value(KD4TagPhysicalFocusTiltMax).toString().toInt());
        }
        else
        {
            qWarning() << Q_FUNC_INFO << "Unknown physical tag:" << doc->name().toString();
        }
        doc->skipCurrentElement();
    }

    if (comparePhysical(fixtureDef->physical(), phys) == true)
        fixtureDef->setPhysical(phys);
    else
        mode->setPhysical(phys);
}

void AvolitesD4Parser::parseInclude(QXmlStreamReader *doc, QLCFixtureMode *mode) const
{
    if (doc->name() != KD4TagModeInclude)
        return;

    QMap <int, QLCChannel*> channelList;

    // loop through Attribute tags
    while (doc->readNextStartElement())
    {
        if (doc->name() == KD4TagModeAttribute)
        {
            QXmlStreamAttributes attrs = doc->attributes();
            // Some channels are conditionals not real channels
            if (attrs.value(KD4TagModeChannelOffset).toString().isEmpty())
            {
                doc->skipCurrentElement();
                continue;
            }

            QString modeID = attrs.value(KD4TagModeID).toString();
            if (m_channels.contains(modeID))
            {
                // might be a 16 bit channel, so we have 2 offsets
                QString offset = attrs.value(KD4TagModeChannelOffset).toString();
                if (offset.contains(KD4TagModeChannelSeparator, Qt::CaseInsensitive))
                {
                    // 16 bit address, we need to add 2 channels, this one, and we need the fine one
                    QStringList offsetValues = offset.split(KD4TagModeChannelSeparator);
                    // if there's more than 2 addresses, or less than 2, bail out, don't know how to handle this, shouldn't happen ever.
                    if (offsetValues.size() > 2 || offsetValues.size() < 2)
                        continue;

                    // Add this one
                    channelList.insert(offsetValues.value(0).toInt(), m_channels.value(modeID));
                    QString name = m_channels.value(modeID)->name();

                    // Search for the fine one
                    QMapIterator <QString,QLCChannel*> it(m_channels);
                    while (it.hasNext() == true)
                    {
                        it.next();
                        QLCChannel *ch(it.value());
                        Q_ASSERT(ch != NULL);

                        if (ch->name() == QString(name + " Fine"))
                            channelList.insert(offsetValues.value(1).toInt(), ch);
                    }
                }
                else
                {
                    channelList.insert(offset.toInt(), m_channels.value(modeID));
                }
            }
        }
        else
        {
            qWarning() << Q_FUNC_INFO << "Unknown include tag:" << doc->name().toString();
        }
        doc->skipCurrentElement();
    }

    QMapIterator <int,QLCChannel*> it(channelList);
    while (it.hasNext() == true)
    {
        it.next();
        Q_ASSERT(mode != NULL);
        mode->insertChannel(it.value(), it.key());
    }
}

AvolitesD4Parser::Attributes AvolitesD4Parser::stringToAttributeEnum(const QString& attr) const
{
    // If there is none, empty or whatever always return something, default is SPECIAL
    if (attr.isEmpty())
        return AvolitesD4Parser::SPECIAL;

    if (s_attributesMap.value(attr.toUpper()))
        return s_attributesMap.value(attr.toUpper());
    else
        return AvolitesD4Parser::SPECIAL;
}

QLCFixtureDef::FixtureType AvolitesD4Parser::guessType(const QLCFixtureDef *def) const
{
    Q_ASSERT(def != NULL);

    int pan = 0, tilt = 0;
    int r = 0, g = 0, b = 0, c = 0, m = 0, y = 0, nocol = 0;
    int gobo = 0, colour = 0;
    int haze = 0, smoke = 0;
    int strobe = 0;

    QListIterator <QLCChannel*> it(def->channels());
    while (it.hasNext() == true)
    {
        const QLCChannel *ch(it.next());
        if (ch->group() == QLCChannel::Pan)
        {
            pan++;
        }
        else if (ch->group() == QLCChannel::Tilt)
        {
            tilt++;
        }
        else if (ch->group() == QLCChannel::Intensity)
        {
            if (ch->colour() == QLCChannel::Red)
                r++;
            else if (ch->colour() == QLCChannel::Green)
                g++;
            else if (ch->colour() == QLCChannel::Blue)
                b++;
            else if (ch->colour() == QLCChannel::Cyan)
                c++;
            else if (ch->colour() == QLCChannel::Magenta)
                m++;
            else if (ch->colour() == QLCChannel::Yellow)
                y++;
            else
                nocol++;
        }
        else if (ch->group() == QLCChannel::Shutter)
        {
            if (ch->searchCapability(/*S/s*/"trobe", false) != NULL)
                strobe++;
        }
        else if (ch->group() == QLCChannel::Gobo)
        {
            gobo++;
        }
        else if (ch->group() == QLCChannel::Colour)
        {
            colour++;
        }
        else if (ch->name().contains("strobe", Qt::CaseInsensitive) == true)
        {
            strobe++;
        }
        else if (ch->name().contains("haze", Qt::CaseInsensitive) == true)
        {
            haze++;
        }
        else if (ch->name().contains("smoke", Qt::CaseInsensitive) == true)
        {
            smoke++;
        }
    }

    if (pan >= 2 && tilt >= 2)
        return QLCFixtureDef::MovingHead; // Quite probable, few scanners with 16bit addressing
    else if (pan == 1 && tilt == 1)
        return QLCFixtureDef::Scanner; // Quite probable, though some moving heads are only 8bit
    else if (gobo > 0)
        return QLCFixtureDef::Flower; // No pan/tilt, but gobo, fairly certain
    else if (colour > 0 || (r > 0 && g > 0 && b > 0) || (c > 0 && m > 0 && y > 0))
        return QLCFixtureDef::ColorChanger; // No pan/tilt/gobos, but RGB/CMY mixing or dichro
    else if (strobe > 0)
        return QLCFixtureDef::Strobe; // Duh.
    else if (smoke > 0)
        return QLCFixtureDef::Smoke; // Duh.
    else if (haze > 0)
        return QLCFixtureDef::Hazer; // Duh.
    else if (nocol > 0)
        return QLCFixtureDef::Dimmer; // Kinda..mmmmh..
    else
        return QLCFixtureDef::Other; // Give up
}
