/**
 ******************************************************************************
 *
 * @file       uavobjectparser.cpp
 * @author     The OpenPilot Team, http://www.openpilot.org Copyright (C) 2010.
 * @brief      Parses XML files and extracts object information.
 *
 * @see        The GNU Public License (GPL) Version 3
 *
 *****************************************************************************/
/*
 * This program 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.
 *
 * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */

#include "uavobjectparser.h"

/**
 * Constructor
 */
UAVObjectParser::UAVObjectParser()
{
    fieldTypeStrXML << "int8" << "int16" << "int32" << "uint8"
        << "uint16" << "uint32" <<"float" << "enum";

    updateModeStrXML << "manual" << "periodic" << "onchange" << "throttled";

    accessModeStr << "ACCESS_READWRITE" << "ACCESS_READONLY";

    fieldTypeNumBytes << int(1) << int(2) << int(4) <<
                        int(1) << int(2) << int(4) <<
                        int(4) << int(1);

    accessModeStrXML << "readwrite" << "readonly";

}

/**
 * Get number of objects
 */
int UAVObjectParser::getNumObjects()
{
    return objInfo.length();
}

/**
 * Get the detailed object information
 */
QList<ObjectInfo*> UAVObjectParser::getObjectInfo()
{
    return objInfo;
}

ObjectInfo* UAVObjectParser::getObjectByIndex(int objIndex)
{
    return objInfo[objIndex];
}

/**
 * Get the name of the object
 */
QString UAVObjectParser::getObjectName(int objIndex)
{
    ObjectInfo* info = objInfo[objIndex];
    if (info == NULL)
        return QString();

    return info->name;
}

/**
 * Get the ID of the object
 */
quint32 UAVObjectParser::getObjectID(int objIndex)
{
    ObjectInfo* info = objInfo[objIndex];
    if (info == NULL)
        return 0;
    return info->id;
}

/**
 * Get the number of bytes in the data fields of this object
 */
int UAVObjectParser::getNumBytes(int objIndex)
{
    ObjectInfo* info = objInfo[objIndex];
    if (info == NULL)
    {
        return 0;
    }
    else
    {
        int numBytes = 0;
        for (int n = 0; n < info->fields.length(); ++n)
        {
            numBytes += info->fields[n]->numBytes * info->fields[n]->numElements;
        }
        return numBytes;
    }
}

bool fieldTypeLessThan(const FieldInfo* f1, const FieldInfo* f2)
{
    return f1->numBytes > f2->numBytes;
}

/**
 * Parse supplied XML file
 * @param xml The xml text
 * @param filename The xml filename
 * @returns Null QString() on success, error message on failure
 */
QString UAVObjectParser::parseXML(QString& xml, QString& filename)
{
    // Create DOM document and parse it
    QDomDocument doc("UAVObjects");
    bool parsed = doc.setContent(xml);
    if (!parsed) return QString("Improperly formated XML file");

    // Read all objects contained in the XML file, creating an new ObjectInfo for each
    QDomElement docElement = doc.documentElement();
    QDomNode node = docElement.firstChild();
    while ( !node.isNull() ) {
        // Create new object entry
        ObjectInfo* info = new ObjectInfo;

        info->filename=filename;
        // Process object attributes
        QString status = processObjectAttributes(node, info);
        if (!status.isNull())
            return status;

        // Process child elements (fields and metadata)
        QDomNode childNode = node.firstChild();
        bool fieldFound = false;
        bool accessFound = false;
        bool telGCSFound = false;
        bool telFlightFound = false;
        bool logFound = false;
        bool descriptionFound = false;
        while ( !childNode.isNull() ) {
            // Process element depending on its type
            if ( childNode.nodeName().compare(QString("field")) == 0 ) {
                QString status = processObjectFields(childNode, info);
                if (!status.isNull())
                    return status;

                fieldFound = true;
            }
            else if ( childNode.nodeName().compare(QString("access")) == 0 ) {
                QString status = processObjectAccess(childNode, info);
                if (!status.isNull())
                    return status;

                accessFound = true;
            }
            else if ( childNode.nodeName().compare(QString("telemetrygcs")) == 0 ) {
                QString status = processObjectMetadata(childNode, &info->gcsTelemetryUpdateMode,
                                                       &info->gcsTelemetryUpdatePeriod, &info->gcsTelemetryAcked);
                if (!status.isNull())
                    return status;

                telGCSFound = true;
            }
            else if ( childNode.nodeName().compare(QString("telemetryflight")) == 0 ) {
                QString status = processObjectMetadata(childNode, &info->flightTelemetryUpdateMode,
                                                       &info->flightTelemetryUpdatePeriod, &info->flightTelemetryAcked);
                if (!status.isNull())
                    return status;

                telFlightFound = true;
            }
            else if ( childNode.nodeName().compare(QString("logging")) == 0 ) {
                QString status = processObjectMetadata(childNode, &info->loggingUpdateMode,
                                                       &info->loggingUpdatePeriod, NULL);
                if (!status.isNull())
                    return status;

                logFound = true;
            }
            else if ( childNode.nodeName().compare(QString("description")) == 0 ) {
                QString status = processObjectDescription(childNode, &info->description);

                if (!status.isNull())
                    return status;

                descriptionFound = true;
            }
            else if (!childNode.isComment()) {
                return QString("Unknown object element");
            }

            // Get next element
            childNode = childNode.nextSibling();
        }
		
		// Sort all fields according to size
        qStableSort(info->fields.begin(), info->fields.end(), fieldTypeLessThan);

        // Sort all fields according to size
        qStableSort(info->fields.begin(), info->fields.end(), fieldTypeLessThan);

        // Make sure that required elements were found
        if ( !accessFound )
            return QString("Object::access element is missing");

        if ( !telGCSFound )
            return QString("Object::telemetrygcs element is missing");

        if ( !telFlightFound )
            return QString("Object::telemetryflight element is missing");

        if ( !logFound )
            return QString("Object::logging element is missing");

        // TODO: Make into error once all objects updated
        if ( !descriptionFound )
            return QString("Object::description element is missing");

        // Calculate ID
        calculateID(info);

        // Add object
        objInfo.append(info);

        // Get next object
        node = node.nextSibling();
    }

    all_units.removeDuplicates();
    // Done, return null string
    return QString();
}

/**
 * Calculate the unique object ID based on the object information.
 * The ID will change if the object definition changes, this is intentional
 * and is used to avoid connecting objects with incompatible configurations.
 * The LSB is set to zero and is reserved for metadata
 */
void UAVObjectParser::calculateID(ObjectInfo* info)
{
    // Hash object name
    quint32 hash = updateHash(info->name, 0);
    // Hash object attributes
    hash = updateHash(info->isSettings, hash);
    hash = updateHash(info->isSingleInst, hash);
    // Hash field information
    for (int n = 0; n < info->fields.length(); ++n) {
        hash = updateHash(info->fields[n]->name, hash);
        hash = updateHash(info->fields[n]->numElements, hash);
        hash = updateHash(info->fields[n]->type, hash);
        if(info->fields[n]->type == FIELDTYPE_ENUM) {
            QStringList options = info->fields[n]->options;
            for (int m = 0; m < options.length(); m++)
                hash = updateHash(options[m], hash);
        }
    }
    // Done
    info->id = hash & 0xFFFFFFFE;
}

/**
 * Shift-Add-XOR hash implementation. LSB is set to zero, it is reserved
 * for the ID of the metaobject.
 *
 * http://eternallyconfuzzled.com/tuts/algorithms/jsw_tut_hashing.aspx
 */
quint32 UAVObjectParser::updateHash(quint32 value, quint32 hash)
{
    return (hash ^ ((hash<<5) + (hash>>2) + value));
}

/**
 * Update the hash given a string
 */
quint32 UAVObjectParser::updateHash(QString& value, quint32 hash)
{
    QByteArray bytes = value.toAscii();
    quint32 hashout = hash;
    for (int n = 0; n < bytes.length(); ++n)
        hashout = updateHash(bytes[n], hashout);

    return hashout;
}

/**
 * Process the metadata part of the XML
 */
QString UAVObjectParser::processObjectMetadata(QDomNode& childNode, UpdateMode* mode, int* period, bool* acked)
{
    // Get updatemode attribute
    QDomNamedNodeMap elemAttributes = childNode.attributes();
    QDomNode elemAttr = elemAttributes.namedItem("updatemode");
    if ( elemAttr.isNull() )
        return QString("Object:telemetrygcs:updatemode attribute is missing");

    int index = updateModeStrXML.indexOf( elemAttr.nodeValue() );

    if (index<0)
        return QString("Object:telemetrygcs:updatemode attribute value is invalid");

    *mode = (UpdateMode)index;

    // Get period attribute
    elemAttr = elemAttributes.namedItem("period");
    if ( elemAttr.isNull() )
        return QString("Object:telemetrygcs:period attribute is missing");

    *period = elemAttr.nodeValue().toInt();


    // Get acked attribute (only if acked parameter is not null, not applicable for logging metadata)
    if ( acked != NULL) {
        elemAttr = elemAttributes.namedItem("acked");
        if ( elemAttr.isNull())
            return QString("Object:telemetrygcs:acked attribute is missing");

        if ( elemAttr.nodeValue().compare(QString("true")) == 0 )
            *acked = true;
        else if ( elemAttr.nodeValue().compare(QString("false")) == 0 )
            *acked = false;
        else
            return QString("Object:telemetrygcs:acked attribute value is invalid");
    }
    // Done
    return QString();
}

/**
 * Process the object access tag of the XML
 */
QString UAVObjectParser::processObjectAccess(QDomNode& childNode, ObjectInfo* info)
{
    // Get gcs attribute
    QDomNamedNodeMap elemAttributes = childNode.attributes();
    QDomNode elemAttr = elemAttributes.namedItem("gcs");
    if ( elemAttr.isNull() )
        return QString("Object:access:gcs attribute is missing");

    int index = accessModeStrXML.indexOf( elemAttr.nodeValue() );
    if (index >= 0)
        info->gcsAccess = (AccessMode)index;
    else
        return QString("Object:access:gcs attribute value is invalid");

    // Get flight attribute
    elemAttr = elemAttributes.namedItem("flight");
    if ( elemAttr.isNull() )
        return QString("Object:access:flight attribute is missing");

    index = accessModeStrXML.indexOf( elemAttr.nodeValue() );
    if (index >= 0)
        info->flightAccess = (AccessMode)index;
    else
        return QString("Object:access:flight attribute value is invalid");

    // Done
    return QString();
}

/**
 * Process the object fields of the XML
 */
QString UAVObjectParser::processObjectFields(QDomNode& childNode, ObjectInfo* info)
{
    // Create field
    FieldInfo* field = new FieldInfo;
    // Get name attribute
    QDomNamedNodeMap elemAttributes = childNode.attributes();
    QDomNode elemAttr = elemAttributes.namedItem("name");
    if ( elemAttr.isNull() )
        return QString("Object:field:name attribute is missing");

    field->name = elemAttr.nodeValue();


    // Get units attribute
    elemAttr = elemAttributes.namedItem("units");
    if ( elemAttr.isNull() )
        return QString("Object:field:units attribute is missing");

    field->units = elemAttr.nodeValue();
    all_units << field->units;

    // Get type attribute
    elemAttr = elemAttributes.namedItem("type");
    if ( elemAttr.isNull() )
        return QString("Object:field:type attribute is missing");

    int index = fieldTypeStrXML.indexOf(elemAttr.nodeValue());
    if (index >= 0) {
        field->type = (FieldType)index;
        field->numBytes = fieldTypeNumBytes[index];
    }  
    else {
        return QString("Object:field:type attribute value is invalid");
    }

    // Get numelements or elementnames attribute
    elemAttr = elemAttributes.namedItem("elementnames");
    if ( !elemAttr.isNull() ) {
        // Get element names
        QStringList names = elemAttr.nodeValue().split(",", QString::SkipEmptyParts);
        for (int n = 0; n < names.length(); ++n)
            names[n] = names[n].trimmed();

        field->elementNames = names;
        field->numElements = names.length();
        field->defaultElementNames = false;
    }
    else {
        elemAttr = elemAttributes.namedItem("elements");
        if ( elemAttr.isNull() ) {
            return QString("Object:field:elements and Object:field:elementnames attribute is missing");
        }
        else {
            field->numElements = elemAttr.nodeValue().toInt();
            for (int n = 0; n < field->numElements; ++n)
                field->elementNames.append(QString("%1").arg(n));

            field->defaultElementNames = true;
        }
    }
    // Get options attribute (only if an enum type)
    if (field->type == FIELDTYPE_ENUM) {

        // Get options attribute
        elemAttr = elemAttributes.namedItem("options");
        if ( elemAttr.isNull() )
            return QString("Object:field:options attribute is missing");

        QStringList options = elemAttr.nodeValue().split(",", QString::SkipEmptyParts);
        for (int n = 0; n < options.length(); ++n)
            options[n] = options[n].trimmed();

        field->options = options;
    }

    // Get the default value attribute (required for settings objects, optional for the rest)
    elemAttr = elemAttributes.namedItem("defaultvalue");
    if ( elemAttr.isNull() ) {
        if ( info->isSettings )
            return QString("Object:field:defaultvalue attribute is missing (required for settings objects)");
        field->defaultValues = QStringList();
    }
    else  {
		QStringList defaults = elemAttr.nodeValue().split(",", QString::SkipEmptyParts);
		for (int n = 0; n < defaults.length(); ++n)
			defaults[n] = defaults[n].trimmed();

		if(defaults.length() != field->numElements) {
			if(defaults.length() != 1)
				return QString("Object:field:incorrect number of default values");

			/*support legacy single default for multiple elements
			We sould really issue a warning*/
			for(int ct=1; ct< field->numElements; ct++)
				defaults.append(defaults[0]);
		}
		field->defaultValues = defaults;
    }
    elemAttr = elemAttributes.namedItem("limits");
    if ( elemAttr.isNull() ) {
        field->limitValues=QString();
    }
    else{
        field->limitValues=elemAttr.nodeValue();
    }
    // Add field to object
    info->fields.append(field);
    // Done
    return QString();
}

/**
 * Process the object attributes from the XML
 */
QString UAVObjectParser::processObjectAttributes(QDomNode& node, ObjectInfo* info)
{
    // Get name attribute
    QDomNamedNodeMap attributes = node.attributes();
    QDomNode attr = attributes.namedItem("name");
    if ( attr.isNull() )
        return QString("Object:name attribute is missing");

    info->name = attr.nodeValue();
    info->namelc = attr.nodeValue().toLower();

    // Get category attribute if present
    attr = attributes.namedItem("category");
    if ( !attr.isNull() )
    {
        info->category = attr.nodeValue();
    }

    // Get singleinstance attribute
    attr = attributes.namedItem("singleinstance");
    if ( attr.isNull() )
        return QString("Object:singleinstance attribute is missing");

    if ( attr.nodeValue().compare(QString("true")) == 0 )
        info->isSingleInst = true;
    else if ( attr.nodeValue().compare(QString("false")) == 0 )
        info->isSingleInst = false;
    else
        return QString("Object:singleinstance attribute value is invalid");

    // Get settings attribute
    attr = attributes.namedItem("settings");
    if ( attr.isNull() )
        return QString("Object:settings attribute is missing");

    if ( attr.nodeValue().compare(QString("true")) == 0 )
            info->isSettings = true;
    else if ( attr.nodeValue().compare(QString("false")) == 0 )
        info->isSettings = false;
    else
        return QString("Object:settings attribute value is invalid");


    // Settings objects can only have a single instance
    if ( info->isSettings && !info->isSingleInst )
        return QString("Object: Settings objects can not have multiple instances");

    // Done
    return QString();
}

/**
 * Process the description field from the XML file
 */
QString UAVObjectParser::processObjectDescription(QDomNode& childNode, QString * description)
{
    description->append(childNode.firstChild().nodeValue());
    return QString();
}