// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "qscxmlcompiler_p.h"
#include "qscxmlexecutablecontent_p.h"

#include <qxmlstream.h>
#include <qloggingcategory.h>
#include <qfile.h>
#include <qlist.h>
#include <qstring.h>

#ifndef BUILD_QSCXMLC
#include "qscxmlinvokableservice_p.h"
#include "qscxmldatamodel_p.h"
#include "qscxmlstatemachine_p.h"
#include "qscxmlstatemachine.h"
#include "qscxmltabledata_p.h"

#include <private/qmetaobjectbuilder_p.h>
#endif // BUILD_QSCXMLC

#include <QtCore/qmap.h>

#include <functional>

namespace {
enum {
    DebugHelper_NameTransitions = 0
};
} // anonymous namespace

QT_BEGIN_NAMESPACE

static QString scxmlNamespace = QStringLiteral("http://www.w3.org/2005/07/scxml");
static QString qtScxmlNamespace = QStringLiteral("http://theqtcompany.com/scxml/2015/06/");

namespace {

class ScxmlVerifier: public DocumentModel::NodeVisitor
{
public:
    ScxmlVerifier(std::function<void (const DocumentModel::XmlLocation &, const QString &)> errorHandler)
        : m_errorHandler(errorHandler)
        , m_doc(nullptr)
        , m_hasErrors(false)
    {}

    bool verify(DocumentModel::ScxmlDocument *doc)
    {
        if (doc->isVerified)
            return true;

        doc->isVerified = true;
        m_doc = doc;
        for (DocumentModel::AbstractState *state : std::as_const(doc->allStates)) {
            if (state->id.isEmpty()) {
                continue;
#ifndef QT_NO_DEBUG
            } else if (m_stateById.contains(state->id)) {
                Q_ASSERT(!"Should be unreachable: the compiler should check for this case!");
#endif // QT_NO_DEBUG
            } else {
                m_stateById[state->id] = state;
            }
        }

        if (doc->root)
            doc->root->accept(this);
        return !m_hasErrors;
    }

private:
    bool visit(DocumentModel::Scxml *scxml) override
    {
        if (!scxml->name.isEmpty() && !isValidToken(scxml->name, XmlNmtoken)) {
            error(scxml->xmlLocation,
                  QStringLiteral("scxml name '%1' is not a valid XML Nmtoken").arg(scxml->name));
        }

        if (scxml->initial.isEmpty()) {
            if (auto firstChild = firstAbstractState(scxml)) {
                scxml->initialTransition = createInitialTransition({firstChild});
            }
        } else {
            QList<DocumentModel::AbstractState *> initialStates;
            for (const QString &initial : std::as_const(scxml->initial)) {
                if (DocumentModel::AbstractState *s = m_stateById.value(initial))
                    initialStates.append(s);
                else
                    error(scxml->xmlLocation, QStringLiteral("initial state '%1' not found for <scxml> element").arg(initial));
            }
            scxml->initialTransition = createInitialTransition(initialStates);
        }

        m_parentNodes.append(scxml);

        return true;
    }

    void endVisit(DocumentModel::Scxml *) override
    {
        m_parentNodes.removeLast();
    }

    bool visit(DocumentModel::State *state) override
    {
        if (!state->id.isEmpty() && !isValidToken(state->id, XmlNCName)) {
            error(state->xmlLocation, QStringLiteral("'%1' is not a valid XML ID").arg(state->id));
        }

        if (state->initialTransition == nullptr) {
            if (state->initial.isEmpty()) {
                if (state->type == DocumentModel::State::Parallel) {
                    auto allChildren = allAbstractStates(state);
                    state->initialTransition = createInitialTransition(allChildren);
                } else {
                    if (auto firstChild = firstAbstractState(state)) {
                        state->initialTransition = createInitialTransition({firstChild});
                    }
                }
            } else {
                Q_ASSERT(state->type == DocumentModel::State::Normal);
                QList<DocumentModel::AbstractState *> initialStates;
                for (const QString &initialState : std::as_const(state->initial)) {
                    if (DocumentModel::AbstractState *s = m_stateById.value(initialState)) {
                        initialStates.append(s);
                    } else {
                        error(state->xmlLocation,
                              QStringLiteral("undefined initial state '%1' for state '%2'")
                              .arg(initialState, state->id));
                    }
                }
                state->initialTransition = createInitialTransition(initialStates);
            }
        } else {
            if (state->initial.isEmpty()) {
                visit(state->initialTransition);
            } else {
                error(state->xmlLocation,
                      QStringLiteral("initial transition and initial attribute for state '%1'")
                      .arg(state->id));
            }
        }

        switch (state->type) {
        case DocumentModel::State::Normal:
            break;
        case DocumentModel::State::Parallel:
            if (!state->initial.isEmpty()) {
                error(state->xmlLocation,
                      QStringLiteral("parallel states cannot have an initial state"));
            }
            break;
        case DocumentModel::State::Final:
            break;
        default:
            Q_UNREACHABLE();
        }

        m_parentNodes.append(state);
        return true;
    }

    void endVisit(DocumentModel::State *) override
    {
        m_parentNodes.removeLast();
    }

    bool visit(DocumentModel::Transition *transition) override
    {
        Q_ASSERT(transition->targetStates.isEmpty());

        if (int size = transition->targets.size())
            transition->targetStates.reserve(size);
        for (const QString &target : std::as_const(transition->targets)) {
            if (DocumentModel::AbstractState *s = m_stateById.value(target)) {
                if (transition->targetStates.contains(s)) {
                    error(transition->xmlLocation, QStringLiteral("duplicate target '%1'").arg(target));
                } else {
                    transition->targetStates.append(s);
                }
            } else if (!target.isEmpty()) {
                error(transition->xmlLocation, QStringLiteral("unknown state '%1' in target").arg(target));
            }
        }
        for (const QString &event : std::as_const(transition->events))
            checkEvent(event, transition->xmlLocation, AllowWildCards);

        m_parentNodes.append(transition);
        return true;
    }

    void endVisit(DocumentModel::Transition *) override
    {
        m_parentNodes.removeLast();
    }

    bool visit(DocumentModel::HistoryState *state) override
    {
        bool seenTransition = false;
        for (DocumentModel::StateOrTransition *sot : std::as_const(state->children)) {
            if (DocumentModel::State *s = sot->asState()) {
                error(s->xmlLocation, QStringLiteral("history state cannot have substates"));
            } else if (DocumentModel::Transition *t = sot->asTransition()) {
                if (seenTransition) {
                    error(t->xmlLocation, QStringLiteral("history state can only have one transition"));
                } else {
                    seenTransition = true;
                    m_parentNodes.append(state);
                    t->accept(this);
                    m_parentNodes.removeLast();
                }
            }
        }

        return false;
    }

    bool visit(DocumentModel::Send *node) override
    {
        checkEvent(node->event, node->xmlLocation, ForbidWildCards);
        checkExpr(node->xmlLocation, QStringLiteral("send"), QStringLiteral("eventexpr"), node->eventexpr);
        return true;
    }

    void visit(DocumentModel::Cancel *node) override
    {
        checkExpr(node->xmlLocation, QStringLiteral("cancel"), QStringLiteral("sendidexpr"), node->sendidexpr);
    }

    bool visit(DocumentModel::DoneData *node) override
    {
        checkExpr(node->xmlLocation, QStringLiteral("donedata"), QStringLiteral("expr"), node->expr);
        return false;
    }

    bool visit(DocumentModel::Invoke *node) override
    {
        if (!node->srcexpr.isEmpty())
            return false;

        if (node->content.isNull()) {
            error(node->xmlLocation, QStringLiteral("no valid content found in <invoke> tag"));
        } else {
            ScxmlVerifier subVerifier(m_errorHandler);
            m_hasErrors = !subVerifier.verify(node->content.data());
        }
        return false;
    }

private:
    enum TokenType {
        XmlNCName,
        XmlNmtoken,
    };

    static bool isValidToken(const QString &id, TokenType tokenType)
    {
        Q_ASSERT(!id.isEmpty());
        int i = 0;
        if (tokenType == XmlNCName) {
            const QChar c = id.at(i++);
            if (!isLetter(c) && c != QLatin1Char('_'))
                return false;
        }
        for (int ei = id.size(); i != ei; ++i) {
            const QChar c = id.at(i);
            if (isLetter(c) || c.isDigit() || c == QLatin1Char('.') || c == QLatin1Char('-')
                    || c == QLatin1Char('_') || isNameTail(c))
                continue;
            else if (tokenType == XmlNmtoken && c == QLatin1Char(':'))
                continue;
            else
                return false;
        }

        return true;
    }

    static bool isLetter(QChar c)
    {
        switch (c.category()) {
        case QChar::Letter_Lowercase:
        case QChar::Letter_Uppercase:
        case QChar::Letter_Other:
        case QChar::Letter_Titlecase:
        case QChar::Number_Letter:
            return true;
        default:
            return false;
        }
    }

    static bool isNameTail(QChar c)
    {
        switch (c.category()) {
        case QChar::Mark_SpacingCombining:
        case QChar::Mark_Enclosing:
        case QChar::Mark_NonSpacing:
        case QChar::Letter_Modifier:
        case QChar::Number_DecimalDigit:
            return true;
        default:
            return false;
        }
    }

    enum WildCardMode {
        ForbidWildCards,
        AllowWildCards
    };

    void checkEvent(const QString &event, const DocumentModel::XmlLocation &loc,
                    WildCardMode wildCardMode)
    {
        if (event.isEmpty())
            return;

        if (!isValidEvent(event, wildCardMode)) {
            error(loc, QStringLiteral("'%1' is not a valid event").arg(event));
        }
    }

    static bool isValidEvent(const QString &event, WildCardMode wildCardMode)
    {
        if (event.isEmpty())
            return false;

        if (wildCardMode == AllowWildCards && event == QLatin1String(".*"))
            return true;

        const QStringList parts = event.split(QLatin1Char('.'));

        for (const QString &part : parts) {
            if (part.isEmpty())
                return false;

            if (wildCardMode == AllowWildCards && part.size() == 1
                    && part.at(0) == QLatin1Char('*')) {
                continue;
            }

            for (int i = 0, ei = part.size(); i != ei; ++i) {
                const QChar c = part.at(i);
                if (!isLetter(c) && !c.isDigit() && c != QLatin1Char('-') && c != QLatin1Char('_')
                        && c != QLatin1Char(':')) {
                    return false;
                }
            }
        }

        return true;
    }

    static const QList<DocumentModel::StateOrTransition *> &allChildrenOfContainer(
            DocumentModel::StateContainer *container)
    {
        if (auto state = container->asState())
            return state->children;
        else if (auto scxml = container->asScxml())
            return scxml->children;
        else
            Q_UNREACHABLE();
    }

    static DocumentModel::AbstractState *firstAbstractState(DocumentModel::StateContainer *container)
    {
        const auto &allChildren = allChildrenOfContainer(container);

        QList<DocumentModel::AbstractState *> childStates;
        for (DocumentModel::StateOrTransition *child : std::as_const(allChildren)) {
            if (DocumentModel::State *s = child->asState())
                return s;
            else if (DocumentModel::HistoryState *h = child->asHistoryState())
                return h;
        }
        return nullptr;
    }

    static QList<DocumentModel::AbstractState *> allAbstractStates(
            DocumentModel::StateContainer *container)
    {
        const auto &allChildren = allChildrenOfContainer(container);

        QList<DocumentModel::AbstractState *> childStates;
        for (DocumentModel::StateOrTransition *child : std::as_const(allChildren)) {
            if (DocumentModel::State *s = child->asState())
                childStates.append(s);
            else if (DocumentModel::HistoryState *h = child->asHistoryState())
                childStates.append(h);
        }
        return childStates;
    }

    DocumentModel::Transition *createInitialTransition(
            const QList<DocumentModel::AbstractState *> &states)
    {
        auto *newTransition = m_doc->newTransition(nullptr, DocumentModel::XmlLocation(-1, -1));
        newTransition->type = DocumentModel::Transition::Synthetic;
        for (auto *s : states) {
            newTransition->targets.append(s->id);
        }

        newTransition->targetStates = states;
        return newTransition;
    }

    void checkExpr(const DocumentModel::XmlLocation &loc, const QString &tag, const QString &attrName, const QString &attrValue)
    {
        if (m_doc->root->dataModel == DocumentModel::Scxml::NullDataModel && !attrValue.isEmpty()) {
            error(loc, QStringLiteral(
                      "%1 in <%2> cannot be used with data model 'null'").arg(attrName, tag));
        }
    }

    void error(const DocumentModel::XmlLocation &location, const QString &message)
    {
        m_hasErrors = true;
        if (m_errorHandler)
            m_errorHandler(location, message);
    }

private:
    std::function<void (const DocumentModel::XmlLocation &, const QString &)> m_errorHandler;
    DocumentModel::ScxmlDocument *m_doc;
    bool m_hasErrors;
    QHash<QString, DocumentModel::AbstractState *> m_stateById;
    QList<DocumentModel::Node *> m_parentNodes;
};

#ifndef BUILD_QSCXMLC
class InvokeDynamicScxmlFactory: public QScxmlInvokableServiceFactory
{
    Q_OBJECT
public:
    InvokeDynamicScxmlFactory(const QScxmlExecutableContent::InvokeInfo &invokeInfo,
                              const QList<QScxmlExecutableContent::StringId> &namelist,
                              const QList<QScxmlExecutableContent::ParameterInfo> &params)
        : QScxmlInvokableServiceFactory(invokeInfo, namelist, params)
    {}

    void setContent(const QSharedPointer<DocumentModel::ScxmlDocument> &content)
    { m_content = content; }

    QScxmlInvokableService *invoke(QScxmlStateMachine *child) override;

private:
    QSharedPointer<DocumentModel::ScxmlDocument> m_content;
};

class DynamicStateMachinePrivate : public QScxmlStateMachinePrivate
{
    struct DynamicMetaObject : public QAbstractDynamicMetaObject
    {
        QMetaObject *toDynamicMetaObject(QObject *) override
        {
            return this;
        }

        int metaCall(QObject *o, QMetaObject::Call c, int id, void **a) override
        {
            return o->qt_metacall(c, id, a);
        }
    };

public:
    DynamicStateMachinePrivate() :
        QScxmlStateMachinePrivate(&QScxmlStateMachine::staticMetaObject)
    {
        metaObject = new DynamicMetaObject;
    }

    void setDynamicMetaObject(const QMetaObject *m) {
        // Prevent the QML engine from creating a property cache for this thing.
        static_cast<DynamicMetaObject *>(metaObject)->d = m->d;
        m_metaObject = m;
    }
};

class DynamicStateMachine: public QScxmlStateMachine, public QScxmlInternal::GeneratedTableData
{
    Q_DECLARE_PRIVATE(DynamicStateMachine)
    // Manually expanded from Q_OBJECT macro:
public:
    const QMetaObject *metaObject() const override
    { return d_func()->m_metaObject; }

    int qt_metacall(QMetaObject::Call _c, int _id, void **_a) override
    {
        Q_D(DynamicStateMachine);
        _id = QScxmlStateMachine::qt_metacall(_c, _id, _a);
        if (_id < 0)
            return _id;
        int ownMethodCount = d->m_metaObject->methodCount() - d->m_metaObject->methodOffset();
        if (_c == QMetaObject::InvokeMetaMethod) {
            if (_id < ownMethodCount)
                qt_static_metacall(this, _c, _id, _a);
            _id -= ownMethodCount;
        } else if (_c == QMetaObject::ReadProperty || _c == QMetaObject::WriteProperty
                   || _c == QMetaObject::ResetProperty || _c == QMetaObject::RegisterPropertyMetaType) {
            qt_static_metacall(this, _c, _id, _a);
            _id -= d->m_metaObject->propertyCount();
        }
        return _id;
    }

private:
    static void qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
    {
        if (_c == QMetaObject::RegisterPropertyMetaType) {
            *reinterpret_cast<int*>(_a[0]) = qRegisterMetaType<bool>();
        } else if (_c == QMetaObject::ReadProperty) {
            DynamicStateMachine *_t = static_cast<DynamicStateMachine *>(_o);
            void *_v = _a[0];
            if (_id >= 0 && _id < _t->m_propertyCount) {
                // getter for the state
                *reinterpret_cast<bool*>(_v) = _t->isActive(_id);
            }
        }
    }
    // end of Q_OBJECT macro

private:
    DynamicStateMachine()
        : QScxmlStateMachine(*new DynamicStateMachinePrivate)
        , m_propertyCount(0)
    {
        // Temporarily wire up the QMetaObject
        Q_D(DynamicStateMachine);
        QMetaObjectBuilder b;
        b.setClassName("DynamicStateMachine");
        b.setSuperClass(&QScxmlStateMachine::staticMetaObject);
        b.setStaticMetacallFunction(qt_static_metacall);
        d->setDynamicMetaObject(b.toMetaObject());
    }

    void initDynamicParts(const MetaDataInfo &info)
    {
        Q_D(DynamicStateMachine);
        // Release the temporary QMetaObject.
        Q_ASSERT(d->m_metaObject != &QScxmlStateMachine::staticMetaObject);
        free(const_cast<QMetaObject *>(d->m_metaObject));
        d->setDynamicMetaObject(&QScxmlStateMachine::staticMetaObject);

        // Build the real one.
        QMetaObjectBuilder b;
        b.setClassName("DynamicStateMachine");
        b.setSuperClass(&QScxmlStateMachine::staticMetaObject);
        b.setStaticMetacallFunction(qt_static_metacall);

        // signals
        for (const QString &stateName : info.stateNames) {
            auto name = stateName.toUtf8();
            const QByteArray signalName = name + "Changed(bool)";
            QMetaMethodBuilder signalBuilder = b.addSignal(signalName);
            signalBuilder.setParameterNames(init("active"));
        }

        // properties
        int notifier = 0;
        for (const QString &stateName : info.stateNames) {
            QMetaPropertyBuilder prop = b.addProperty(stateName.toUtf8(), "bool", notifier);
            prop.setWritable(false);
            ++m_propertyCount;
            ++notifier;
        }

        // And we're done
        d->setDynamicMetaObject(b.toMetaObject());
    }

public:
    ~DynamicStateMachine()
    {
        Q_D(DynamicStateMachine);
        if (d->m_metaObject != &QScxmlStateMachine::staticMetaObject) {
            free(const_cast<QMetaObject *>(d->m_metaObject));
            d->setDynamicMetaObject(&QScxmlStateMachine::staticMetaObject);
        }
    }

    QScxmlInvokableServiceFactory *serviceFactory(int id) const override final
    { return m_allFactoriesById.at(id); }

    static DynamicStateMachine *build(DocumentModel::ScxmlDocument *doc)
    {
        auto stateMachine = new DynamicStateMachine;
        MetaDataInfo info;
        DataModelInfo dm;
        auto factoryIdCreator = [stateMachine](
                const QScxmlExecutableContent::InvokeInfo &invokeInfo,
                const QList<QScxmlExecutableContent::StringId> &namelist,
                const QList<QScxmlExecutableContent::ParameterInfo> &params,
                const QSharedPointer<DocumentModel::ScxmlDocument> &content) -> int {
            auto factory = new InvokeDynamicScxmlFactory(invokeInfo, namelist, params);
            factory->setContent(content);
            stateMachine->m_allFactoriesById.append(factory);
            return stateMachine->m_allFactoriesById.size() - 1;
        };

        GeneratedTableData::build(doc, stateMachine, &info, &dm, factoryIdCreator);
        stateMachine->setTableData(stateMachine);
        stateMachine->initDynamicParts(info);

        return stateMachine;
    }

private:
    static QList<QByteArray> init(const char *s)
    {
#ifdef Q_COMPILER_INITIALIZER_LISTS
        return QList<QByteArray>({ QByteArray::fromRawData(s, int(strlen(s))) });
#else // insane compiler:
        return QList<QByteArray>() << QByteArray::fromRawData(s, int(strlen(s)));
#endif
    }

private:
    QList<QScxmlInvokableServiceFactory *> m_allFactoriesById;
    int m_propertyCount;
};

inline QScxmlInvokableService *InvokeDynamicScxmlFactory::invoke(
        QScxmlStateMachine *parentStateMachine)
{
    bool ok = true;
    auto srcexpr = calculateSrcexpr(parentStateMachine, invokeInfo().expr, &ok);
    if (!ok)
        return nullptr;

    if (!srcexpr.isEmpty())
        return invokeDynamicScxmlService(srcexpr, parentStateMachine, this);

    auto childStateMachine = DynamicStateMachine::build(m_content.data());

    auto dm = QScxmlDataModelPrivate::instantiateDataModel(m_content->root->dataModel);
    dm->setParent(childStateMachine);
    childStateMachine->setDataModel(dm);

    return invokeStaticScxmlService(childStateMachine, parentStateMachine, this);
}
#endif // BUILD_QSCXMLC

} // anonymous namespace

#ifndef BUILD_QSCXMLC
QScxmlScxmlService *invokeDynamicScxmlService(const QString &sourceUrl,
                                              QScxmlStateMachine *parentStateMachine,
                                              QScxmlInvokableServiceFactory *factory)
{
    QScxmlCompiler::Loader *loader = parentStateMachine->loader();

    const QString baseDir = sourceUrl.isEmpty() ? QString() : QFileInfo(sourceUrl).path();
    QStringList errs;
    const QByteArray data = loader->load(sourceUrl, baseDir, &errs);

    if (!errs.isEmpty()) {
        qWarning() << errs;
        return nullptr;
    }

    QXmlStreamReader reader(data);
    QScxmlCompiler compiler(&reader);
    compiler.setFileName(sourceUrl);
    compiler.setLoader(parentStateMachine->loader());
    compiler.compile();
    if (!compiler.errors().isEmpty()) {
        const auto errors = compiler.errors();
        for (const QScxmlError &error : errors)
            qWarning().noquote() << error.toString();
        return nullptr;
    }

    auto mainDoc = QScxmlCompilerPrivate::get(&compiler)->scxmlDocument();
    if (mainDoc == nullptr) {
        Q_ASSERT(!compiler.errors().isEmpty());
        const auto errors = compiler.errors();
        for (const QScxmlError &error : errors)
            qWarning().noquote() << error.toString();
        return nullptr;
    }

    auto childStateMachine = DynamicStateMachine::build(mainDoc);

    auto dm = QScxmlDataModelPrivate::instantiateDataModel(mainDoc->root->dataModel);
    dm->setParent(childStateMachine);
    childStateMachine->setDataModel(dm);

    return invokeStaticScxmlService(childStateMachine, parentStateMachine, factory);
}
#endif // BUILD_QSCXMLC

/*!
 * \class QScxmlCompiler
 * \brief The QScxmlCompiler class is a compiler for SCXML files.
 * \since 5.7
 * \inmodule QtScxml
 *
 * Parses an \l{SCXML Specification}{SCXML} file and dynamically instantiates a
 * state machine for a successfully parsed SCXML file. If parsing fails, the
 * new state machine cannot start. All errors are returned by
 * QScxmlStateMachine::parseErrors().
 *
 * To load an SCXML file, QScxmlStateMachine::fromFile or QScxmlStateMachine::fromData should be
 * used. Using QScxmlCompiler directly is only needed when the compiler needs to use a custom
 * QScxmlCompiler::Loader.
 */

/*!
 * Creates a new SCXML compiler for the specified \a reader.
 */
QScxmlCompiler::QScxmlCompiler(QXmlStreamReader *reader)
    : d(new QScxmlCompilerPrivate(reader))
{ }

/*!
 * Destroys the SCXML compiler.
 */
QScxmlCompiler::~QScxmlCompiler()
{
    delete d;
}

/*!
 * Returns the file name associated with the current input.
 *
 * \sa setFileName()
 */
QString QScxmlCompiler::fileName() const
{
    return d->fileName();
}

/*!
 * Sets the file name for the current input to \a fileName.
 *
 * The file name is used for error reporting and for resolving relative path URIs.
 *
 * \sa fileName()
 */
void QScxmlCompiler::setFileName(const QString &fileName)
{
    d->setFileName(fileName);
}

/*!
 * Returns the loader that is currently used to resolve and load URIs for the
 * SCXML compiler.
 *
 * \sa setLoader()
 */
QScxmlCompiler::Loader *QScxmlCompiler::loader() const
{
    return d->loader();
}

/*!
 * Sets \a newLoader to be used for resolving and loading URIs for the SCXML
 * compiler.
 *
 * \sa loader()
 */
void QScxmlCompiler::setLoader(QScxmlCompiler::Loader *newLoader)
{
    d->setLoader(newLoader);
}

/*!
 * Parses an SCXML file and creates a new state machine from it.
 *
 * If parsing is successful, the returned state machine can be initialized and started. If
 * parsing fails, QScxmlStateMachine::parseErrors() can be used to retrieve a list of errors.
 */
QScxmlStateMachine *QScxmlCompiler::compile()
{
    d->readDocument();
    if (d->errors().isEmpty()) {
        // Only verify the document if there were no parse errors: if there were any, the document
        // is incomplete and will contain errors for sure. There is no need to heap more errors on
        // top of other errors.
        d->verifyDocument();
    }
    return d->instantiateStateMachine();
}

/*!
 * \internal
 * Instantiates a new state machine from the parsed SCXML.
 *
 * If parsing is successful, the returned state machine can be initialized and started. If
 * parsing fails, QScxmlStateMachine::parseErrors() can be used to retrieve a list of errors.
 *
 * \note The instantiated state machine will not have an associated data model set.
 * \sa QScxmlCompilerPrivate::instantiateDataModel
 */
QScxmlStateMachine *QScxmlCompilerPrivate::instantiateStateMachine() const
{
#ifdef BUILD_QSCXMLC
    return nullptr;
#else // BUILD_QSCXMLC
    DocumentModel::ScxmlDocument *doc = scxmlDocument();
    if (doc && doc->root) {
        auto stateMachine = DynamicStateMachine::build(doc);
        instantiateDataModel(stateMachine);
        return stateMachine;
    } else {
        class InvalidStateMachine: public QScxmlStateMachine {
        public:
            InvalidStateMachine() : QScxmlStateMachine(&QScxmlStateMachine::staticMetaObject)
            {}
        };

        auto stateMachine = new InvalidStateMachine;
        QScxmlStateMachinePrivate::get(stateMachine)->parserData()->m_errors = errors();
        instantiateDataModel(stateMachine);
        return stateMachine;
    }
#endif // BUILD_QSCXMLC
}

/*!
 * \internal
 * Instantiates the data model as described in the SCXML file.
 *
 * After instantiation, the \a stateMachine takes ownership of the data model.
 */
void QScxmlCompilerPrivate::instantiateDataModel(QScxmlStateMachine *stateMachine) const
{
#ifdef BUILD_QSCXMLC
    Q_UNUSED(stateMachine);
#else
    if (!m_errors.isEmpty()) {
        qWarning() << "SCXML document has errors";
        return;
    }

    auto doc = scxmlDocument();
    auto root = doc ? doc->root : nullptr;
    if (root == nullptr) {
        qWarning() << "SCXML document has no root element";
    } else {
        QScxmlDataModel *dm = QScxmlDataModelPrivate::instantiateDataModel(root->dataModel);
        QScxmlStateMachinePrivate::get(stateMachine)->parserData()->m_ownedDataModel.reset(dm);
        stateMachine->setDataModel(dm);
        if (dm == nullptr)
            qWarning() << "No data-model instantiated";
    }
#endif // BUILD_QSCXMLC
}

/*!
 * Returns the list of parse errors.
 */
QList<QScxmlError> QScxmlCompiler::errors() const
{
    return d->errors();
}

bool QScxmlCompilerPrivate::ParserState::collectChars() {
    switch (kind) {
    case Content:
    case Data:
    case Script:
        return true;
    default:
        break;
    }
    return false;
}

bool QScxmlCompilerPrivate::ParserState::validChild(ParserState::Kind child) const {
    return validChild(kind, child);
}

bool QScxmlCompilerPrivate::ParserState::validChild(ParserState::Kind parent, ParserState::Kind child)
{
    switch (parent) {
    case ParserState::Scxml:
        switch (child) {
        case ParserState::State:
        case ParserState::Parallel:
        case ParserState::Final:
        case ParserState::DataModel:
        case ParserState::Script:
        case ParserState::Transition:
            return true;
        default:
            break;
        }
        return false;
    case ParserState::State:
        switch (child) {
        case ParserState::OnEntry:
        case ParserState::OnExit:
        case ParserState::Transition:
        case ParserState::Initial:
        case ParserState::State:
        case ParserState::Parallel:
        case ParserState::Final:
        case ParserState::History:
        case ParserState::DataModel:
        case ParserState::Invoke:
            return true;
        default:
            break;
        }
        return false;
    case ParserState::Parallel:
        switch (child) {
        case ParserState::OnEntry:
        case ParserState::OnExit:
        case ParserState::Transition:
        case ParserState::State:
        case ParserState::Parallel:
        case ParserState::History:
        case ParserState::DataModel:
        case ParserState::Invoke:
            return true;
        default:
            break;
        }
        return false;
    case ParserState::Transition:
        return isExecutableContent(child);
    case ParserState::Initial:
        return (child == ParserState::Transition);
    case ParserState::Final:
        switch (child) {
        case ParserState::OnEntry:
        case ParserState::OnExit:
        case ParserState::DoneData:
            return true;
        default:
            break;
        }
        return false;
    case ParserState::OnEntry:
    case ParserState::OnExit:
        return isExecutableContent(child);
    case ParserState::History:
        return child == ParserState::Transition;
    case ParserState::Raise:
        return false;
    case ParserState::If:
        return child == ParserState::ElseIf || child == ParserState::Else
                || isExecutableContent(child);
    case ParserState::ElseIf:
    case ParserState::Else:
        return false;
    case ParserState::Foreach:
        return isExecutableContent(child);
    case ParserState::Log:
        return false;
    case ParserState::DataModel:
        return (child == ParserState::Data);
    case ParserState::Data:
        return false;
    case ParserState::Assign:
        return false;
    case ParserState::DoneData:
    case ParserState::Send:
        return child == ParserState::Content || child == ParserState::Param;
    case ParserState::Content:
        return child == ParserState::Scxml || isExecutableContent(child);
    case ParserState::Param:
    case ParserState::Cancel:
        return false;
    case ParserState::Finalize:
        return isExecutableContent(child);
    case ParserState::Invoke:
        return child == ParserState::Content || child == ParserState::Finalize
                || child == ParserState::Param;
    case ParserState::Script:
    case ParserState::None:
        break;
    }
    return false;
}

bool QScxmlCompilerPrivate::ParserState::isExecutableContent(ParserState::Kind kind) {
    switch (kind) {
    case Raise:
    case Send:
    case Log:
    case Script:
    case Assign:
    case If:
    case Foreach:
    case Cancel:
    case Invoke:
        return true;
    default:
        break;
    }
    return false;
}

QScxmlCompilerPrivate::ParserState::Kind QScxmlCompilerPrivate::ParserState::nameToParserStateKind(QStringView name)
{
    static QMap<QString, ParserState::Kind> nameToKind;
    if (nameToKind.isEmpty()) {
        nameToKind.insert(QLatin1String("scxml"),       Scxml);
        nameToKind.insert(QLatin1String("state"),       State);
        nameToKind.insert(QLatin1String("parallel"),    Parallel);
        nameToKind.insert(QLatin1String("transition"),  Transition);
        nameToKind.insert(QLatin1String("initial"),     Initial);
        nameToKind.insert(QLatin1String("final"),       Final);
        nameToKind.insert(QLatin1String("onentry"),     OnEntry);
        nameToKind.insert(QLatin1String("onexit"),      OnExit);
        nameToKind.insert(QLatin1String("history"),     History);
        nameToKind.insert(QLatin1String("raise"),       Raise);
        nameToKind.insert(QLatin1String("if"),          If);
        nameToKind.insert(QLatin1String("elseif"),      ElseIf);
        nameToKind.insert(QLatin1String("else"),        Else);
        nameToKind.insert(QLatin1String("foreach"),     Foreach);
        nameToKind.insert(QLatin1String("log"),         Log);
        nameToKind.insert(QLatin1String("datamodel"),   DataModel);
        nameToKind.insert(QLatin1String("data"),        Data);
        nameToKind.insert(QLatin1String("assign"),      Assign);
        nameToKind.insert(QLatin1String("donedata"),    DoneData);
        nameToKind.insert(QLatin1String("content"),     Content);
        nameToKind.insert(QLatin1String("param"),       Param);
        nameToKind.insert(QLatin1String("script"),      Script);
        nameToKind.insert(QLatin1String("send"),        Send);
        nameToKind.insert(QLatin1String("cancel"),      Cancel);
        nameToKind.insert(QLatin1String("invoke"),      Invoke);
        nameToKind.insert(QLatin1String("finalize"),    Finalize);
    }
    QMap<QString, ParserState::Kind>::ConstIterator it = nameToKind.constBegin();
    const QMap<QString, ParserState::Kind>::ConstIterator itEnd = nameToKind.constEnd();
    while (it != itEnd) {
        if (it.key() == name)
            return it.value();
        ++it;
    }
    return None;
}

QStringList QScxmlCompilerPrivate::ParserState::requiredAttributes(QScxmlCompilerPrivate::ParserState::Kind kind)
{
    switch (kind) {
    case Scxml:      return QStringList() << QStringLiteral("version");
    case State:      return QStringList();
    case Parallel:   return QStringList();
    case Transition: return QStringList();
    case Initial:    return QStringList();
    case Final:      return QStringList();
    case OnEntry:    return QStringList();
    case OnExit:     return QStringList();
    case History:    return QStringList();
    case Raise:      return QStringList() << QStringLiteral("event");
    case If:         return QStringList() << QStringLiteral("cond");
    case ElseIf:     return QStringList() << QStringLiteral("cond");
    case Else:       return QStringList();
    case Foreach:    return QStringList() << QStringLiteral("array")
                                          << QStringLiteral("item");
    case Log:        return QStringList();
    case DataModel:  return QStringList();
    case Data:       return QStringList() << QStringLiteral("id");
    case Assign:     return QStringList() << QStringLiteral("location");
    case DoneData:   return QStringList();
    case Content:    return QStringList();
    case Param:      return QStringList() << QStringLiteral("name");
    case Script:     return QStringList();
    case Send:       return QStringList();
    case Cancel:     return QStringList();
    case Invoke:     return QStringList();
    case Finalize:   return QStringList();
    default:         return QStringList();
    }
    return QStringList();
}

QStringList QScxmlCompilerPrivate::ParserState::optionalAttributes(QScxmlCompilerPrivate::ParserState::Kind kind)
{
    switch (kind) {
    case Scxml:      return QStringList() << QStringLiteral("initial")
                                          << QStringLiteral("datamodel")
                                          << QStringLiteral("binding")
                                          << QStringLiteral("name");
    case State:      return QStringList() << QStringLiteral("id")
                                          << QStringLiteral("initial");
    case Parallel:   return QStringList() << QStringLiteral("id");
    case Transition: return QStringList() << QStringLiteral("event")
                                          << QStringLiteral("cond")
                                          << QStringLiteral("target")
                                          << QStringLiteral("type");
    case Initial:    return QStringList();
    case Final:      return QStringList() << QStringLiteral("id");
    case OnEntry:    return QStringList();
    case OnExit:     return QStringList();
    case History:    return QStringList() << QStringLiteral("id")
                                          << QStringLiteral("type");
    case Raise:      return QStringList();
    case If:         return QStringList();
    case ElseIf:     return QStringList();
    case Else:       return QStringList();
    case Foreach:    return QStringList() << QStringLiteral("index");
    case Log:        return QStringList() << QStringLiteral("label")
                                          << QStringLiteral("expr");
    case DataModel:  return QStringList();
    case Data:       return QStringList() << QStringLiteral("src")
                                          << QStringLiteral("expr");
    case Assign:     return QStringList() << QStringLiteral("expr");
    case DoneData:   return QStringList();
    case Content:    return QStringList() << QStringLiteral("expr");
    case Param:      return QStringList() << QStringLiteral("expr")
                                          << QStringLiteral("location");
    case Script:     return QStringList() << QStringLiteral("src");
    case Send:       return QStringList() << QStringLiteral("event")
                                          << QStringLiteral("eventexpr")
                                          << QStringLiteral("id")
                                          << QStringLiteral("idlocation")
                                          << QStringLiteral("type")
                                          << QStringLiteral("typeexpr")
                                          << QStringLiteral("namelist")
                                          << QStringLiteral("delay")
                                          << QStringLiteral("delayexpr")
                                          << QStringLiteral("target")
                                          << QStringLiteral("targetexpr");
    case Cancel:     return QStringList() << QStringLiteral("sendid")
                                          << QStringLiteral("sendidexpr");
    case Invoke:     return QStringList() << QStringLiteral("type")
                                          << QStringLiteral("typeexpr")
                                          << QStringLiteral("src")
                                          << QStringLiteral("srcexpr")
                                          << QStringLiteral("id")
                                          << QStringLiteral("idlocation")
                                          << QStringLiteral("namelist")
                                          << QStringLiteral("autoforward");
    case Finalize:   return QStringList();
    default:         return QStringList();
    }
    return QStringList();
}

DocumentModel::Node::~Node()
{
}

DocumentModel::AbstractState *DocumentModel::Node::asAbstractState()
{
    if (State *state = asState())
        return state;
    if (HistoryState *history = asHistoryState())
        return history;
    return nullptr;
}

void DocumentModel::DataElement::accept(DocumentModel::NodeVisitor *visitor)
{
    visitor->visit(this);
}

void DocumentModel::Param::accept(DocumentModel::NodeVisitor *visitor)
{
    visitor->visit(this);
}

void DocumentModel::DoneData::accept(DocumentModel::NodeVisitor *visitor)
{
    if (visitor->visit(this)) {
        for (Param *param : std::as_const(params))
            param->accept(visitor);
    }
    visitor->endVisit(this);
}

void DocumentModel::Send::accept(DocumentModel::NodeVisitor *visitor)
{
    if (visitor->visit(this)) {
        visitor->visit(params);
    }
    visitor->endVisit(this);
}

void DocumentModel::Invoke::accept(DocumentModel::NodeVisitor *visitor)
{
    if (visitor->visit(this)) {
        visitor->visit(params);
        visitor->visit(&finalize);
    }
    visitor->endVisit(this);
}

void DocumentModel::Raise::accept(DocumentModel::NodeVisitor *visitor)
{
    visitor->visit(this);
}

void DocumentModel::Log::accept(DocumentModel::NodeVisitor *visitor)
{
    visitor->visit(this);
}

void DocumentModel::Script::accept(DocumentModel::NodeVisitor *visitor)
{
    visitor->visit(this);
}

void DocumentModel::Assign::accept(DocumentModel::NodeVisitor *visitor)
{
    visitor->visit(this);
}

void DocumentModel::If::accept(DocumentModel::NodeVisitor *visitor)
{
    if (visitor->visit(this)) {
        visitor->visit(blocks);
    }
    visitor->endVisit(this);
}

void DocumentModel::Foreach::accept(DocumentModel::NodeVisitor *visitor)
{
    if (visitor->visit(this)) {
        visitor->visit(&block);
    }
    visitor->endVisit(this);
}

void DocumentModel::Cancel::accept(DocumentModel::NodeVisitor *visitor)
{
    visitor->visit(this);
}

void DocumentModel::State::accept(DocumentModel::NodeVisitor *visitor)
{
    if (visitor->visit(this)) {
        visitor->visit(dataElements);
        visitor->visit(children);
        visitor->visit(onEntry);
        visitor->visit(onExit);
        if (doneData)
            doneData->accept(visitor);
        for (Invoke *invoke : std::as_const(invokes))
            invoke->accept(visitor);
    }
    visitor->endVisit(this);
}

void DocumentModel::Transition::accept(DocumentModel::NodeVisitor *visitor)
{
    if (visitor->visit(this)) {
        visitor->visit(&instructionsOnTransition);
    }
    visitor->endVisit(this);
}

void DocumentModel::HistoryState::accept(DocumentModel::NodeVisitor *visitor)
{
    if (visitor->visit(this)) {
        if (Transition *t = defaultConfiguration())
            t->accept(visitor);
    }
    visitor->endVisit(this);
}

void DocumentModel::Scxml::accept(DocumentModel::NodeVisitor *visitor)
{
    if (visitor->visit(this)) {
        visitor->visit(children);
        visitor->visit(dataElements);
        if (script)
            script->accept(visitor);
        visitor->visit(&initialSetup);
    }
    visitor->endVisit(this);
}

DocumentModel::NodeVisitor::~NodeVisitor()
{}

/*!
 * \class QScxmlCompiler::Loader
 * \brief The Loader class is a URI resolver and resource loader for an SCXML compiler.
 * \since 5.8
 * \inmodule QtScxml
 */

/*!
 * Creates a new loader.
 */
QScxmlCompiler::Loader::Loader()
{
}

/*!
 * Destroys the loader.
 */
QScxmlCompiler::Loader::~Loader()
{}

/*!
 * \fn QScxmlCompiler::Loader::load(const QString &name, const QString &baseDir, QStringList *errors)
 * Resolves the URI \a name and loads an SCXML file from the directory
 * specified by \a baseDir. \a errors contains information about the errors that
 * might have occurred.
 *
 * Returns a QByteArray that stores the contents of the file.
 */

QScxmlCompilerPrivate *QScxmlCompilerPrivate::get(QScxmlCompiler *compiler)
{
    return compiler->d;
}

QScxmlCompilerPrivate::QScxmlCompilerPrivate(QXmlStreamReader *reader)
    : m_currentState(nullptr)
    , m_loader(&m_defaultLoader)
    , m_reader(reader)
{}

bool QScxmlCompilerPrivate::verifyDocument()
{
    if (!m_doc)
        return false;

    auto handler = [this](const DocumentModel::XmlLocation &location, const QString &msg) {
        this->addError(location, msg);
    };

    if (ScxmlVerifier(handler).verify(m_doc.get()))
        return true;
    else
        return false;
}

DocumentModel::ScxmlDocument *QScxmlCompilerPrivate::scxmlDocument() const
{
    return m_doc && m_errors.isEmpty() ? m_doc.get() : nullptr;
}

QString QScxmlCompilerPrivate::fileName() const
{
    return m_fileName;
}

void QScxmlCompilerPrivate::setFileName(const QString &fileName)
{
    m_fileName = fileName;
}

QScxmlCompiler::Loader *QScxmlCompilerPrivate::loader() const
{
    return m_loader;
}

void QScxmlCompilerPrivate::setLoader(QScxmlCompiler::Loader *loader)
{
    m_loader = loader;
}

void QScxmlCompilerPrivate::parseSubDocument(DocumentModel::Invoke *parentInvoke,
                                           QXmlStreamReader *reader,
                                           const QString &fileName)
{
    QScxmlCompiler p(reader);
    p.setFileName(fileName);
    p.setLoader(loader());
    p.d->readDocument();
    parentInvoke->content.reset(p.d->m_doc.release());
    m_doc->allSubDocuments.append(parentInvoke->content.data());
    m_errors.append(p.errors());
}

bool QScxmlCompilerPrivate::parseSubElement(DocumentModel::Invoke *parentInvoke,
                                          QXmlStreamReader *reader,
                                          const QString &fileName)
{
    QScxmlCompiler p(reader);
    p.setFileName(fileName);
    p.setLoader(loader());
    p.d->resetDocument();
    bool ok = p.d->readElement();
    parentInvoke->content.reset(p.d->m_doc.release());
    m_doc->allSubDocuments.append(parentInvoke->content.data());
    m_errors.append(p.errors());
    return ok;
}

bool QScxmlCompilerPrivate::preReadElementScxml()
{
    if (m_doc->root) {
        addError(QLatin1String("Doc root already allocated"));
        return false;
    }
    m_doc->root = new DocumentModel::Scxml(xmlLocation());

    auto scxml = m_doc->root;
    const QXmlStreamAttributes attributes = m_reader->attributes();
    if (attributes.hasAttribute(QStringLiteral("initial"))) {
        const QString initial = attributes.value(QStringLiteral("initial")).toString();
        scxml->initial += initial.split(QChar::Space, Qt::SkipEmptyParts);
    }

    const QStringView datamodel = attributes.value(QLatin1String("datamodel"));
    if (datamodel.isEmpty() || datamodel == QLatin1String("null")) {
        scxml->dataModel = DocumentModel::Scxml::NullDataModel;
    } else if (datamodel == QLatin1String("ecmascript")) {
        scxml->dataModel = DocumentModel::Scxml::JSDataModel;
    } else if (datamodel.startsWith(QLatin1String("cplusplus"))) {
        scxml->dataModel = DocumentModel::Scxml::CppDataModel;
        int firstColon = datamodel.indexOf(QLatin1Char(':'));
        if (firstColon == -1) {
            scxml->cppDataModelClassName = attributes.value(QStringLiteral("name")).toString() + QStringLiteral("DataModel");
            scxml->cppDataModelHeaderName = scxml->cppDataModelClassName + QStringLiteral(".h");
        } else {
            int lastColon = datamodel.lastIndexOf(QLatin1Char(':'));
            if (lastColon == -1) {
                lastColon = datamodel.size();
            } else {
                scxml->cppDataModelHeaderName = datamodel.mid(lastColon + 1).toString();
            }
            scxml->cppDataModelClassName = datamodel.mid(firstColon + 1, lastColon - firstColon - 1).toString();
        }
    } else {
        addError(QStringLiteral("Unsupported data model '%1' in scxml")
                 .arg(datamodel.toString()));
    }
    const QStringView binding = attributes.value(QLatin1String("binding"));
    if (binding.isEmpty() || binding == QLatin1String("early")) {
        scxml->binding = DocumentModel::Scxml::EarlyBinding;
    } else if (binding == QLatin1String("late")) {
        scxml->binding = DocumentModel::Scxml::LateBinding;
    } else {
        addError(QStringLiteral("Unsupperted binding type '%1'")
                 .arg(binding.toString()));
        return false;
    }
    const QStringView name = attributes.value(QLatin1String("name"));
    if (!name.isEmpty()) {
        scxml->name = name.toString();
    }
    m_currentState = m_doc->root;
    current().instructionContainer = &m_doc->root->initialSetup;
    return true;
}


bool QScxmlCompilerPrivate::preReadElementState()
{
    const QXmlStreamAttributes attributes = m_reader->attributes();
    auto newState = m_doc->newState(m_currentState, DocumentModel::State::Normal, xmlLocation());
    if (!maybeId(attributes, &newState->id))
        return false;

    if (attributes.hasAttribute(QStringLiteral("initial"))) {
        const QString initial = attributes.value(QStringLiteral("initial")).toString();
        newState->initial += initial.split(QChar::Space, Qt::SkipEmptyParts);
    }
    m_currentState = newState;
    return true;
}

bool QScxmlCompilerPrivate::preReadElementParallel()
{
    const QXmlStreamAttributes attributes = m_reader->attributes();
    auto newState = m_doc->newState(m_currentState, DocumentModel::State::Parallel, xmlLocation());
    if (!maybeId(attributes, &newState->id))
        return false;

    m_currentState = newState;
    return true;
}

bool QScxmlCompilerPrivate::preReadElementInitial()
{
    DocumentModel::AbstractState *parent = currentParent();
    if (!parent) {
        addError(QStringLiteral("<initial> found outside a state"));
        return false;
    }

    DocumentModel::State *parentState = parent->asState();
    if (!parentState) {
        addError(QStringLiteral("<initial> found outside a state"));
        return false;
    }

    if (parentState->type == DocumentModel::State::Parallel) {
        addError(QStringLiteral("Explicit initial state for parallel states not supported (only implicitly through the initial states of its substates)"));
        return false;
    }
    return true;
}

bool QScxmlCompilerPrivate::preReadElementTransition()
{
    // Parser stack at this point:
    // <transition>
    // <initial>
    // <state> or <scxml>
    //
    // Or:
    // <transition>
    // <state> or <scxml>

    DocumentModel::Transition *transition = nullptr;
    if (previous().kind == ParserState::Initial) {
        transition = m_doc->newTransition(nullptr, xmlLocation());
        const auto &initialParentState = m_stack.at(m_stack.size() - 3);
        if (initialParentState.kind == ParserState::Scxml) {
            m_currentState->asScxml()->initialTransition = transition;
        } else if (initialParentState.kind == ParserState::State) {
            m_currentState->asState()->initialTransition = transition;
        } else {
            Q_UNREACHABLE();
        }
    } else {
        transition = m_doc->newTransition(m_currentState, xmlLocation());
    }

    const QXmlStreamAttributes attributes = m_reader->attributes();
    transition->events = attributes.value(QLatin1String("event")).toString().split(QLatin1Char(' '), Qt::SkipEmptyParts);
    transition->targets = attributes.value(QLatin1String("target")).toString().split(QLatin1Char(' '), Qt::SkipEmptyParts);
    if (attributes.hasAttribute(QStringLiteral("cond")))
        transition->condition.reset(new QString(attributes.value(QLatin1String("cond")).toString()));
    QStringView type = attributes.value(QLatin1String("type"));
    if (type.isEmpty() || type == QLatin1String("external")) {
        transition->type = DocumentModel::Transition::External;
    } else if (type == QLatin1String("internal")) {
        transition->type = DocumentModel::Transition::Internal;
    } else {
        addError(QStringLiteral("invalid transition type '%1', valid values are 'external' and 'internal'").arg(type.toString()));
        return true; // TODO: verify me
    }
    current().instructionContainer = &transition->instructionsOnTransition;
    return true;
}

bool QScxmlCompilerPrivate::preReadElementFinal()
{
    const QXmlStreamAttributes attributes = m_reader->attributes();
    auto newState = m_doc->newState(m_currentState, DocumentModel::State::Final, xmlLocation());
    if (!maybeId(attributes, &newState->id))
        return false;
    m_currentState = newState;
    return true;
}

bool QScxmlCompilerPrivate::preReadElementHistory()
{
    const QXmlStreamAttributes attributes = m_reader->attributes();

    DocumentModel::AbstractState *parent = currentParent();
    if (!parent) {
        addError(QStringLiteral("<history> found outside a state"));
        return false;
    }
    auto newState = m_doc->newHistoryState(parent, xmlLocation());
    if (!maybeId(attributes, &newState->id))
        return false;

    const QStringView type = attributes.value(QLatin1String("type"));
    if (type.isEmpty() || type == QLatin1String("shallow")) {
        newState->type = DocumentModel::HistoryState::Shallow;
    } else if (type == QLatin1String("deep")) {
        newState->type = DocumentModel::HistoryState::Deep;
    } else {
        addError(QStringLiteral("invalid history type %1, valid values are 'shallow' and 'deep'").arg(type.toString()));
        return false;
    }
    m_currentState = newState;
    return true;
}

bool QScxmlCompilerPrivate::preReadElementOnEntry()
{
    const ParserState::Kind previousKind = previous().kind;
    switch (previousKind) {
    case ParserState::Final:
    case ParserState::State:
    case ParserState::Parallel:
        if (DocumentModel::State *s = m_currentState->asState()) {
            current().instructionContainer = m_doc->newSequence(&s->onEntry);
            break;
        }
        Q_FALLTHROUGH();
    default:
        addError(QStringLiteral("unexpected container state for onentry"));
        break;
    }
    return true;
}

bool QScxmlCompilerPrivate::preReadElementOnExit()
{
    ParserState::Kind previousKind = previous().kind;
    switch (previousKind) {
    case ParserState::Final:
    case ParserState::State:
    case ParserState::Parallel:
        if (DocumentModel::State *s = m_currentState->asState()) {
            current().instructionContainer = m_doc->newSequence(&s->onExit);
            break;
        }
        Q_FALLTHROUGH();
    default:
        addError(QStringLiteral("unexpected container state for onexit"));
        break;
    }
    return true;
}

bool QScxmlCompilerPrivate::preReadElementRaise()
{
    const QXmlStreamAttributes attributes = m_reader->attributes();
    auto raise = m_doc->newNode<DocumentModel::Raise>(xmlLocation());
    raise->event = attributes.value(QLatin1String("event")).toString();
    current().instruction = raise;
    return true;
}

bool QScxmlCompilerPrivate::preReadElementIf()
{
    const QXmlStreamAttributes attributes = m_reader->attributes();
    auto *ifI = m_doc->newNode<DocumentModel::If>(xmlLocation());
    current().instruction = ifI;
    ifI->conditions.append(attributes.value(QLatin1String("cond")).toString());
    current().instructionContainer = m_doc->newSequence(&ifI->blocks);
    return true;
}

bool QScxmlCompilerPrivate::preReadElementElseIf()
{
    const QXmlStreamAttributes attributes = m_reader->attributes();

    DocumentModel::If *ifI = lastIf();
    if (!ifI)
        return false;

    ifI->conditions.append(attributes.value(QLatin1String("cond")).toString());
    previous().instructionContainer = m_doc->newSequence(&ifI->blocks);
    return true;
}

bool QScxmlCompilerPrivate::preReadElementElse()
{
    DocumentModel::If *ifI = lastIf();
    if (!ifI)
        return false;

    previous().instructionContainer = m_doc->newSequence(&ifI->blocks);
    return true;
}

bool QScxmlCompilerPrivate::preReadElementForeach()
{
    const QXmlStreamAttributes attributes = m_reader->attributes();
    auto foreachI = m_doc->newNode<DocumentModel::Foreach>(xmlLocation());
    foreachI->array = attributes.value(QLatin1String("array")).toString();
    foreachI->item = attributes.value(QLatin1String("item")).toString();
    foreachI->index = attributes.value(QLatin1String("index")).toString();
    current().instruction = foreachI;
    current().instructionContainer = &foreachI->block;
    return true;
}

bool QScxmlCompilerPrivate::preReadElementLog()
{
    const QXmlStreamAttributes attributes = m_reader->attributes();
    auto logI = m_doc->newNode<DocumentModel::Log>(xmlLocation());
    logI->label = attributes.value(QLatin1String("label")).toString();
    logI->expr = attributes.value(QLatin1String("expr")).toString();
    current().instruction = logI;
    return true;
}

bool QScxmlCompilerPrivate::preReadElementDataModel()
{
    return true;
}

bool QScxmlCompilerPrivate::preReadElementData()
{
    const QXmlStreamAttributes attributes = m_reader->attributes();
    auto data = m_doc->newNode<DocumentModel::DataElement>(xmlLocation());
    data->id = attributes.value(QLatin1String("id")).toString();
    data->src = attributes.value(QLatin1String("src")).toString();
    data->expr = attributes.value(QLatin1String("expr")).toString();
    if (DocumentModel::Scxml *scxml = m_currentState->asScxml()) {
        scxml->dataElements.append(data);
    } else if (DocumentModel::State *state = m_currentState->asState()) {
        state->dataElements.append(data);
    } else {
        Q_UNREACHABLE();
    }
    return true;
}

bool QScxmlCompilerPrivate::preReadElementAssign()
{
    const QXmlStreamAttributes attributes = m_reader->attributes();
    auto assign = m_doc->newNode<DocumentModel::Assign>(xmlLocation());
    assign->location = attributes.value(QLatin1String("location")).toString();
    assign->expr = attributes.value(QLatin1String("expr")).toString();
    current().instruction = assign;
    return true;
}

bool QScxmlCompilerPrivate::preReadElementDoneData()
{
    DocumentModel::State *s = m_currentState->asState();
    if (s && s->type == DocumentModel::State::Final) {
        if (s->doneData) {
            addError(QLatin1String("state can only have one donedata"));
        } else {
            s->doneData = m_doc->newNode<DocumentModel::DoneData>(xmlLocation());
        }
    } else {
        addError(QStringLiteral("donedata can only occur in a final state"));
    }
    return true;
}

bool QScxmlCompilerPrivate::preReadElementContent()
{
    const QXmlStreamAttributes attributes = m_reader->attributes();
    ParserState::Kind previousKind = previous().kind;
    switch (previousKind) {
    case ParserState::DoneData: {
        DocumentModel::State *s = m_currentState->asState();
        Q_ASSERT(s);
        s->doneData->expr = attributes.value(QLatin1String("expr")).toString();
    } break;
    case ParserState::Send: {
        DocumentModel::Send *s = previous().instruction->asSend();
        Q_ASSERT(s);
        s->contentexpr = attributes.value(QLatin1String("expr")).toString();
    } break;
    case ParserState::Invoke: {
        DocumentModel::Invoke *i = previous().instruction->asInvoke();
        Q_ASSERT(i);
        Q_UNUSED(i);
        if (attributes.hasAttribute(QStringLiteral("expr"))) {
            addError(QStringLiteral("expr attribute in content of invoke is not supported"));
            break;
        }
    } break;
    default:
        addError(QStringLiteral("unexpected parent of content %1").arg(previous().kind));
    }
    return true;
}

bool QScxmlCompilerPrivate::preReadElementParam()
{
    const QXmlStreamAttributes attributes = m_reader->attributes();
    auto param = m_doc->newNode<DocumentModel::Param>(xmlLocation());
    param->name = attributes.value(QLatin1String("name")).toString();
    param->expr = attributes.value(QLatin1String("expr")).toString();
    param->location = attributes.value(QLatin1String("location")).toString();

    ParserState::Kind previousKind = previous().kind;
    switch (previousKind) {
    case ParserState::DoneData: {
        DocumentModel::State *s = m_currentState->asState();
        Q_ASSERT(s);
        Q_ASSERT(s->doneData);
        s->doneData->params.append(param);
    } break;
    case ParserState::Send: {
        DocumentModel::Send *s = previous().instruction->asSend();
        Q_ASSERT(s);
        s->params.append(param);
    } break;
    case ParserState::Invoke: {
        DocumentModel::Invoke *i = previous().instruction->asInvoke();
        Q_ASSERT(i);
        i->params.append(param);
    } break;
    default:
        addError(QStringLiteral("unexpected parent of param %1").arg(previous().kind));
    }
    return true;
}

bool QScxmlCompilerPrivate::preReadElementScript()
{
    const QXmlStreamAttributes attributes = m_reader->attributes();
    auto *script = m_doc->newNode<DocumentModel::Script>(xmlLocation());
    script->src = attributes.value(QLatin1String("src")).toString();
    current().instruction = script;
    return true;
}

bool QScxmlCompilerPrivate::preReadElementSend()
{
    const QXmlStreamAttributes attributes = m_reader->attributes();
    auto *send = m_doc->newNode<DocumentModel::Send>(xmlLocation());
    send->event = attributes.value(QLatin1String("event")).toString();
    send->eventexpr = attributes.value(QLatin1String("eventexpr")).toString();
    send->delay = attributes.value(QLatin1String("delay")).toString();
    send->delayexpr = attributes.value(QLatin1String("delayexpr")).toString();
    send->id = attributes.value(QLatin1String("id")).toString();
    send->idLocation = attributes.value(QLatin1String("idlocation")).toString();
    send->type = attributes.value(QLatin1String("type")).toString();
    send->typeexpr = attributes.value(QLatin1String("typeexpr")).toString();
    send->target = attributes.value(QLatin1String("target")).toString();
    send->targetexpr = attributes.value(QLatin1String("targetexpr")).toString();
    if (attributes.hasAttribute(QLatin1String("namelist")))
        send->namelist = attributes.value(QLatin1String("namelist")).toString().split(QLatin1Char(' '), Qt::SkipEmptyParts);
    current().instruction = send;
    return true;
}

bool QScxmlCompilerPrivate::preReadElementCancel()
{
    const QXmlStreamAttributes attributes = m_reader->attributes();
    auto *cancel = m_doc->newNode<DocumentModel::Cancel>(xmlLocation());
    cancel->sendid = attributes.value(QLatin1String("sendid")).toString();
    cancel->sendidexpr = attributes.value(QLatin1String("sendidexpr")).toString();
    current().instruction = cancel;
    return true;
}

bool QScxmlCompilerPrivate::preReadElementInvoke()
{
    const QXmlStreamAttributes attributes = m_reader->attributes();
    DocumentModel::State *parentState = m_currentState->asState();
    if (!parentState ||
            (parentState->type != DocumentModel::State::Normal && parentState->type != DocumentModel::State::Parallel)) {
        addError(QStringLiteral("invoke can only occur in <state> or <parallel>"));
        return true; // TODO: verify me
    }
    auto *invoke = m_doc->newNode<DocumentModel::Invoke>(xmlLocation());
    parentState->invokes.append(invoke);
    invoke->src = attributes.value(QLatin1String("src")).toString();
    invoke->srcexpr = attributes.value(QLatin1String("srcexpr")).toString();
    invoke->id = attributes.value(QLatin1String("id")).toString();
    invoke->idLocation = attributes.value(QLatin1String("idlocation")).toString();
    invoke->type = attributes.value(QLatin1String("type")).toString();
    invoke->typeexpr = attributes.value(QLatin1String("typeexpr")).toString();
    QStringView autoforwardS = attributes.value(QLatin1String("autoforward"));
    if (autoforwardS.compare(QLatin1String("true"), Qt::CaseInsensitive) == 0
            || autoforwardS.compare(QLatin1String("yes"), Qt::CaseInsensitive) == 0
            || autoforwardS.compare(QLatin1String("t"), Qt::CaseInsensitive) == 0
            || autoforwardS.compare(QLatin1String("y"), Qt::CaseInsensitive) == 0
            || autoforwardS == QLatin1String("1"))
        invoke->autoforward = true;
    else
        invoke->autoforward = false;
    invoke->namelist = attributes.value(QLatin1String("namelist")).toString().split(QLatin1Char(' '), Qt::SkipEmptyParts);
    current().instruction = invoke;
    return true;
}

bool QScxmlCompilerPrivate::preReadElementFinalize()
{
    auto instr = previous().instruction;
    if (!instr) {
        addError(QStringLiteral("no previous instruction found for <finalize>"));
        return false;
    }
    auto invoke = instr->asInvoke();
    if (!invoke) {
        addError(QStringLiteral("instruction before <finalize> is not <invoke>"));
        return false;
    }
    current().instructionContainer = &invoke->finalize;
    return true;
}

bool QScxmlCompilerPrivate::postReadElementScxml()
{
    return true;
}

bool QScxmlCompilerPrivate::postReadElementState()
{
    currentStateUp();
    return true;
}

bool QScxmlCompilerPrivate::postReadElementParallel()
{
    currentStateUp();
    return true;
}

bool QScxmlCompilerPrivate::postReadElementInitial()
{
    return true;
}

bool QScxmlCompilerPrivate::postReadElementTransition()
{
    return true;
}

bool QScxmlCompilerPrivate::postReadElementFinal()
{
    currentStateUp();
    return true;
}

bool QScxmlCompilerPrivate::postReadElementHistory()
{
    currentStateUp();
    return true;
}

bool QScxmlCompilerPrivate::postReadElementOnEntry()
{
    return true;
}

bool QScxmlCompilerPrivate::postReadElementOnExit()
{
    return true;
}

bool QScxmlCompilerPrivate::postReadElementRaise()
{
    return flushInstruction();
}

bool QScxmlCompilerPrivate::postReadElementIf()
{
    return flushInstruction();
}

bool QScxmlCompilerPrivate::postReadElementElseIf()
{
    return true;
}

bool QScxmlCompilerPrivate::postReadElementElse()
{
    return true;
}

bool QScxmlCompilerPrivate::postReadElementForeach()
{
    return flushInstruction();
}

bool QScxmlCompilerPrivate::postReadElementLog()
{
    return flushInstruction();
}

bool QScxmlCompilerPrivate::postReadElementDataModel()
{
    return true;
}

bool QScxmlCompilerPrivate::postReadElementData()
{
    const ParserState parserState = current();
    DocumentModel::DataElement *data = nullptr;
    if (auto state = m_currentState->asState()) {
        data = state->dataElements.last();
    } else if (auto scxml = m_currentState->asScxml()) {
        data = scxml->dataElements.last();
    } else {
        Q_UNREACHABLE();
    }
    if (!data->src.isEmpty() && !data->expr.isEmpty()) {
        addError(QStringLiteral("data element with both 'src' and 'expr' attributes"));
        return false;
    }
    if (!parserState.chars.trimmed().isEmpty()) {
        if (!data->src.isEmpty()) {
            addError(QStringLiteral("data element with both 'src' attribute and CDATA"));
            return false;
        } else if (!data->expr.isEmpty()) {
            addError(QStringLiteral("data element with both 'expr' attribute and CDATA"));
            return false;
        } else {
            // w3c-ecma/test558 - "if a child element of <data> is not a XML,
            // treat it as a string with whitespace normalization"
            // We've modified the test, so that a string is enclosed with quotes.
            data->expr = parserState.chars;
        }
    } else if (!data->src.isEmpty()) {
        if (!m_loader) {
            addError(QStringLiteral("cannot parse a document with external dependencies without a loader"));
        } else {
            bool ok;
            const QByteArray ba = load(data->src, &ok);
            if (!ok) {
                addError(QStringLiteral("failed to load external dependency"));
            } else {
                // w3c-ecma/test558 - "if XML is loaded via "src" attribute,
                // treat it as a string with whitespace normalization"
                // We've enclosed the text in file with quotes.
                data->expr = QString::fromUtf8(ba);
            }
        }
    }
    return true;
}

bool QScxmlCompilerPrivate::postReadElementAssign()
{
    return flushInstruction();
}

bool QScxmlCompilerPrivate::postReadElementDoneData()
{
    return true;
}

bool QScxmlCompilerPrivate::postReadElementContent()
{
    const ParserState parserState = current();
    if (!parserState.chars.trimmed().isEmpty()) {

        switch (previous().kind) {
        case ParserState::DoneData: // see test529
            m_currentState->asState()->doneData->contents = parserState.chars.simplified();
            break;
        case ParserState::Send: // see test179
            previous().instruction->asSend()->content = parserState.chars.simplified();
            break;
        default:
            break;
        }
    }
    return true;
}

bool QScxmlCompilerPrivate::postReadElementParam()
{
    return true;
}

bool QScxmlCompilerPrivate::postReadElementScript()
{
    const ParserState parserState = current();
    DocumentModel::Script *scriptI = parserState.instruction->asScript();
    if (!parserState.chars.trimmed().isEmpty()) {
        scriptI->content = parserState.chars.trimmed();
        if (!scriptI->src.isEmpty())
            addError(QStringLiteral("both src and source content given to script, will ignore external content"));
    } else if (!scriptI->src.isEmpty()) {
        if (!m_loader) {
            addError(QStringLiteral("cannot parse a document with external dependencies without a loader"));
        } else {
            bool ok;
            const QByteArray data = load(scriptI->src, &ok);
            if (!ok) {
                addError(QStringLiteral("failed to load external dependency"));
            } else {
                scriptI->content = QString::fromUtf8(data);
            }
        }
    } else {
        addError(scriptI->xmlLocation,
                QStringLiteral("neither src nor any content has been given in the script tag"));
    }
    return flushInstruction();
}

bool QScxmlCompilerPrivate::postReadElementSend()
{
    return flushInstruction();
}

bool QScxmlCompilerPrivate::postReadElementCancel()
{
    return flushInstruction();
}

bool QScxmlCompilerPrivate::postReadElementInvoke()
{
    DocumentModel::Invoke *i = current().instruction->asInvoke();
    const QString fileName = i->src;
    if (!i->content.data()) {
        if (!fileName.isEmpty()) {
            bool ok = true;
            const QByteArray data = load(fileName, &ok);
            if (!ok) {
                addError(QStringLiteral("failed to load external dependency"));
            } else {
                QXmlStreamReader reader(data);
                parseSubDocument(i, &reader, fileName);
            }
        }
    } else if (!fileName.isEmpty()) {
        addError(QStringLiteral("both src and content given to invoke"));
    }

    return true;
}

bool QScxmlCompilerPrivate::postReadElementFinalize()
{
    return true;
}

void QScxmlCompilerPrivate::resetDocument()
{
    m_doc.reset(new DocumentModel::ScxmlDocument(fileName()));
}

bool QScxmlCompilerPrivate::readDocument()
{
    resetDocument();
    m_currentState = m_doc->root;
    for (bool finished = false; !finished && !m_reader->hasError();) {
        switch (m_reader->readNext()) {
        case QXmlStreamReader::StartElement : {
            const QStringView newTag = m_reader->name();
            const ParserState::Kind newElementKind = ParserState::nameToParserStateKind(newTag);

            auto ns = m_reader->namespaceUri();

            if (ns != scxmlNamespace) {
                m_reader->skipCurrentElement();
            } else if (newElementKind == ParserState::None) {
                addError(QStringLiteral("Unknown element %1").arg(newTag.toString()));
                m_reader->skipCurrentElement();
            } else if (newElementKind == ParserState::Scxml) {
                if (readElement() == false)
                    return false;
            } else {
                addError(QStringLiteral("Unexpected element %1").arg(newTag.toString()));
                m_reader->skipCurrentElement();
            }
        }
            break;
        case QXmlStreamReader::EndElement :
            finished = true;
            break;
        default :
            break;
        }
    }
    if (!m_doc->root) {
        addError(QStringLiteral("Missing root element"));
        return false;
    }

    if (m_reader->hasError() && m_reader->error() != QXmlStreamReader::PrematureEndOfDocumentError) {
        addError(QStringLiteral("Error parsing SCXML file: %1").arg(m_reader->errorString()));
        return false;
    }

    return true;
}

bool QScxmlCompilerPrivate::readElement()
{
    const QStringView currentTag = m_reader->name();
    const QXmlStreamAttributes attributes = m_reader->attributes();

    const ParserState::Kind elementKind = ParserState::nameToParserStateKind(currentTag);

    if (!checkAttributes(attributes, elementKind))
        return false;

    if (elementKind == ParserState::Scxml && m_doc->root) {
        if (!hasPrevious()) {
            addError(QStringLiteral("misplaced scxml"));
            return false;
        }

        DocumentModel::Invoke *i = previous().instruction->asInvoke();
        if (!i) {
            addError(QStringLiteral("misplaced scxml"));
            return false;
        }

        return parseSubElement(i, m_reader, m_fileName);
    }

    if (elementKind != ParserState::Scxml && !m_stack.size()) {
        addError(QStringLiteral("misplaced %1").arg(currentTag.toString()));
        return false;
    }

    ParserState pNew = ParserState(elementKind);

    m_stack.append(pNew);

    switch (elementKind) {
    case ParserState::Scxml:      if (!preReadElementScxml())      return false; break;
    case ParserState::State:      if (!preReadElementState())      return false; break;
    case ParserState::Parallel:   if (!preReadElementParallel())   return false; break;
    case ParserState::Initial:    if (!preReadElementInitial())    return false; break;
    case ParserState::Transition: if (!preReadElementTransition()) return false; break;
    case ParserState::Final:      if (!preReadElementFinal())      return false; break;
    case ParserState::History:    if (!preReadElementHistory())    return false; break;
    case ParserState::OnEntry:    if (!preReadElementOnEntry())    return false; break;
    case ParserState::OnExit:     if (!preReadElementOnExit())     return false; break;
    case ParserState::Raise:      if (!preReadElementRaise())      return false; break;
    case ParserState::If:         if (!preReadElementIf())         return false; break;
    case ParserState::ElseIf:     if (!preReadElementElseIf())     return false; break;
    case ParserState::Else:       if (!preReadElementElse())       return false; break;
    case ParserState::Foreach:    if (!preReadElementForeach())    return false; break;
    case ParserState::Log:        if (!preReadElementLog())        return false; break;
    case ParserState::DataModel:  if (!preReadElementDataModel())  return false; break;
    case ParserState::Data:       if (!preReadElementData())       return false; break;
    case ParserState::Assign:     if (!preReadElementAssign())     return false; break;
    case ParserState::DoneData:   if (!preReadElementDoneData())   return false; break;
    case ParserState::Content:    if (!preReadElementContent())    return false; break;
    case ParserState::Param:      if (!preReadElementParam())      return false; break;
    case ParserState::Script:     if (!preReadElementScript())     return false; break;
    case ParserState::Send:       if (!preReadElementSend())       return false; break;
    case ParserState::Cancel:     if (!preReadElementCancel())     return false; break;
    case ParserState::Invoke:     if (!preReadElementInvoke())     return false; break;
    case ParserState::Finalize:   if (!preReadElementFinalize())   return false; break;
    default: addError(QStringLiteral("Unknown element %1").arg(currentTag.toString())); return false;
    }

    for (bool finished = false; !finished && !m_reader->hasError();) {
        switch (m_reader->readNext()) {
        case QXmlStreamReader::StartElement : {
            const QStringView newTag = m_reader->name();
            const ParserState::Kind newElementKind = ParserState::nameToParserStateKind(newTag);

            auto ns = m_reader->namespaceUri();

            if (ns != scxmlNamespace) {
                m_reader->skipCurrentElement();
            } else if (newElementKind == ParserState::None) {
                addError(QStringLiteral("Unknown element %1").arg(newTag.toString()));
                m_reader->skipCurrentElement();
            } else if (pNew.validChild(newElementKind)) {
                if (readElement() == false)
                    return false;
            } else {
                addError(QStringLiteral("Unexpected element %1").arg(newTag.toString()));
                m_reader->skipCurrentElement();
            }
        }
            break;
        case QXmlStreamReader::EndElement :
            finished = true;
            break;
        case QXmlStreamReader::Characters :
            if (m_stack.isEmpty())
                break;
            if (current().collectChars())
                current().chars.append(m_reader->text());
            break;
        default :
            break;
        }
    }

    switch (elementKind) {
    case ParserState::Scxml:      if (!postReadElementScxml())      return false; break;
    case ParserState::State:      if (!postReadElementState())      return false; break;
    case ParserState::Parallel:   if (!postReadElementParallel())   return false; break;
    case ParserState::Initial:    if (!postReadElementInitial())    return false; break;
    case ParserState::Transition: if (!postReadElementTransition()) return false; break;
    case ParserState::Final:      if (!postReadElementFinal())      return false; break;
    case ParserState::History:    if (!postReadElementHistory())    return false; break;
    case ParserState::OnEntry:    if (!postReadElementOnEntry())    return false; break;
    case ParserState::OnExit:     if (!postReadElementOnExit())     return false; break;
    case ParserState::Raise:      if (!postReadElementRaise())      return false; break;
    case ParserState::If:         if (!postReadElementIf())         return false; break;
    case ParserState::ElseIf:     if (!postReadElementElseIf())     return false; break;
    case ParserState::Else:       if (!postReadElementElse())       return false; break;
    case ParserState::Foreach:    if (!postReadElementForeach())    return false; break;
    case ParserState::Log:        if (!postReadElementLog())        return false; break;
    case ParserState::DataModel:  if (!postReadElementDataModel())  return false; break;
    case ParserState::Data:       if (!postReadElementData())       return false; break;
    case ParserState::Assign:     if (!postReadElementAssign())     return false; break;
    case ParserState::DoneData:   if (!postReadElementDoneData())   return false; break;
    case ParserState::Content:    if (!postReadElementContent())    return false; break;
    case ParserState::Param:      if (!postReadElementParam())      return false; break;
    case ParserState::Script:     if (!postReadElementScript())     return false; break;
    case ParserState::Send:       if (!postReadElementSend())       return false; break;
    case ParserState::Cancel:     if (!postReadElementCancel())     return false; break;
    case ParserState::Invoke:     if (!postReadElementInvoke())     return false; break;
    case ParserState::Finalize:   if (!postReadElementFinalize())   return false; break;
    default: break;
    }

    m_stack.removeLast();

    if (m_reader->hasError()/* && m_reader->error() != QXmlStreamReader::PrematureEndOfDocumentError*/) {
        addError(QStringLiteral("Error parsing SCXML file: %1").arg(m_reader->errorString()));
        return false;
    }

    return true;
}

void QScxmlCompilerPrivate::currentStateUp()
{
    Q_ASSERT(m_currentState->parent);
    m_currentState = m_currentState->parent;
}

bool QScxmlCompilerPrivate::flushInstruction()
{
    if (!hasPrevious()) {
        addError(QStringLiteral("missing instructionContainer"));
        return false;
    }
    DocumentModel::InstructionSequence *instructions = previous().instructionContainer;
    if (!instructions) {
        addError(QStringLiteral("got executable content within an element that did not set instructionContainer"));
        return false;
    }
    instructions->append(current().instruction);
    return true;
}


QByteArray QScxmlCompilerPrivate::load(const QString &name, bool *ok)
{
    QStringList errs;
    const QByteArray result = m_loader->load(name, m_fileName.isEmpty() ?
                              QString() : QFileInfo(m_fileName).path(), &errs);
    for (const QString &err : errs)
        addError(err);

    *ok = errs.isEmpty();

    return result;
}

QList<QScxmlError> QScxmlCompilerPrivate::errors() const
{
    return m_errors;
}

void QScxmlCompilerPrivate::addError(const QString &msg)
{
    m_errors.append(QScxmlError(m_fileName, m_reader->lineNumber(), m_reader->columnNumber(), msg));
}

void QScxmlCompilerPrivate::addError(const DocumentModel::XmlLocation &location, const QString &msg)
{
    m_errors.append(QScxmlError(m_fileName, location.line, location.column, msg));
}

DocumentModel::AbstractState *QScxmlCompilerPrivate::currentParent() const
{
    return m_currentState ? m_currentState->asAbstractState() : nullptr;
}

DocumentModel::XmlLocation QScxmlCompilerPrivate::xmlLocation() const
{
    return DocumentModel::XmlLocation(m_reader->lineNumber(), m_reader->columnNumber());
}

bool QScxmlCompilerPrivate::maybeId(const QXmlStreamAttributes &attributes, QString *id)
{
    Q_ASSERT(id);
    QString idStr = attributes.value(QLatin1String("id")).toString();
    if (!idStr.isEmpty()) {
        if (m_allIds.contains(idStr)) {
            addError(xmlLocation(), QStringLiteral("duplicate id '%1'").arg(idStr));
        } else {
            m_allIds.insert(idStr);
            *id = idStr;
        }
    }
    return true;
}

DocumentModel::If *QScxmlCompilerPrivate::lastIf()
{
    if (!hasPrevious()) {
        addError(QStringLiteral("No previous instruction found for else block"));
        return nullptr;
    }

    DocumentModel::Instruction *lastI = previous().instruction;
    if (!lastI) {
        addError(QStringLiteral("No previous instruction found for else block"));
        return nullptr;
    }
    DocumentModel::If *ifI = lastI->asIf();
    if (!ifI) {
        addError(QStringLiteral("Previous instruction for else block is not an 'if'"));
        return nullptr;
    }
    return ifI;
}

QScxmlCompilerPrivate::ParserState &QScxmlCompilerPrivate::current()
{
    return m_stack.last();
}

QScxmlCompilerPrivate::ParserState &QScxmlCompilerPrivate::previous()
{
    return m_stack[m_stack.size() - 2];
}

bool QScxmlCompilerPrivate::hasPrevious() const
{
    return m_stack.size() > 1;
}

bool QScxmlCompilerPrivate::checkAttributes(const QXmlStreamAttributes &attributes,
                                          QScxmlCompilerPrivate::ParserState::Kind kind)
{
    return checkAttributes(attributes,
                           ParserState::requiredAttributes(kind),
                           ParserState::optionalAttributes(kind));
}

bool QScxmlCompilerPrivate::checkAttributes(const QXmlStreamAttributes &attributes,
                                          const QStringList &requiredNames,
                                          const QStringList &optionalNames)
{
    QStringList required = requiredNames;
    for (const QXmlStreamAttribute &attribute : attributes) {
        const QStringView ns = attribute.namespaceUri();
        if (!ns.isEmpty() && ns != scxmlNamespace && ns != qtScxmlNamespace)
            continue;

        const QString name = attribute.name().toString();
        if (!required.removeOne(name) && !optionalNames.contains(name)) {
            addError(QStringLiteral("Unexpected attribute '%1'").arg(name));
            return false;
        }
    }
    if (!required.isEmpty()) {
        addError(QStringLiteral("Missing required attributes: '%1'")
                 .arg(required.join(QLatin1String("', '"))));
        return false;
    }
    return true;
}

QScxmlCompilerPrivate::DefaultLoader::DefaultLoader()
    : Loader()
{}

QByteArray QScxmlCompilerPrivate::DefaultLoader::load(const QString &name, const QString &baseDir, QStringList *errors)
{
    QStringList errs;
    QByteArray contents;
#ifdef BUILD_QSCXMLC
    QString cleanName = name;
    if (name.startsWith(QStringLiteral("file:")))
        cleanName = name.mid(5);
    QFileInfo fInfo(cleanName);
#else
    const QUrl url(name);
    if (!url.isLocalFile() && !url.isRelative())
        errs << QStringLiteral("src attribute is not a local file (%1)").arg(name);
    QFileInfo fInfo(url.isLocalFile() ? url.toLocalFile() : name);
#endif // BUILD_QSCXMLC
    if (fInfo.isRelative())
        fInfo = QFileInfo(QDir(baseDir).filePath(fInfo.filePath()));

    if (!fInfo.exists()) {
        errs << QStringLiteral("src attribute resolves to non existing file (%1)").arg(fInfo.filePath());
    } else {
        QFile f(fInfo.filePath());
        if (f.open(QFile::ReadOnly))
            contents = f.readAll();
        else
            errs << QStringLiteral("Failure opening file %1: %2")
                      .arg(fInfo.filePath(), f.errorString());
    }
    if (errors)
        *errors = errs;

    return contents;
}

QScxmlCompilerPrivate::ParserState::ParserState(QScxmlCompilerPrivate::ParserState::Kind someKind)
    : kind(someKind)
    , instruction(0)
    , instructionContainer(0)
{}

QT_END_NAMESPACE

#ifndef BUILD_QSCXMLC
#include "qscxmlcompiler.moc"
#endif
