text: /*
    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   "S"
  #define KD4GroupIntensity "I"
  #define KD4GroupPanTilt   "P"
  #define KD4GroupColour    "C"
  #define KD4GroupGobo      "G"
  #define KD4GroupBeam      "B"
  #define KD4GroupEffect    "E"
  
  // Channels
  #define KD4TagFixture   "Fixture"
  #define KD4TagName      "Name"
  #define KD4TagShortName "ShortName"
  #define KD4TagCompany   "Company"
  #define KD4TagControl   "Control"
  #define KD4TagID        "ID"
  #define KD4TagGroup     "Group"
  #define KD4TagSpeed     "Speed"
  #define KD4TagMacro     "Macro"
  #define KD4TagReserved  "Reserved"
  #define KD4TagShutter   "Shutter"
  #define KD4TagPan       "Pan"
  #define KD4TagTilt      "Tilt"
  #define KD4TagCyan      "Cyan"
  #define KD4TagMagenta   "Magenta"
  #define KD4TagYellow    "Yellow"
  #define KD4TagRed       "Red"
  #define KD4TagBlue      "Blue"
  #define KD4TagGreen     "Green"
  #define KD4TagPrism     "Prism"
  #define KD4TagEffect    "Effect"
  #define KD4TagAttribute "Attribute"
  #define KD4TagUpdate    "Update"
  
  // Capabilities
  #define KD4TagFunction                  "Function"
  #define KD4TagFunctionName              "Name"
  #define KD4TagFunctionDmx               "Dmx"
  #define KD4TagFunctionDmxValueSeparator '~'
  
  // Mode section
  #define KD4TagMode                 "Mode"
  #define KD4TagModeName             "Name"
  #define KD4TagModeInclude          "Include"
  #define KD4TagModeAttribute        "Attribute"
  #define KD4TagModeChannelOffset    "ChannelOffset"
  #define KD4TagModeID               "ID"
  #define KD4TagModeChannelSeparator ','
  
  // Palettes section
  #define KD4TagPalettes          "Palettes"
  #define KD4TagPalette           "Palette"
  
  // Physical section
  #define KD4TagPhysical                     "Physical"
  #define KD4TagPhysicalBulb                 "Bulb"
  #define KD4TagPhysicalBulbType             "Type"
  #define KD4TagPhysicalBulbLumens           "Lumens"
  #define KD4TagPhysicalBulbColourTemp       "ColourTemp"
  #define KD4TagPhysicalLens                 "Lens"
  #define KD4TagPhysicalLensName             "Name"
  #define KD4TagPhysicalLensDegrees          "Degrees"
  #define KD4TagPhysicalLensDegreesSeparator '~'
  #define KD4TagPhysicalWeight               "Weight"
  #define KD4TagPhysicalWeightKg             "Kg"
  #define KD4TagPhysicalSize                 "Size"
  #define KD4TagPhysicalSizeHeight           "Height"
  #define KD4TagPhysicalSizeWidth            "Width"
  #define KD4TagPhysicalSizeDepth            "Depth"
  #define KD4TagPhysicalFocus                "Focus"
  #define KD4TagPhysicalFocusType            "Type"
  #define KD4TagPhysicalFocusPanMax          "PanMax"
  #define KD4TagPhysicalFocusTiltMax         "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(QString ID, QString name, QString group)
  {
      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(QString ID, QString name, QString group)
  {
      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(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(QString dmx, QString name, bool isFine)
  {
      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;
      }
  
      if (isFine)
          name += " Fine";
  
      QLCCapability *cap = new QLCCapability(minValue, maxValue, 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)
  {
      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)
  {
      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)
  {
      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)
  {
      // 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(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 (nocol > 0)
          return QLCFixtureDef::Dimmer; // Kinda..mmmmh..
      else
          return QLCFixtureDef::Other; // Give up
  }
  