/***************************************************************************
**
** Copyright (C) 2014 Klaralvdalens Datakonsult AB (KDAB)
** Contact: https://www.qt.io/licensing/
**
** This file is part of the utilities of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "xmlspecparser.h"

#include <QDebug>
#include <QFile>
#include <QRegularExpression>
#include <QStringList>
#include <QTextStream>
#include <QXmlStreamReader>

#ifdef SPECPARSER_DEBUG
#define qXmlSpecParserDebug qDebug
#else
#define qXmlSpecParserDebug QT_NO_QDEBUG_MACRO
#endif

bool XmlSpecParser::parse()
{
    // Open up a stream on the actual OpenGL function spec file
    QFile file(specFileName());
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        qWarning() << "Failed to open spec file:" << specFileName() << "Aborting";
        return false;
    }

    QXmlStreamReader stream(&file);

    // Extract the info that we need
    parseFunctions(stream);
    return true;
}

void XmlSpecParser::parseParam(QXmlStreamReader &stream, Function &func)
{
    Argument arg;
    arg.type = QString();

    while (!stream.isEndDocument()) {
        stream.readNext();

        if (stream.isStartElement()) {
            QString tag = stream.name().toString();

            if (tag == "ptype") {
                if (stream.readNext() == QXmlStreamReader::Characters)
                    arg.type.append(stream.text().toString());
            }

            else if (tag == "name") {
                if (stream.readNext() == QXmlStreamReader::Characters)
                    arg.name = stream.text().toString().trimmed();
            }
        } else if (stream.isCharacters()) {
            arg.type.append(stream.text().toString());
        } else if (stream.isEndElement()) {
            QString tag = stream.name().toString();

            if (tag == "param") {
                // compatibility with old spec
                QRegularExpression typeRegExp("(const )?(.+)(?<!\\*)((?:(?!\\*$)\\*)*)(\\*)?");

                // remove extra whitespace
                arg.type = arg.type.trimmed();

                // set default
                arg.direction = Argument::In;
                arg.mode = Argument::Value;

                QRegularExpressionMatch exp = typeRegExp.match(arg.type);
                if (exp.hasMatch()) {
                    if (!exp.captured(4).isEmpty()) {
                        arg.mode = Argument::Reference;

                        if (exp.captured(1).isEmpty())
                            arg.direction = Argument::Out;
                    }

                    arg.type = exp.captured(2) + exp.captured(3);
                }

                break;
            }
        }
    }

    // remove any excess whitespace
    arg.type = arg.type.trimmed();
    arg.name = arg.name.trimmed();

    // maybe some checks?
    func.arguments.append(arg);
}

void XmlSpecParser::parseCommand(QXmlStreamReader &stream)
{
    Function func;

    while (!stream.isEndDocument()) {
        stream.readNext();

        if (stream.isStartElement()) {
            QString tag = stream.name().toString();

            if (tag == "proto") {
                while (!stream.isEndDocument()) {
                    stream.readNext();
                    if (stream.isStartElement() && (stream.name().toString() == "name")) {
                        if (stream.readNext() == QXmlStreamReader::Characters)
                            func.name = stream.text().toString();
                    } else if (stream.isCharacters()) {
                        func.returnType.append(stream.text().toString());
                    } else if (stream.isEndElement() && (stream.name().toString() == "proto")) {
                        break;
                    }
                }
            }

            if (tag == "param")
                parseParam(stream, func);
        }

        else if (stream.isEndElement()) {
            QString tag = stream.name().toString();

            if (tag == "command")
                break;
        }
    }

    // maybe checks?
    func.returnType = func.returnType.trimmed();

    // for compatibility with old spec
    if (func.name.startsWith("gl"))
        func.name.remove(0, 2);

    m_functionList.insert(func.name, func);
}

void XmlSpecParser::parseCommands(QXmlStreamReader &stream)
{
    while (!stream.isEndDocument()) {
        stream.readNext();

        if (stream.isStartElement()) {
            QString tag = stream.name().toString();

            if (tag == "command")
                parseCommand(stream);
        }

        else if (stream.isEndElement()) {
            QString tag = stream.name().toString();

            if (tag == "commands")
                break;
        }
    }
}

void XmlSpecParser::parseRequire(QXmlStreamReader &stream, FunctionList &funcs)
{
    while (!stream.isEndDocument()) {
        stream.readNext();

        if (stream.isStartElement()) {
            QString tag = stream.name().toString();

            if (tag == "command") {
                QString func = stream.attributes().value("name").toString();

                // for compatibility with old spec
                if (func.startsWith("gl"))
                    func.remove(0, 2);

                funcs.append(m_functionList[func]);
            }
        } else if (stream.isEndElement()) {
            QString tag = stream.name().toString();

            if (tag == "require")
                break;
        }
    }
}

void XmlSpecParser::parseRemoveCore(QXmlStreamReader &stream)
{
    while (!stream.isEndDocument()) {
        stream.readNext();

        if (stream.isStartElement()) {
            QString tag = stream.name().toString();

            if (tag == "command") {
                QString func = stream.attributes().value("name").toString();

                // for compatibility with old spec
                if (func.startsWith("gl"))
                    func.remove(0, 2);

                // go through list of version and mark as incompatible
                Q_FOREACH (const Version& v, m_versions) {
                    // Combine version and profile for this subset of functions
                    VersionProfile version;
                    version.version = v;
                    version.profile = VersionProfile::CoreProfile;

                    // Fetch the functions and add to collection for this class
                    Q_FOREACH (const Function& f, m_functions.values(version)) {
                        if (f.name == func) {
                            VersionProfile newVersion;
                            newVersion.version = v;
                            newVersion.profile = VersionProfile::CompatibilityProfile;

                            m_functions.insert(newVersion, f);
                            m_functions.remove(version, f);
                        }
                    }
                }
            }
        } else if (stream.isEndElement()) {
            QString tag = stream.name().toString();

            if (tag == "remove")
                break;
        }
    }
}

void XmlSpecParser::parseFeature(QXmlStreamReader &stream)
{
    QRegularExpression versionRegExp("(\\d).(\\d)");
    QXmlStreamAttributes attributes = stream.attributes();

    QRegularExpressionMatch match = versionRegExp.match(attributes.value("number").toString());

    if (!match.hasMatch()) {
        qWarning() << "Malformed version indicator";
        return;
    }

    if (attributes.value("api").toString() != "gl")
        return;

    int majorVersion = match.captured(1).toInt();
    int minorVersion = match.captured(2).toInt();

    Version v;
    VersionProfile core, compat;

    v.major = majorVersion;
    v.minor = minorVersion;
    core.version = compat.version = v;
    core.profile = VersionProfile::CoreProfile;
    compat.profile = VersionProfile::CompatibilityProfile;

    while (!stream.isEndDocument()) {
        stream.readNext();

        if (stream.isStartElement()) {
            QString tag = stream.name().toString();

            if (tag == "require") {
                FunctionList funcs;

                if (stream.attributes().value("profile").toString() == "compatibility") {
                    parseRequire(stream, funcs);
                    Q_FOREACH (const Function& f, funcs) {
                        m_functions.insert(compat, f);
                    }
                } else {
                    parseRequire(stream, funcs);
                    Q_FOREACH (const Function& f, funcs) {
                        m_functions.insert(core, f);
                    }
                }
            } else if (tag == "remove") {
                if (stream.attributes().value("profile").toString() == "core")
                    parseRemoveCore(stream);
            }
        } else if (stream.isEndElement()) {
            QString tag = stream.name().toString();

            if (tag == "feature")
                break;
        }
    }

    m_versions.append(v);
}

void XmlSpecParser::parseExtension(QXmlStreamReader &stream)
{
    QXmlStreamAttributes attributes = stream.attributes();
    QString name = attributes.value("name").toString();

    while (!stream.isEndDocument()) {
        stream.readNext();

        if (stream.isStartElement()) {
            QString tag = stream.name().toString();

            if (tag == "require") {
                if (stream.attributes().value("profile").toString() == "compatibility") {
                    FunctionList funcs;
                    parseRequire(stream, funcs);

                    Q_FOREACH (const Function& f, funcs) {
                        FunctionProfile fp;
                        fp.function = f;
                        fp.profile = VersionProfile::CompatibilityProfile;
                        m_extensionFunctions.insert(name, fp);
                    }
                } else {
                    FunctionList funcs;
                    parseRequire(stream, funcs);
                    Q_FOREACH (const Function& f, funcs) {
                        FunctionProfile fp;
                        fp.function = f;
                        fp.profile = VersionProfile::CoreProfile;
                        m_extensionFunctions.insert(name, fp);
                    }
                }


            }
        } else if (stream.isEndElement()) {
            QString tag = stream.name().toString();

            if (tag == "extension")
                break;
        }
    }
}

void XmlSpecParser::parseFunctions(QXmlStreamReader &stream)
{
    while (!stream.isEndDocument()) {
        stream.readNext();

        if (stream.isStartElement()) {
            QString tag = stream.name().toString();

            if (tag == "feature") {
                parseFeature(stream);
            } else if (tag == "commands") {
                parseCommands(stream);
            } else if (tag == "extension") {
                parseExtension(stream);
            }
        } else if (stream.isEndElement()) {
            stream.readNext();
        }
    }

    // hack - add GL_ARB_imaging to every version after 1.2 inclusive
    Version versionThreshold;
    versionThreshold.major = 1;
    versionThreshold.minor = 2;
    QList<FunctionProfile> funcs = m_extensionFunctions.values("GL_ARB_imaging");

    VersionProfile vp;
    vp.version = versionThreshold;

    Q_FOREACH (const FunctionProfile& fp, funcs) {
        vp.profile = fp.profile;
        m_functions.insert(vp, fp.function);
    }

    // now we will prune any duplicates
    QSet<QString> funcset;

    Q_FOREACH (const Version& v, m_versions) {
        // check compatibility first
        VersionProfile vp;
        vp.version = v;

        vp.profile = VersionProfile::CompatibilityProfile;

        Q_FOREACH (const Function& f, m_functions.values(vp)) {
            // remove duplicate
            if (funcset.contains(f.name))
                m_functions.remove(vp, f);

            funcset.insert(f.name);
        }

        vp.profile = VersionProfile::CoreProfile;

        Q_FOREACH (const Function& f, m_functions.values(vp)) {

            // remove duplicate
            if (funcset.contains(f.name))
                m_functions.remove(vp, f);

            funcset.insert(f.name);
        }
    }
}
