// 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 "qscxmlexecutablecontent_p.h"
#include "qscxmlevent_p.h"
#include "qscxmlstatemachine_p.h"

#include <qjsondocument.h>
#include <qjsonobject.h>

QT_BEGIN_NAMESPACE

using namespace QScxmlExecutableContent;

QAtomicInt QScxmlEventBuilder::idCounter = QAtomicInt(0);

QScxmlEvent *QScxmlEventBuilder::buildEvent()
{
    auto dataModel = stateMachine ? stateMachine->dataModel() : nullptr;
    auto tableData = stateMachine ? stateMachine->tableData() : nullptr;

    QString eventName = event;
    bool ok = true;
    if (eventexpr != NoEvaluator) {
        eventName = dataModel->evaluateToString(eventexpr, &ok);
        ok = true; // ignore failure.
    }

    QVariant data;
    if ((!params || params->count == 0) && (!namelist || namelist->count == 0)) {
        if (contentExpr == NoEvaluator) {
            data = contents;
        } else {
            data = dataModel->evaluateToVariant(contentExpr, &ok);
        }
        if (!ok) {
            // expr evaluation failure results in the data property of the event being set to null. See e.g. test528.
            data = QVariant(QMetaType(QMetaType::VoidStar), nullptr);
        }
    } else {
        QVariantMap keyValues;
        if (evaluate(params, stateMachine, keyValues)) {
            if (namelist) {
                for (qint32 i = 0; i < namelist->count; ++i) {
                    QString name = tableData->string(namelist->const_data()[i]);
                    keyValues.insert(name, dataModel->scxmlProperty(name));
                }
            }
            data = keyValues;
        } else {
            // If the evaluation of the <param> tags fails, set _event.data to an empty string.
            // See test343.
            data = QVariant(QMetaType(QMetaType::VoidStar), nullptr);
        }
    }

    QString sendid = id;
    if (!idLocation.isEmpty()) {
        sendid = generateId();
        ok = stateMachine->dataModel()->setScxmlProperty(idLocation, sendid, tableData->string(instructionLocation));
        if (!ok)
            return nullptr;
    }

    QString origin = target;
    if (targetexpr != NoEvaluator) {
        origin = dataModel->evaluateToString(targetexpr, &ok);
        if (!ok)
            return nullptr;
    }
    if (origin.isEmpty()) {
        if (eventType == QScxmlEvent::ExternalEvent) {
            origin = QStringLiteral("#_internal");
        }
    } else if (origin == QStringLiteral("#_parent")) {
        // allow sending messages to the parent, independently of whether we're invoked or not.
    } else if (!origin.startsWith(QLatin1Char('#'))) {
        // [6.2.4] and test194.
        submitError(QStringLiteral("error.execution"),
                    QStringLiteral("Error in %1: %2 is not a legal target")
                    .arg(tableData->string(instructionLocation), origin),
                    sendid);
        return nullptr;
    } else if (!stateMachine->isDispatchableTarget(origin)) {
        // [6.2.4] and test521.
        submitError(QStringLiteral("error.communication"),
                    QStringLiteral("Error in %1: cannot dispatch to target '%2'")
                    .arg(tableData->string(instructionLocation), origin),
                    sendid);
        return nullptr;
    }

    QString origintype = type;
    if (origintype.isEmpty()) {
        // [6.2.5] and test198
        origintype = QStringLiteral("http://www.w3.org/TR/scxml/#SCXMLEventProcessor");
    }
    if (typeexpr != NoEvaluator) {
        origintype = dataModel->evaluateToString(typeexpr, &ok);
        if (!ok)
            return nullptr;
    }
    if (!origintype.isEmpty()
            && origintype != QStringLiteral("http://www.w3.org/TR/scxml/#SCXMLEventProcessor")) {
        // [6.2.5] and test199
        submitError(QStringLiteral("error.execution"),
                    QStringLiteral("Error in %1: %2 is not a valid type")
                    .arg(tableData->string(instructionLocation), origintype),
                    sendid);
        return nullptr;
    }

    QString invokeid;
    if (stateMachine && stateMachine->isInvoked()) {
        invokeid = stateMachine->sessionId();
    }

    QScxmlEvent *event = new QScxmlEvent;
    event->setName(eventName);
    event->setEventType(eventType);
    event->setData(data);
    event->setSendId(sendid);
    event->setOrigin(origin);
    event->setOriginType(origintype);
    event->setInvokeId(invokeid);
    return event;
}

QScxmlEvent *QScxmlEventBuilder::errorEvent(QScxmlStateMachine *stateMachine, const QString &name,
                                            const QString &message, const QString &sendid)
{
    QScxmlEventBuilder event;
    event.stateMachine = stateMachine;
    event.event = name;
    event.eventType = QScxmlEvent::PlatformEvent; // Errors are platform events. See e.g. test331.
    // _event.data == null, see test528
    event.id = sendid;
    auto error = event();
    error->setErrorMessage(message);
    return error;
}

bool QScxmlEventBuilder::evaluate(const ParameterInfo &param, QScxmlStateMachine *stateMachine,
                                  QVariantMap &keyValues)
{
    auto dataModel = stateMachine->dataModel();
    auto tableData = stateMachine->tableData();
    if (param.expr != NoEvaluator) {
        bool success = false;
        auto v = dataModel->evaluateToVariant(param.expr, &success);
        keyValues.insert(tableData->string(param.name), v);
        return success;
    }

    QString loc;
    if (param.location != QScxmlExecutableContent::NoString) {
        loc = tableData->string(param.location);
    }

    if (loc.isEmpty()) {
        return false;
    }

    if (dataModel->hasScxmlProperty(loc)) {
        keyValues.insert(tableData->string(param.name), dataModel->scxmlProperty(loc));
        return true;
    } else {
        submitError(QStringLiteral("error.execution"),
                    QStringLiteral("Error in <param>: %1 is not a valid location")
                    .arg(loc));
        return false;
    }
}

bool QScxmlEventBuilder::evaluate(const QScxmlExecutableContent::Array<ParameterInfo> *params,
                                  QScxmlStateMachine *stateMachine, QVariantMap &keyValues)
{
    if (!params)
        return true;

    auto paramPtr = params->const_data();
    for (qint32 i = 0; i != params->count; ++i, ++paramPtr) {
        if (!evaluate(*paramPtr, stateMachine, keyValues))
            return false;
    }

    return true;
}

void QScxmlEventBuilder::submitError(const QString &type, const QString &msg, const QString &sendid)
{
    QScxmlStateMachinePrivate::get(stateMachine)->submitError(type, msg, sendid);
}

/*!
 * \class QScxmlEvent
 * \brief The QScxmlEvent class is an event for a Qt SCXML state machine.
 * \since 5.7
 * \inmodule QtScxml
 *
 * SCXML \e events drive transitions. Most events are generated by using the
 * \c <raise> and \c <send> elements in the application. The state machine
 * automatically generates some mandatory events, such as errors.
 *
 * For more information, see
 * \l {SCXML Specification - 5.10.1 The Internal Structure of Events}.
 * For more information about how the Qt SCXML API differs from the
 * specification, see \l {SCXML Compliance}.
 *
 * \sa QScxmlStateMachine
 */

/*!
    \enum QScxmlEvent::EventType

    This enum type specifies the type of an SCXML event:

    \value  PlatformEvent
            An event generated internally by the state machine. For example,
            errors.
    \value  InternalEvent
            An event generated by a \c <raise> element.
    \value  ExternalEvent
            An event generated by a \c <send> element.
 */

/*!
 * Creates a new external SCXML event.
 */
QScxmlEvent::QScxmlEvent()
    : d(new QScxmlEventPrivate)
{ }

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

/*!
    \property QScxmlEvent::scxmlType
    \brief The event type.

*/

/*!
 * Returns the event type.
 */
QString QScxmlEvent::scxmlType() const
{
    switch (d->eventType) {
    case PlatformEvent:
        return QLatin1String("platform");
    case InternalEvent:
        return QLatin1String("internal");
    case ExternalEvent:
        break;
    }
    return QLatin1String("external");
}

/*!
 * Clears the contents of the event.
 */
void QScxmlEvent::clear()
{
    *d = QScxmlEventPrivate();
}

/*!
 * Assigns \a other to this SCXML event and returns a reference to this SCXML
 * event.
 */
QScxmlEvent &QScxmlEvent::operator=(const QScxmlEvent &other)
{
    *d = *other.d;
    return *this;
}

/*!
 * Constructs a copy of \a other.
 */
QScxmlEvent::QScxmlEvent(const QScxmlEvent &other)
    : d(new QScxmlEventPrivate(*other.d))
{
}

/*!
    \property QScxmlEvent::name

    \brief the name of the event.

    If the event is generated inside the SCXML document, this property holds the
    value of the \e event attribute specified inside the \c <raise> or \c <send>
    element.

    If the event is created in the C++ code and submitted to the
    QScxmlStateMachine, the value of this property is matched against the value
    of the \e event attribute specified inside the \c <transition> element in
    the SCXML document.
*/

/*!
 * Returns the name of the event.
 */
QString QScxmlEvent::name() const
{
    return d->name;
}

/*!
 * Sets the name of the event to \a name.
 */
void QScxmlEvent::setName(const QString &name)
{
    d->name = name;
}

/*!
    \property QScxmlEvent::sendId

    \brief the ID of the event.

    The ID is used by the \c <cancel> element to identify the event to be
    canceled.

    \note The state machine generates a unique ID if the \e id attribute is not
    specified in the \c <send> element. The generated ID can be accessed through
    this property.
*/

/*!
 * Returns the ID of the event.
 */
QString QScxmlEvent::sendId() const
{
    return d->sendid;
}

/*!
 * Sets the ID \a sendid for this event.
 */
void QScxmlEvent::setSendId(const QString &sendid)
{
    d->sendid = sendid;
}

/*!
    \property QScxmlEvent::origin

    \brief the URI that points to the origin of an SCXML event.

    The origin is equivalent to the \e target attribute of the \c <send>
    element.
*/

/*!
 * Returns a URI that points to the origin of an SCXML event.
 */
QString QScxmlEvent::origin() const
{
    return d->origin;
}

/*!
 * Sets the origin of an SCXML event to \a origin.
 *
 * \sa QScxmlEvent::origin
 */
void QScxmlEvent::setOrigin(const QString &origin)
{
    d->origin = origin;
}

/*!
    \property QScxmlEvent::originType

    \brief the origin type of an SCXML event.

    The origin type is equivalent to the \e type attribute of the \c <send>
    element.
*/

/*!
 * Returns the origin type of an SCXML event.
 */
QString QScxmlEvent::originType() const
{
    return d->originType;
}

/*!
 * Sets the origin type of an SCXML event to \a origintype.
 *
 * \sa QScxmlEvent::originType
 */
void QScxmlEvent::setOriginType(const QString &origintype)
{
    d->originType = origintype;
}

/*!
    \property QScxmlEvent::invokeId

    \brief the ID of the invoked state machine if the event is generated by one.
*/

/*!
 * If this event is generated by an invoked state machine, returns the ID of
 * the \c <invoke> element. Otherwise, returns an empty value.
 */
QString QScxmlEvent::invokeId() const
{
    return d->invokeId;
}

/*!
 * Sets the ID of an invoked state machine to \a invokeid.
 * \sa QScxmlEvent::invokeId
 */
void QScxmlEvent::setInvokeId(const QString &invokeid)
{
    d->invokeId = invokeid;
}

/*!
    \property QScxmlEvent::delay

    \brief The delay in milliseconds after which the event is to be delivered
    after processing the \c <send> element.
*/

/*!
 * Returns the delay in milliseconds after which this event is to be delivered
 * after processing the \c <send> element.
 */
int QScxmlEvent::delay() const
{
    return d->delayInMiliSecs;
}

/*!
 * Sets the delay in milliseconds as the value of \a delayInMiliSecs.
 * \sa QScxmlEvent::delay
 */
void QScxmlEvent::setDelay(int delayInMiliSecs)
{
    d->delayInMiliSecs = delayInMiliSecs;
}
/*!
    \property QScxmlEvent::eventType

    \brief the type of the event.
*/

/*!
 * Returns the type of this event.
 * \sa QScxmlEvent::EventType
 */
QScxmlEvent::EventType QScxmlEvent::eventType() const
{
    return d->eventType;
}

/*!
 * Sets the event type to \a type.
 * \sa QScxmlEvent::eventType QScxmlEvent::EventType
 */
void QScxmlEvent::setEventType(const EventType &type)
{
    d->eventType = type;
}

/*!
    \property QScxmlEvent::data

    \brief the data included by the sender.

    When \c <param> elements are used in the \c <send> element, the data will
    contain a QVariantMap where the key is the \e name attribute, and the value
    is taken from the \e expr attribute or the \e location attribute.

    When a \c <content> element is used, the data will contain a single item
    with either the value of the \e expr attribute of the \c <content> element
    or the child data of the \c <content> element.
*/

/*!
 * Returns the data included by the sender.
 */
QVariant QScxmlEvent::data() const
{
    if (isErrorEvent())
        return QVariant();
    return d->data;
}

/*!
 * Sets the payload data to \a data.
 * \sa QScxmlEvent::data
 */
void QScxmlEvent::setData(const QVariant &data)
{
    if (!isErrorEvent())
        d->data = data;
}

/*!
    \property QScxmlEvent::errorEvent
    \brief Whether the event represents an error.
*/

/*!
 * Returns \c true when this is an error event, \c false otherwise.
 */
bool QScxmlEvent::isErrorEvent() const
{
    return eventType() == PlatformEvent && name().startsWith(QStringLiteral("error."));
}

/*!
    \property QScxmlEvent::errorMessage
    \brief An error message for an error event, or an empty QString.
*/

/*!
 * If this is an error event, returns the error message. Otherwise, returns an
 *         empty QString.
 */
QString QScxmlEvent::errorMessage() const
{
    if (!isErrorEvent())
        return QString();
    return d->data.toString();
}

/*!
 * If this is an error event, the \a message is set as the error message.
 */
void QScxmlEvent::setErrorMessage(const QString &message)
{
    if (isErrorEvent())
        d->data = message;
}

QByteArray QScxmlEventPrivate::debugString(QScxmlEvent *event)
{
    if (event == nullptr) {
        return "<null>";
    }

    QJsonObject o;
    if (!event->name().isNull())
        o[QStringLiteral("name")] = event->name();
    if (!event->scxmlType().isNull())
        o[QStringLiteral("type")] = event->scxmlType();
    if (!event->sendId().isNull())
        o[QStringLiteral("sendid")] = event->sendId();
    if (!event->origin().isNull())
        o[QStringLiteral("origin")] = event->origin();
    if (!event->originType().isNull())
        o[QStringLiteral("origintype")] = event->originType();
    if (!event->invokeId().isNull())
        o[QStringLiteral("invokeid")] = event->invokeId();
    if (!event->data().isNull())
        o[QStringLiteral("data")] = QJsonValue::fromVariant(event->data());

    return QJsonDocument(o).toJson(QJsonDocument::Compact);
}

QT_END_NAMESPACE
