/****************************************************************************
**
** Copyright (C) 2017 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the tools applications 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 <QtCore/qcoreapplication.h>
#include <QtCore/qfile.h>
#include <QtCore/qfileinfo.h>
#include <QtCore/qlist.h>
#include <QtCore/qxmlstream.h>

class VkSpecParser
{
public:
    bool parse();

    struct TypedName {
        QString name;
        QString type;
        QString typeSuffix;
    };

    struct Command {
        TypedName cmd;
        QList<TypedName> args;
        bool deviceLevel;
    };

    QList<Command> commands() const { return m_commands; }

    void setFileName(const QString &fn) { m_fn = fn; }

private:
    void skip();
    void parseCommands();
    Command parseCommand();
    TypedName parseParamOrProto(const QString &tag);
    QString parseName();

    QFile m_file;
    QXmlStreamReader m_reader;
    QList<Command> m_commands;
    QString m_fn;
};

bool VkSpecParser::parse()
{
    m_file.setFileName(m_fn);
    if (!m_file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        qWarning("Failed to open %s", qPrintable(m_file.fileName()));
        return false;
    }

    m_reader.setDevice(&m_file);
    while (!m_reader.atEnd()) {
        m_reader.readNext();
        if (m_reader.isStartElement()) {
            if (m_reader.name() == QStringLiteral("commands"))
                parseCommands();
        }
    }

    return true;
}

void VkSpecParser::skip()
{
    QString tag = m_reader.name().toString();
    while (!m_reader.atEnd()) {
        m_reader.readNext();
        if (m_reader.isEndElement() && m_reader.name() == tag)
            break;
    }
}

void VkSpecParser::parseCommands()
{
    m_commands.clear();

    while (!m_reader.atEnd()) {
        m_reader.readNext();
        if (m_reader.isEndElement() && m_reader.name() == QStringLiteral("commands"))
            return;
        if (m_reader.isStartElement() && m_reader.name() == u"command")
            m_commands.append(parseCommand());
    }
}

VkSpecParser::Command VkSpecParser::parseCommand()
{
    Command c;

    while (!m_reader.atEnd()) {
        m_reader.readNext();
        if (m_reader.isEndElement() && m_reader.name() == QStringLiteral("command"))
            break;
        if (m_reader.isStartElement()) {
            const QString protoStr = QStringLiteral("proto");
            const QString paramStr = QStringLiteral("param");
            if (m_reader.name() == protoStr) {
                c.cmd = parseParamOrProto(protoStr);
            } else if (m_reader.name() == paramStr) {
                c.args.append(parseParamOrProto(paramStr));
            } else {
                skip();
            }
        }
    }

    c.deviceLevel = false;
    if (!c.args.isEmpty()) {
        QStringList dispatchableDeviceAndChildTypes {
            QStringLiteral("VkDevice"),
            QStringLiteral("VkQueue"),
            QStringLiteral("VkCommandBuffer")
        };
        if (dispatchableDeviceAndChildTypes.contains(c.args[0].type)
                && c.cmd.name != QStringLiteral("vkGetDeviceProcAddr"))
        {
            c.deviceLevel = true;
        }
    }

    return c;
}

VkSpecParser::TypedName VkSpecParser::parseParamOrProto(const QString &tag)
{
    TypedName t;

    while (!m_reader.atEnd()) {
        m_reader.readNext();
        if (m_reader.isEndElement() && m_reader.name() == tag)
            break;
        if (m_reader.isStartElement()) {
            if (m_reader.name() == QStringLiteral("name")) {
                t.name = parseName();
            } else if (m_reader.name() != QStringLiteral("type")) {
                skip();
            }
        } else {
            auto text = m_reader.text().trimmed();
            if (!text.isEmpty()) {
                if (text.startsWith(QLatin1Char('['))) {
                    t.typeSuffix += text;
                } else {
                    if (!t.type.isEmpty())
                        t.type += QLatin1Char(' ');
                    t.type += text;
                }
            }
        }
    }

    return t;
}

QString VkSpecParser::parseName()
{
    QString name;
    while (!m_reader.atEnd()) {
        m_reader.readNext();
        if (m_reader.isEndElement() && m_reader.name() == QStringLiteral("name"))
            break;
        name += m_reader.text();
    }
    return name.trimmed();
}

QString funcSig(const VkSpecParser::Command &c, const char *className = nullptr)
{
    QString s(QString::asprintf("%s %s%s%s", qPrintable(c.cmd.type),
                                (className ? className : ""), (className ? "::" : ""),
                                qPrintable(c.cmd.name)));
    if (!c.args.isEmpty()) {
        s += QLatin1Char('(');
        bool first = true;
        for (const VkSpecParser::TypedName &a : c.args) {
            if (!first)
                s += QStringLiteral(", ");
            else
                first = false;
            s += QString::asprintf("%s%s%s%s", qPrintable(a.type),
                                   (a.type.endsWith(QLatin1Char('*')) ? "" : " "),
                                   qPrintable(a.name), qPrintable(a.typeSuffix));
        }
        s += QLatin1Char(')');
    }
    return s;
}

QString funcCall(const VkSpecParser::Command &c, int idx)
{
    // template:
    //     [return] reinterpret_cast<PFN_vkEnumeratePhysicalDevices>(d_ptr->m_funcs[0])(instance, pPhysicalDeviceCount, pPhysicalDevices);
    QString s = QString::asprintf("%sreinterpret_cast<PFN_%s>(d_ptr->m_funcs[%d])",
                                  (c.cmd.type == QStringLiteral("void") ? "" : "return "),
                                  qPrintable(c.cmd.name),
                                  idx);
    if (!c.args.isEmpty()) {
        s += QLatin1Char('(');
        bool first = true;
        for (const VkSpecParser::TypedName &a : c.args) {
            if (!first)
                s += QStringLiteral(", ");
            else
                first = false;
            s += a.name;
        }
        s += QLatin1Char(')');
    }
    return s;
}

class Preamble
{
public:
    QByteArray get(const QString &fn);

private:
    QByteArray m_str;
} preamble;

QByteArray Preamble::get(const QString &fn)
{
    if (!m_str.isEmpty())
        return m_str;

    QFile f(fn);
    if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
        qWarning("Failed to open %s", qPrintable(fn));
        return m_str;
    }

    m_str = f.readAll();
    m_str.replace("FOO", "QtGui");
    m_str += "\n// This file is automatically generated by qvkgen. Do not edit.\n";

    return m_str;
}

bool genVulkanFunctionsH(const QList<VkSpecParser::Command> &commands, const QString &licHeaderFn,
                         const QString &outputBase)
{
    QFile f(outputBase + QStringLiteral(".h"));
    if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) {
        qWarning("Failed to write %s", qPrintable(f.fileName()));
        return false;
    }

    static const char *s =
"%s\n"
"#ifndef QVULKANFUNCTIONS_H\n"
"#define QVULKANFUNCTIONS_H\n"
"\n"
"#include <QtGui/qtguiglobal.h>\n"
"\n"
"#if QT_CONFIG(vulkan) || defined(Q_CLANG_QDOC)\n"
"\n"
"#ifndef VK_NO_PROTOTYPES\n"
"#define VK_NO_PROTOTYPES\n"
"#endif\n"
"#include <vulkan/vulkan.h>\n"
"\n"
"#include <QtCore/qscopedpointer.h>\n"
"\n"
"QT_BEGIN_NAMESPACE\n"
"\n"
"class QVulkanInstance;\n"
"class QVulkanFunctionsPrivate;\n"
"class QVulkanDeviceFunctionsPrivate;\n"
"\n"
"class Q_GUI_EXPORT QVulkanFunctions\n"
"{\n"
"public:\n"
"    ~QVulkanFunctions();\n"
"\n"
"%s\n"
"private:\n"
"    Q_DISABLE_COPY(QVulkanFunctions)\n"
"    QVulkanFunctions(QVulkanInstance *inst);\n"
"\n"
"    QScopedPointer<QVulkanFunctionsPrivate> d_ptr;\n"
"    friend class QVulkanInstance;\n"
"};\n"
"\n"
"class Q_GUI_EXPORT QVulkanDeviceFunctions\n"
"{\n"
"public:\n"
"    ~QVulkanDeviceFunctions();\n"
"\n"
"%s\n"
"private:\n"
"    Q_DISABLE_COPY(QVulkanDeviceFunctions)\n"
"    QVulkanDeviceFunctions(QVulkanInstance *inst, VkDevice device);\n"
"\n"
"    QScopedPointer<QVulkanDeviceFunctionsPrivate> d_ptr;\n"
"    friend class QVulkanInstance;\n"
"};\n"
"\n"
"QT_END_NAMESPACE\n"
"\n"
"#endif // QT_CONFIG(vulkan) || defined(Q_CLANG_QDOC)\n"
"\n"
"#endif // QVULKANFUNCTIONS_H\n";

    QString instCmdStr;
    QString devCmdStr;
    for (const VkSpecParser::Command &c : commands) {
        QString *dst = c.deviceLevel ? &devCmdStr : &instCmdStr;
        *dst += QStringLiteral("    ");
        *dst += funcSig(c);
        *dst += QStringLiteral(";\n");
    }

    f.write(QString::asprintf(s, preamble.get(licHeaderFn).constData(),
                              instCmdStr.toUtf8().constData(),
                              devCmdStr.toUtf8().constData()).toUtf8());

    return true;
}

bool genVulkanFunctionsPH(const QList<VkSpecParser::Command> &commands, const QString &licHeaderFn,
                          const QString &outputBase)
{
    QFile f(outputBase + QStringLiteral("_p.h"));
    if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) {
        qWarning("Failed to write %s", qPrintable(f.fileName()));
        return false;
    }

    static const char *s =
"%s\n"
"#ifndef QVULKANFUNCTIONS_P_H\n"
"#define QVULKANFUNCTIONS_P_H\n"
"\n"
"//\n"
"//  W A R N I N G\n"
"//  -------------\n"
"//\n"
"// This file is not part of the Qt API.  It exists purely as an\n"
"// implementation detail.  This header file may change from version to\n"
"// version without notice, or even be removed.\n"
"//\n"
"// We mean it.\n"
"//\n"
"\n"
"#include \"qvulkanfunctions.h\"\n"
"\n"
"QT_BEGIN_NAMESPACE\n"
"\n"
"class QVulkanInstance;\n"
"\n"
"class QVulkanFunctionsPrivate\n"
"{\n"
"public:\n"
"    QVulkanFunctionsPrivate(QVulkanInstance *inst);\n"
"\n"
"    PFN_vkVoidFunction m_funcs[%d];\n"
"};\n"
"\n"
"class QVulkanDeviceFunctionsPrivate\n"
"{\n"
"public:\n"
"    QVulkanDeviceFunctionsPrivate(QVulkanInstance *inst, VkDevice device);\n"
"\n"
"    PFN_vkVoidFunction m_funcs[%d];\n"
"};\n"
"\n"
"QT_END_NAMESPACE\n"
"\n"
"#endif // QVULKANFUNCTIONS_P_H\n";

    const int devLevelCount = std::count_if(commands.cbegin(), commands.cend(),
                                            [](const VkSpecParser::Command &c) { return c.deviceLevel; });
    const int instLevelCount = commands.count() - devLevelCount;

    f.write(QString::asprintf(s, preamble.get(licHeaderFn).constData(), instLevelCount, devLevelCount).toUtf8());

    return true;
}

bool genVulkanFunctionsPC(const QList<VkSpecParser::Command> &commands, const QString &licHeaderFn,
                          const QString &outputBase)
{
    QFile f(outputBase + QStringLiteral("_p.cpp"));
    if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) {
        qWarning("Failed to write %s", qPrintable(f.fileName()));
        return false;
    }

    static const char *s =
"%s\n"
"#include \"qvulkanfunctions_p.h\"\n"
"#include \"qvulkaninstance.h\"\n"
"\n"
"QT_BEGIN_NAMESPACE\n"
"\n%s"
"QVulkanFunctionsPrivate::QVulkanFunctionsPrivate(QVulkanInstance *inst)\n"
"{\n"
"    static const char *funcNames[] = {\n"
"%s\n"
"    };\n"
"    for (int i = 0; i < %d; ++i) {\n"
"        m_funcs[i] = inst->getInstanceProcAddr(funcNames[i]);\n"
"        if (!m_funcs[i])\n"
"            qWarning(\"QVulkanFunctions: Failed to resolve %%s\", funcNames[i]);\n"
"    }\n"
"}\n"
"\n%s"
"QVulkanDeviceFunctionsPrivate::QVulkanDeviceFunctionsPrivate(QVulkanInstance *inst, VkDevice device)\n"
"{\n"
"    QVulkanFunctions *f = inst->functions();\n"
"    Q_ASSERT(f);\n\n"
"    static const char *funcNames[] = {\n"
"%s\n"
"    };\n"
"    for (int i = 0; i < %d; ++i) {\n"
"        m_funcs[i] = f->vkGetDeviceProcAddr(device, funcNames[i]);\n"
"        if (!m_funcs[i])\n"
"            qWarning(\"QVulkanDeviceFunctions: Failed to resolve %%s\", funcNames[i]);\n"
"    }\n"
"}\n"
"\n"
"QT_END_NAMESPACE\n";

    QString devCmdWrapperStr;
    QString instCmdWrapperStr;
    int devIdx = 0;
    int instIdx = 0;
    QString devCmdNamesStr;
    QString instCmdNamesStr;

    for (int i = 0; i < commands.count(); ++i) {
        QString *dst = commands[i].deviceLevel ? &devCmdWrapperStr : &instCmdWrapperStr;
        int *idx = commands[i].deviceLevel ? &devIdx : &instIdx;
        *dst += funcSig(commands[i], commands[i].deviceLevel ? "QVulkanDeviceFunctions" : "QVulkanFunctions");
        *dst += QString(QStringLiteral("\n{\n    Q_ASSERT(d_ptr->m_funcs[%1]);\n    ")).arg(*idx);
        *dst += funcCall(commands[i], *idx);
        *dst += QStringLiteral(";\n}\n\n");
        ++*idx;

        dst = commands[i].deviceLevel ? &devCmdNamesStr : &instCmdNamesStr;
        *dst += QStringLiteral("        \"");
        *dst += commands[i].cmd.name;
        *dst += QStringLiteral("\",\n");
    }

    if (devCmdNamesStr.count() > 2)
        devCmdNamesStr = devCmdNamesStr.left(devCmdNamesStr.count() - 2);
    if (instCmdNamesStr.count() > 2)
        instCmdNamesStr = instCmdNamesStr.left(instCmdNamesStr.count() - 2);

    const QString str =
            QString::asprintf(s, preamble.get(licHeaderFn).constData(),
                              instCmdWrapperStr.toUtf8().constData(),
                              instCmdNamesStr.toUtf8().constData(), instIdx,
                              devCmdWrapperStr.toUtf8().constData(),
                              devCmdNamesStr.toUtf8().constData(), commands.count() - instIdx);

    f.write(str.toUtf8());

    return true;
}

int main(int argc, char **argv)
{
    QCoreApplication app(argc, argv);
    VkSpecParser parser;

    if (argc < 4) {
        qWarning("Usage: qvkgen input_vk_xml input_license_header output_base\n"
                 "  For example: qvkgen vulkan/vk.xml vulkan/qvulkanfunctions.header vulkan/qvulkanfunctions");
        return 1;
    }

    parser.setFileName(QString::fromUtf8(argv[1]));

    if (!parser.parse())
        return 1;

    QList<VkSpecParser::Command> commands = parser.commands();
    QStringList ignoredFuncs {
        QStringLiteral("vkCreateInstance"),
        QStringLiteral("vkDestroyInstance"),
        QStringLiteral("vkGetInstanceProcAddr")
    };

    // Filter out extensions and unwanted functions.
    // The check for the former is rather simplistic for now: skip if the last letter is uppercase...
    for (int i = 0; i < commands.count(); ++i) {
        QString name = commands[i].cmd.name;
        QChar c = name[name.count() - 1];
        if (c.isUpper() || ignoredFuncs.contains(name))
            commands.remove(i--);
    }

    QString licenseHeaderFileName = QString::fromUtf8(argv[2]);
    QString outputBase = QString::fromUtf8(argv[3]);
    genVulkanFunctionsH(commands, licenseHeaderFileName, outputBase);
    genVulkanFunctionsPH(commands, licenseHeaderFileName, outputBase);
    genVulkanFunctionsPC(commands, licenseHeaderFileName, outputBase);

    return 0;
}
