// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

#include "qqmljsfunctioninitializer_p.h"

#include <private/qqmljsmemorypool_p.h>

#include <QtCore/qloggingcategory.h>
#include <QtCore/qfileinfo.h>

#include <QtQml/private/qqmlsignalnames_p.h>

QT_BEGIN_NAMESPACE

using namespace Qt::StringLiterals;

/*!
 * \internal
 * \class QQmlJSFunctionInitializer
 *
 * QQmlJSFunctionInitializer analyzes the IR to produce an initial
 * QQmlJSCompilePass::Function for further analysis. It only looks for the
 * signature and the QML scope and doesn't visit the byte code.
 */

static QString bindingTypeDescription(QmlIR::Binding::Type type)
{
    switch (type) {
    case QmlIR::Binding::Type_Invalid:
        return u"invalid"_s;
    case QmlIR::Binding::Type_Boolean:
        return u"a boolean"_s;
    case QmlIR::Binding::Type_Number:
        return u"a number"_s;
    case QmlIR::Binding::Type_String:
        return u"a string"_s;
    case QmlIR::Binding::Type_Null:
        return u"null"_s;
    case QmlIR::Binding::Type_Translation:
        return u"a translation"_s;
    case QmlIR::Binding::Type_TranslationById:
        return u"a translation by id"_s;
    case QmlIR::Binding::Type_Script:
        return u"a script"_s;
    case QmlIR::Binding::Type_Object:
        return u"an object"_s;
    case QmlIR::Binding::Type_AttachedProperty:
        return u"an attached property"_s;
    case QmlIR::Binding::Type_GroupProperty:
        return u"a grouped property"_s;
    }

    return u"nothing"_s;
}

void QQmlJSFunctionInitializer::populateSignature(
        const QV4::Compiler::Context *context, QQmlJS::AST::FunctionExpression *ast,
        QQmlJSCompilePass::Function *function)
{
    const auto signatureError = [&](const QString &message) {
        m_logger->logCompileError(message, ast->firstSourceLocation());
        function->isFullyTyped = false;
    };

    if (!m_typeResolver->canCallJSFunctions()) {
        signatureError(u"Ignoring type annotations as requested "
                       "by pragma FunctionSignatureBehavior"_s);
        return;
    }

    QQmlJS::AST::BoundNames arguments;
    if (ast->formals)
        arguments = ast->formals->formals();

    // If the function has no arguments and no return type annotation we assume it's untyped.
    // You can annotate it to return void to make it typed.
    // Otherwise we first assume it's typed and reset the flag if we detect a problem.
    function->isFullyTyped = !arguments.isEmpty() || ast->typeAnnotation;

    if (function->argumentTypes.isEmpty()) {
        bool alreadyWarnedAboutMissingAnnotations = false;
        for (const QQmlJS::AST::BoundName &argument : std::as_const(arguments)) {
            if (argument.typeAnnotation) {
                if (const auto type = m_typeResolver->typeFromAST(argument.typeAnnotation->type)) {
                    function->argumentTypes.append(m_typeResolver->namedType(type));
                } else {
                    function->argumentTypes.append(
                            m_typeResolver->namedType(m_typeResolver->varType()));
                    signatureError(u"Cannot resolve the argument type %1."_s
                                   .arg(argument.typeAnnotation->type->toString()));
                }
            } else {
                if (!alreadyWarnedAboutMissingAnnotations) {
                    alreadyWarnedAboutMissingAnnotations = true;
                    signatureError(u"Functions without type annotations won't be compiled"_s);
                }
                function->argumentTypes.append(
                        m_typeResolver->namedType(m_typeResolver->varType()));
            }
        }
    } else {
        for (qsizetype i = 0, end = arguments.size(); i != end; ++i) {
            const QQmlJS::AST::BoundName &argument = arguments[i];
            if (argument.typeAnnotation) {
                if (const auto type = m_typeResolver->typeFromAST(argument.typeAnnotation->type)) {
                    if (!function->argumentTypes[i].contains(type)) {
                        signatureError(u"Type annotation %1 on signal handler "
                                        "contradicts signal argument type %2"_s
                                       .arg(argument.typeAnnotation->type->toString(),
                                            function->argumentTypes[i].descriptiveName()));
                    }
                }
            }
        }
    }

    if (!function->returnType.isValid()) {
        if (ast->typeAnnotation) {
            function->returnType = m_typeResolver->namedType(
                    m_typeResolver->typeFromAST(ast->typeAnnotation->type));
            if (!function->returnType.isValid())
                signatureError(u"Cannot resolve return type %1"_s.arg(
                                   QmlIR::IRBuilder::asString(ast->typeAnnotation->type->typeId)));
        }
    }

    for (int i = QQmlJSCompilePass::FirstArgument + function->argumentTypes.size();
         i < context->registerCountInFunction; ++i) {
        function->registerTypes.append(m_typeResolver->namedType(m_typeResolver->voidType()));
    }

    function->addressableScopes = m_typeResolver->objectsById();
    function->code = context->code;
    function->sourceLocations = context->sourceLocationTable.get();
}

static void diagnose(
        const QString &message, const QQmlJS::SourceLocation &location, QQmlJSLogger *logger)
{
    logger->logCompileError(message, location);
}

QQmlJSCompilePass::Function QQmlJSFunctionInitializer::run(
        const QV4::Compiler::Context *context, const QString &propertyName,
        QQmlJS::AST::Node *astNode, const QmlIR::Binding &irBinding)
{
    QQmlJS::SourceLocation bindingLocation;
    bindingLocation.startLine = irBinding.location.line();
    bindingLocation.startColumn = irBinding.location.column();

    QQmlJSCompilePass::Function function;
    function.qmlScope = m_typeResolver->registerContentPool()->createType(
            m_scopeType, QQmlJSRegisterContent::InvalidLookupIndex,
            QQmlJSRegisterContent::ScopeObject);

    // If we find a problem before we can determine whether it's a signal handler,
    // that's bad and warrants a warning, even if it then turns out to be a signal handler.
    const auto setIsSignalHandler = [&]() {
        function.isSignalHandler = true;

        // If the function is a signal handler and just returns a closure, it's harmless.
        // Otherwise it's a warning.
        m_logger->setCompileErrorLevel(context->returnsClosure ? QtDebugMsg : QtWarningMsg);
    };

    if (irBinding.type() != QmlIR::Binding::Type_Script) {
        diagnose(u"Binding is not a script binding, but %1."_s.arg(
                     bindingTypeDescription(QmlIR::Binding::Type(quint32(irBinding.type())))),
                 bindingLocation, m_logger);
    }

    function.isProperty = m_objectType->hasProperty(propertyName);
    if (function.isProperty) {
        const auto property = m_objectType->property(propertyName);
        if (const QQmlJSScope::ConstPtr propertyType = property.type()) {
            function.returnType = m_typeResolver->namedType(propertyType->isListProperty()
                ? m_typeResolver->qObjectListType()
                : QQmlJSScope::ConstPtr(property.type()));
        } else {
            diagnose(u"Cannot resolve property type %1 for binding on %2."_s
                             .arg(property.typeName(), propertyName),
                     bindingLocation, m_logger);
        }

        if (!property.bindable().isEmpty() && !property.isPrivate())
            function.isQPropertyBinding = true;
    } else if (QQmlSignalNames::isHandlerName(propertyName)) {
        if (auto actualPropertyName =
                QQmlSignalNames::changedHandlerNameToPropertyName(propertyName);
                actualPropertyName && m_objectType->hasProperty(*actualPropertyName)) {
            setIsSignalHandler();
        } else {
            auto signalName = QQmlSignalNames::handlerNameToSignalName(propertyName);
            const auto methods = m_objectType->methods(*signalName);
            for (const auto &method : methods) {
                if (method.isCloned())
                    continue;
                if (method.methodType() == QQmlJSMetaMethodType::Signal) {
                    setIsSignalHandler();
                    const auto arguments = method.parameters();
                    for (qsizetype i = 0, end = arguments.size(); i < end; ++i) {
                        const auto &type = arguments[i].type();
                        if (type.isNull()) {
                            diagnose(u"Cannot resolve the argument type %1."_s.arg(
                                             arguments[i].typeName()),
                                     bindingLocation, m_logger);
                            function.argumentTypes.append(
                                    m_typeResolver->namedType(m_typeResolver->varType()));
                        } else {
                            function.argumentTypes.append(m_typeResolver->namedType(type));
                        }
                    }
                    break;
                }
            }
            if (!function.isSignalHandler) {
                diagnose(u"Could not find signal \"%1\"."_s.arg(*signalName),
                         bindingLocation, m_logger);
            }
        }
    } else {
        QString message = u"Could not find property \"%1\"."_s.arg(propertyName);
        if (m_objectType->isNameDeferred(propertyName)) {
            // If the property doesn't exist but the name is deferred, then
            // it's deferred via the presence of immediate names. Those are
            // most commonly used to enable generalized grouped properties.
            message += u" You may want use ID-based grouped properties here.";
        }

        diagnose(message, bindingLocation, m_logger);
    }

    QQmlJS::MemoryPool pool;
    auto ast = astNode->asFunctionDefinition();
    if (!ast) {
        QQmlJS::AST::Statement *stmt = astNode->statementCast();
        if (!stmt) {
            Q_ASSERT(astNode->expressionCast());
            QQmlJS::AST::ExpressionNode *expr = astNode->expressionCast();
            stmt = new (&pool) QQmlJS::AST::ExpressionStatement(expr);
        }
        auto body = new (&pool) QQmlJS::AST::StatementList(stmt);
        body = body->finish();

        QString name = u"binding for "_s; // ####
        ast = new (&pool) QQmlJS::AST::FunctionDeclaration(
                pool.newString(std::move(name)), /*formals*/ nullptr, body);
        ast->lbraceToken = astNode->firstSourceLocation();
        ast->functionToken = ast->lbraceToken;
        ast->rbraceToken = astNode->lastSourceLocation();
    }

    populateSignature(context, ast, &function);
    return function;
}

QQmlJSCompilePass::Function QQmlJSFunctionInitializer::run(
        const QV4::Compiler::Context *context, const QString &functionName,
        QQmlJS::AST::Node *astNode)
{
    Q_UNUSED(functionName);

    QQmlJSCompilePass::Function function;
    function.qmlScope = m_typeResolver->registerContentPool()->createType(
            m_scopeType, QQmlJSRegisterContent::InvalidLookupIndex,
            QQmlJSRegisterContent::ScopeObject);

    auto ast = astNode->asFunctionDefinition();
    Q_ASSERT(ast);

    populateSignature(context, ast, &function);
    return function;
}

QT_END_NAMESPACE
