/*
    SPDX-FileCopyrightText: 2011, 2012, 2013 Alex Richardson <alex.richardson@gmx.de>

    SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/

#include "defaultscriptclass.hpp"

// lib
#include <datainformation.hpp>
#include <topleveldatainformation.hpp>
#include <uniondatainformation.hpp>
#include <structuredatainformation.hpp>
#include <pointerdatainformation.hpp>
#include <parserutils.hpp>
#include <scriptvalueconverter.hpp>
#include <scriptlogger.hpp>
#include <scripthandlerinfo.hpp>
#include <safereference.hpp>
#include <structureslogging.hpp>
// Qt
#include <QScriptContext>
// Std
#include <utility>

DefaultScriptClass::DefaultScriptClass(QScriptEngine* engine, ScriptHandlerInfo* handlerInfo,
                                       int propertiesSize)
    : QScriptClass(engine)
    , s_valid(engine->toStringHandle(ParserStrings::PROPERTY_VALID()))
    , s_wasAbleToRead(engine->toStringHandle(ParserStrings::PROPERTY_ABLE_TO_READ()))
    , s_validationError(engine->toStringHandle(ParserStrings::PROPERTY_VALIDATION_ERROR()))
    , s_parent(engine->toStringHandle(ParserStrings::PROPERTY_PARENT()))
    , s_byteOrder(engine->toStringHandle(ParserStrings::PROPERTY_BYTEORDER()))
    , s_name(engine->toStringHandle(ParserStrings::PROPERTY_NAME()))
    , s_datatype(engine->toStringHandle(ParserStrings::PROPERTY_DATATYPE()))
    , s_updateFunc(engine->toStringHandle(ParserStrings::PROPERTY_UPDATE_FUNC()))
    , s_validationFunc(engine->toStringHandle(ParserStrings::PROPERTY_VALIDATION_FUNC()))
    , s_customTypeName(engine->toStringHandle(ParserStrings::PROPERTY_CUSTOM_TYPE_NAME()))
    , s_asStringFunc(engine->toStringHandle(ParserStrings::PROPERTY_TO_STRING_FUNC()))
    , mHandlerInfo(handlerInfo)
{

    // TODO remove, every subclass should have proto
    mDefaultPrototype = engine->newObject();
    mDefaultPrototype.setProperty(QStringLiteral("toString"), engine->newFunction(Default_proto_toString));

    // add all our properties
    // TODO: find a pattern to set this (with proper size) in one go with properties from all subclasses
    mIterableProperties.reserve(11 + propertiesSize);
    appendProperty(s_parent, QScriptValue::ReadOnly | QScriptValue::Undeletable);
    appendProperty(s_name, QScriptValue::Undeletable);
    appendProperty(s_wasAbleToRead, QScriptValue::ReadOnly | QScriptValue::Undeletable);
    appendProperty(s_byteOrder, QScriptValue::Undeletable);
    appendProperty(s_valid, QScriptValue::ReadOnly | QScriptValue::Undeletable);
    appendProperty(s_validationError, QScriptValue::ReadOnly | QScriptValue::Undeletable);
    appendProperty(s_validationFunc, QScriptValue::Undeletable);
    appendProperty(s_updateFunc, QScriptValue::Undeletable);
    appendProperty(s_datatype, QScriptValue::Undeletable);
    appendProperty(s_customTypeName, QScriptValue::Undeletable);
    appendProperty(s_asStringFunc, QScriptValue::Undeletable);
}

DefaultScriptClass::~DefaultScriptClass() = default;

DataInformation* DefaultScriptClass::toDataInformation(const QScriptValue& obj)
{
    if (!obj.scriptClass()) {
        return nullptr;
    }
    Q_ASSERT(obj.data().isVariant());
    const QVariant variant = obj.data().toVariant();
    if (variant.isValid() && variant.canConvert<SafeReference>() && variant.userType() == qMetaTypeId<SafeReference>()) {
        const SafeReference& ref = *reinterpret_cast<const SafeReference*>(variant.constData());
        return ref.data();
    }
    return nullptr;
}

QScriptClass::QueryFlags DefaultScriptClass::queryProperty(const QScriptValue& object,
                                                           const QScriptString& name, QScriptClass::QueryFlags flags, uint* id)
{
    const ScriptHandlerInfo::Mode mode = mHandlerInfo->mode();
    Q_ASSERT(mode != ScriptHandlerInfo::Mode::None);
    DataInformation* const data = toDataInformation(object);
    if (!data) {
        mHandlerInfo->logger()->error().nospace() << "Could not cast data from '" << object.data().toString() << "'.";
        std::ignore = engine()->currentContext()->throwError(QScriptContext::ReferenceError,
                                                             QStringLiteral("Attempting to access an invalid object"));
        return {};
    }
    if (name == s_valid || name == s_validationError) {
        return mode == ScriptHandlerInfo::Mode::Validating ? flags : flags& ~HandlesWriteAccess;
    }
    if (mode != ScriptHandlerInfo::Mode::Updating) {
        // the only properties that are possibly writable when not updating are valid and validationError
        // but we checked them before so we remove handlesWriteAccess from the flags
        flags &= ~HandlesWriteAccess;
    }

    if (name == s_byteOrder || name == s_name || name == s_updateFunc || name == s_validationFunc
        || name == s_datatype || name == s_customTypeName || name == s_asStringFunc) {
        return flags;
    }
    if (name == s_wasAbleToRead || name == s_parent) {
        return flags & ~HandlesWriteAccess;
    }
    if (queryAdditionalProperty(data, name, &flags, id)) {
        return flags;
    }

    data->logError() << "Could not find property with name" << name.toString();
    std::ignore = engine()->currentContext()->throwError(QScriptContext::ReferenceError,
                                                         QLatin1String("Could not find property with name ") + name.toString());
    return {};
}

QScriptValue DefaultScriptClass::property(const QScriptValue& object, const QScriptString& name, uint id)
{
    Q_ASSERT(mHandlerInfo->mode() != ScriptHandlerInfo::Mode::None);
    DataInformation* const data = toDataInformation(object);
    if (!data) {
        mHandlerInfo->logger()->error().nospace() << "Could not cast data from '" << object.data().toString() << ".";
        return engine()->currentContext()->throwError(QScriptContext::ReferenceError,
                                                      QStringLiteral("Attempting to access an invalid object"));
    }
    if (name == s_valid) {
        return data->validationSuccessful();
    }
    if (name == s_wasAbleToRead) {
        return data->wasAbleToRead();
    }
    if (name == s_parent) {
        if (data->parent()->isTopLevel()) {
            return engine()->nullValue();
        }
        return data->parent()->asDataInformation()->toScriptValue(engine(), mHandlerInfo);
    }
    if (name == s_datatype) {
        return data->typeName();
    }
    if (name == s_updateFunc) {
        return data->updateFunc();
    }
    if (name == s_validationFunc) {
        return data->validationFunc();
    }
    if (name == s_validationError) {
        return data->validationError();
    }
    if (name == s_byteOrder) {
        return ParserUtils::byteOrderToString(data->byteOrder());
    }
    if (name == s_name) {
        return data->name();
    }
    if (name == s_customTypeName) {
        return data->typeName();
    }
    if (name == s_asStringFunc) {
        return data->toStringFunction();
    }
    QScriptValue other = additionalProperty(data, name, id);
    if (other.isValid()) {
        return other;
    }
    data->logError() << "Could not find property with name" << name.toString();
    return engine()->currentContext()->throwError(QScriptContext::ReferenceError,
                                                  QLatin1String("Cannot read property ") + name.toString());
}

void DefaultScriptClass::setDataType(const QScriptValue& value, DataInformation* data)
{
    DataInformation* const thisObj = toDataInformation(engine()->currentContext()->thisObject());
    const bool isThisObj = thisObj == data;
    // this object always has mHasBeenUpdated set just before calling updateFunc, so in that case it is okay
    if (data->hasBeenUpdated() && !isThisObj) {
        // this element has already been updated (and probably read, replacing it could cause crazy errors
        data->logError() << "Attempting to replace an already updated object. This could cause errors."
            " Current this object:" << (thisObj ? thisObj->fullObjectPath() : QString());
        return;
    }
    // change the type of the underlying object
    std::unique_ptr<DataInformation> newType = ScriptValueConverter::convert(value, data->name(), data->logger(), data);
    if (!newType) {
        data->logError() << "Failed to set new type, could not convert value.";
        return;
    }

    DataInformation* const rawNewType = newType.get();
    DataInformationBase* const parent = data->parent();
    TopLevelDataInformation* const top = data->topLevelDataInformation();
    // only if parent is toplevel, struct or union, can we replace
    bool replaced = false;
    if (parent->isTopLevel()) {
        Q_ASSERT(isThisObj); // we can only do this if we are currently at the top level element
        parent->asTopLevel()->setActualDataInformation(std::move(newType));
        replaced = true;
    } else if (parent->isStruct()) {
        StructureDataInformation* const stru = parent->asStruct();
        int index = stru->indexOf(data);
        Q_ASSERT(index != -1);
        Q_ASSERT(uint(index) < stru->childCount());
        replaced = stru->replaceChildAt(index, std::move(newType));
        if (!replaced) {
            stru->logError() << "Failed to replace child at index:" << index;
        }
    } else if (parent->isUnion()) {
        UnionDataInformation* const un = parent->asUnion();
        int index = un->indexOf(data);
        Q_ASSERT(index != -1);
        Q_ASSERT(uint(index) < un->childCount());
        replaced = un->replaceChildAt(index, std::move(newType));
        if (!replaced) {
            un->logError() << "Failed to replace child at index: " << index;
        }
    } else if (parent->isPointer()) {
        parent->asPointer()->setPointerTarget(std::move(newType));
        replaced = true;
    } else {
        data->logError() << "Failed to set data type since element is not toplevel and parent"
            " is neither struct nor union nor pointer.";
    }
    if (replaced) {
        top->setChildDataChanged();
        // if the current object was "this" in javascript we have to replace it
        if (isThisObj) {
            engine()->currentContext()->setThisObject(rawNewType->toScriptValue(engine(), mHandlerInfo));
        }
        rawNewType->mHasBeenUpdated = true;
    }
}

void DefaultScriptClass::setProperty(QScriptValue& object, const QScriptString& name, uint id, const QScriptValue& value)
{
    const ScriptHandlerInfo::Mode mode = mHandlerInfo->mode();
    Q_ASSERT(mode != ScriptHandlerInfo::Mode::None);
    DataInformation* const data = toDataInformation(object);
    if (!data) {
        mHandlerInfo->logger()->error().nospace() << "Could not cast data from '" << object.data().toString() << "'.";
        std::ignore = engine()->currentContext()->throwError(QScriptContext::ReferenceError,
                                                             QStringLiteral("Attempting to access an invalid object"));
        return;
    }
    if (mode == ScriptHandlerInfo::Mode::Validating) {
        // only way write access is allowed is when validating: valid and validationError
        if (data->hasBeenValidated()) {
            data->logError() << "Cannot modify this object, it has already been validated.";
        } else if (name == s_valid) {
            data->mValidationSuccessful = value.toBool();
        } else if (name == s_validationError) {
            data->setValidationError(value.toString());
        } else {
            data->logError().nospace() << "Cannot write to property '" << name.toString() << "' while validating.";
        }
        return;
    }

    if (mode != ScriptHandlerInfo::Mode::Updating) {
        data->logError().nospace() << "Writing to property '" << name.toString() << "' is only allowed when updating.";
        return;
    }
    Q_ASSERT(mode == ScriptHandlerInfo::Mode::Updating);

    if (name == s_byteOrder) {
        data->setByteOrder(ParserUtils::byteOrderFromString(value.toString(),
                                                            LoggerWithContext(data->logger(), data->fullObjectPath())));
    } else if (name == s_datatype) {
        // change the type of the underlying object
        setDataType(value, data);
    } else if (name == s_updateFunc) {
        data->setUpdateFunc(value);
    } else if (name == s_validationFunc) {
        data->setValidationFunc(value);
    } else if (name == s_name) {
        data->setName(value.toString());
    } else if (name == s_customTypeName) {
        if (!value.isValid() || value.isNull() || value.isUndefined()) {
            data->setCustomTypeName(QString()); // unset
        } else {
            data->setCustomTypeName(value.toString());
        }
    } else if (name == s_asStringFunc) {
        data->setToStringFunction(value);
    } else {
        bool setAdditional = setAdditionalProperty(data, name, id, value);
        if (setAdditional) {
            return;
        }
        data->logError().nospace() << "Could not set property with name '" << name.toString() << "'.";
        std::ignore = engine()->currentContext()->throwError(QScriptContext::ReferenceError,
                                                             QLatin1String("Cannot write property ") + name.toString());
    }
}

QScriptValue::PropertyFlags DefaultScriptClass::propertyFlags(const QScriptValue& object, const QScriptString& name, uint id)
{
    QScriptValue::PropertyFlags result;
    const ScriptHandlerInfo::Mode mode = mHandlerInfo->mode();
    Q_ASSERT(mode != ScriptHandlerInfo::Mode::None);
    DataInformation* const data = toDataInformation(object);
    if (!data) {
        mHandlerInfo->logger()->error().nospace() << "Could not cast data from '" << object.data().toString() << "'.";
        std::ignore = engine()->currentContext()->throwError(QScriptContext::ReferenceError,
                                                             QStringLiteral("Attempting to access an invalid object"));
        return {};
    }
    if (name == s_valid || name == s_validationError) {
        if (mode != ScriptHandlerInfo::Mode::Validating) {
            result |= QScriptValue::ReadOnly;
        }
    } else if (mode != ScriptHandlerInfo::Mode::Updating) {
        result |= QScriptValue::ReadOnly;
    }

    for (const auto& property : mIterableProperties) {
        if (property.name == name) {
            return result | property.propertyFlags;
        }
    }

    if (additionalPropertyFlags(data, name, id, &result)) {
        return result; // is a child element
    }
    data->logError().nospace() << "Could not find flags for property with name '" << name.toString() << "'.";
    return {};
}

QScriptValue DefaultScriptClass::prototype() const
{
    return mDefaultPrototype;
}

QScriptValue DefaultScriptClass::Default_proto_toString(QScriptContext* ctx, QScriptEngine* eng)
{
    DataInformation* const data = toDataInformation(ctx->thisObject());
    if (!data) {
        qCWarning(LOG_KASTEN_OKTETA_CONTROLLERS_STRUCTURES) << "could not cast data";
        return eng->undefinedValue();
    }
    return QString(data->typeName() + QLatin1Char(' ') + data->name());
}

QScriptClassPropertyIterator* DefaultScriptClass::newIterator(const QScriptValue& object)
{
    return new DefaultscriptClassIterator(object, this);
}

DefaultscriptClassIterator::DefaultscriptClassIterator(const QScriptValue& object, DefaultScriptClass* cls)
    : QScriptClassPropertyIterator(object)
    , mClass(cls)
{
    DataInformation* const data = DefaultScriptClass::toDataInformation(object);
    mData = data;
}

DefaultscriptClassIterator::~DefaultscriptClassIterator() = default;

bool DefaultscriptClassIterator::hasNext() const
{
    return mCurrent < static_cast<int>(mClass->mIterableProperties.size()) - 1;
}

bool DefaultscriptClassIterator::hasPrevious() const
{
    return mCurrent > 0;
}

QScriptString DefaultscriptClassIterator::name() const
{
    Q_ASSERT(mCurrent >= 0 && mCurrent < static_cast<int>(mClass->mIterableProperties.size() + mData->childCount()));
    if (mCurrent < 0 || mCurrent >= static_cast<int>(mClass->mIterableProperties.size() + mData->childCount())) {
        return {};
    }
    if (mCurrent < static_cast<int>(mClass->mIterableProperties.size())) {
        return mClass->mIterableProperties[mCurrent].name;
    }
    int index = mCurrent - mClass->mIterableProperties.size();
    Q_ASSERT(index >= 0);
    DataInformation* const child = mData->childAt(index);
    return mClass->engine()->toStringHandle(child->name());
}

QScriptValue::PropertyFlags DefaultscriptClassIterator::flags() const
{
    Q_ASSERT(mCurrent >= 0 && mCurrent < static_cast<int>(mClass->mIterableProperties.size() + mData->childCount()));
    if (mCurrent < 0 || mCurrent >= static_cast<int>(mClass->mIterableProperties.size() + mData->childCount())) {
        return {};
    }
    if (mCurrent < static_cast<int>(mClass->mIterableProperties.size())) {
        return mClass->propertyFlags(object(), mClass->mIterableProperties[mCurrent].name, id());
    }
    return QScriptValue::ReadOnly;
}

uint DefaultscriptClassIterator::id() const
{
    Q_ASSERT(mCurrent >= 0 && mCurrent < static_cast<int>(mClass->mIterableProperties.size() + mData->childCount()));
    if (mCurrent < 0 || mCurrent >= static_cast<int>(mClass->mIterableProperties.size() + mData->childCount())) {
        return 0;
    }
    // only children have an id assigned
    if (mCurrent < static_cast<int>(mClass->mIterableProperties.size())) {
        return 0;
    }
    return mCurrent - mClass->mIterableProperties.size() + 1;
}

void DefaultscriptClassIterator::next()
{
    Q_ASSERT(mCurrent == -1 || mCurrent < static_cast<int>(mClass->mIterableProperties.size() + mData->childCount()));
    mCurrent++;
}

void DefaultscriptClassIterator::previous()
{
    Q_ASSERT(mCurrent >= 0);
    mCurrent--;
}

void DefaultscriptClassIterator::toBack()
{
    mCurrent = mClass->mIterableProperties.size() + mData->childCount();
}

void DefaultscriptClassIterator::toFront()
{
    mCurrent = -1;
}
