/*
 SPDX-FileCopyrightText: 2005 Tobias Koenig <tokoe@kde.org>

 SPDX-License-Identifier: MIT
*/

#include "converter.h"
#include "elementargumentserializer.h"
#include "settings.h"
#include <QCoreApplication>
#include <QDebug>
#include <code_generation/style.h>

using namespace KWSDL;

SoapBinding::Style Converter::soapStyle(const Binding &binding) const
{
    if (binding.type() == Binding::SOAPBinding) {
        const SoapBinding soapBinding(binding.soapBinding());
        return soapBinding.binding().style();
    }
    return SoapBinding::RPCStyle;
}

static Part::List selectedParts(const Binding &binding, const Message &message, const Operation &operation, bool input)
{
    if (binding.type() == Binding::SOAPBinding) {
        const SoapBinding soapBinding = binding.soapBinding();
        const SoapBinding::Operation op = soapBinding.operations().value(operation.name());
        const QString selectedPart = input ? op.input().part() : op.output().part();
        if (!selectedPart.isEmpty()) {
            Part::List selected;
            const Part::List parts = message.parts();
            for (const Part &part : parts) {
                if (part.name() == selectedPart) { // support for <soap:body parts="MoveFolderResult"/> (msexchange)
                    selected << part;
                }
            }
            return selected;
        }
    }

    return message.parts();
}

static QString fullyQualified(const KODE::Class &c)
{
    if (c.nameSpace().isEmpty()) {
        return c.name();
    } else {
        return c.nameSpace() + QLatin1String("::") + c.name();
    }
}

static SoapBinding::Headers getInputHeaders(const Binding &binding, const QString &operationName)
{
    if (binding.type() == Binding::SOAPBinding) {
        const SoapBinding soapBinding(binding.soapBinding());
        const SoapBinding::Operation op = soapBinding.operations().value(operationName);
        return op.inputHeaders();
    }
    return SoapBinding::Headers();
}

static SoapBinding::Headers getOutputHeaders(const Binding &binding, const QString &operationName)
{
    if (binding.type() == Binding::SOAPBinding) {
        const SoapBinding soapBinding(binding.soapBinding());
        const SoapBinding::Operation op = soapBinding.operations().value(operationName);
        return op.outputHeaders();
    }
    return SoapBinding::Headers();
}

bool Converter::convertClientService()
{
    KODE::Class::List bindingClasses;
    const Service::List services = mWSDL.definitions().services();
    for (const Service &service : services) {
        Q_ASSERT(!service.name().isEmpty());

        QSet<QName> uniqueBindings = mWSDL.uniqueBindings(service);
        // qDebug() << "Looking at" << service.name() << uniqueBindings;

        for (const QName &bindingName : std::as_const(uniqueBindings)) {
            const Binding binding = mWSDL.findBinding(bindingName);

            QString className = KODE::Style::className(service.name());
            QString nameSpace;
            if (uniqueBindings.count() > 1) {
                // Multiple bindings: use Service::Binding as classname.
                nameSpace = className;
                className = KODE::Style::className(bindingName.localName());
            }

            if (!Settings::self()->nameSpace().isEmpty()) {
                if (nameSpace.isEmpty()) {
                    nameSpace = Settings::self()->nameSpace();
                } else {
                    nameSpace = Settings::self()->nameSpace() + QLatin1String("::") + nameSpace;
                }
            }

            KODE::Class newClass(className, nameSpace);
            if (!Settings::self()->exportDeclaration().isEmpty()) {
                newClass.setExportDeclaration(Settings::self()->exportDeclaration());
            }
            newClass.setUseDPointer(true, QLatin1String("d_ptr") /*avoid clash with possible d() method*/);
            newClass.addBaseClass(mQObject);
            newClass.setDocs(service.documentation());

            // Files included in the header
            newClass.addHeaderInclude(QLatin1String("QtCore/QObject"));
            newClass.addHeaderInclude(QLatin1String("QtCore/QString"));
            newClass.addHeaderInclude(QLatin1String("KDSoapClient/KDSoapClientInterface.h"));
            if (Settings::self()->optionalElementType() == Settings::EBoostOptional) {
                newClass.addHeaderInclude(QLatin1String("boost/optional.hpp"));
            } else if (Settings::self()->optionalElementType() == Settings::EStdOptional) {
                newClass.addHeaderInclude(QLatin1String("optional"));
            }

            // Files included in the impl, with optional forward-declarations in the header
            newClass.addInclude(QLatin1String("KDSoapClient/KDSoapMessage.h"), QLatin1String("KDSoapMessage"));
            newClass.addInclude(QLatin1String("KDSoapClient/KDSoapValue.h"), QLatin1String("KDSoapValue"));
            newClass.addInclude(QLatin1String("KDSoapClient/KDSoapPendingCallWatcher.h"), QLatin1String("KDSoapPendingCallWatcher"));
            newClass.addInclude(QLatin1String("KDSoapClient/KDSoapNamespaceManager.h"));

            // Variables (which will go into the d pointer)
            KODE::MemberVariable clientInterfaceVar(QLatin1String("m_clientInterface"), QLatin1String("KDSoapClientInterface*"));
            clientInterfaceVar.setInitializer(QLatin1String("nullptr"));
            newClass.addMemberVariable(clientInterfaceVar);

            KODE::MemberVariable lastReply(QLatin1String("m_lastReply"), QLatin1String("KDSoapMessage"));
            newClass.addMemberVariable(lastReply);

            KODE::MemberVariable endPoint(QLatin1String("m_endPoint"), QLatin1String("QString"));
            newClass.addMemberVariable(endPoint);

            // Ctor and dtor
            {
                KODE::Function ctor(newClass.name());
                ctor.addArgument(KODE::Function::Argument(QLatin1String("QObject* _parent"), QLatin1String("nullptr")));
                ctor.addInitializer(QLatin1String("QObject(_parent)"));
                ctor.setExplicit(true);
                KODE::Function dtor(QLatin1Char('~') + newClass.name());
                KODE::Code ctorCode, dtorCode;

                ctor.setBody(ctorCode);
                newClass.addFunction(ctor);

                dtorCode += "delete d_ptr->m_clientInterface;";

                dtor.setBody(dtorCode);
                newClass.addFunction(dtor);
            }
            // ignoreSslErrors() method
            {
                KODE::Function ignoreSslErrors(QLatin1String("ignoreSslErrors"), QLatin1String("void"));
                KODE::Code code;
                code += "clientInterface()->ignoreSslErrors();";
                ignoreSslErrors.setBody(code);
                ignoreSslErrors.setDocs(QLatin1String("Asks Qt to ignore ssl errors in https requests. Use this for testing only!"));
                newClass.addFunction(ignoreSslErrors);
            }
            // setEndPoint() method
            {
                KODE::Function setEndPoint(QLatin1String("setEndPoint"), QLatin1String("void"));
                setEndPoint.addArgument(QLatin1String("const QString& endPoint"));
                KODE::Code code;
                code += "d_ptr->m_endPoint = endPoint;";
                code += "if (d_ptr->m_clientInterface)";
                code.indent();
                code += "d_ptr->m_clientInterface->setEndPoint( endPoint );";
                code.unindent();
                setEndPoint.setBody(code);
                setEndPoint.setDocs(QLatin1String("Overwrite the end point defined in the .wsdl file, with another http/https URL."));
                newClass.addFunction(setEndPoint);
            }
            // endPoint() accessor
            {
                KODE::Function getEndPoint(QLatin1String("endPoint"), QLatin1String("QString"));
                getEndPoint.setConst(true);
                KODE::Code code;
                code += "return d_ptr->m_endPoint;";
                getEndPoint.setBody(code);
                getEndPoint.setDocs(QLatin1String("Return the end point that will be used."));
                newClass.addFunction(getEndPoint);
            }
            // setSoapVersion() method
            {
                KODE::Function setSoapVersion(QLatin1String("setSoapVersion"), QLatin1String("void"));
                setSoapVersion.addArgument(QLatin1String("KDSoapClientInterface::SoapVersion soapVersion"));
                KODE::Code code;
                code += "clientInterface()->setSoapVersion(soapVersion);";
                setSoapVersion.setBody(code);
                setSoapVersion.setDocs(QLatin1String("Overwrite the soap version defined in the .wsdl file, with another version. \n"
                                                     "version can be KDSoapClientInterface::SOAP1_1 or KDSoapClientInterface::SOAP1_2"));
                newClass.addFunction(setSoapVersion);
            }
            // soapVersion() method
            {
                KODE::Function getSoapVersion(QLatin1String("soapVersion"), QLatin1String("KDSoapClientInterface::SoapVersion"));
                getSoapVersion.setConst(true);
                KODE::Code code;
                code += "return clientInterface()->soapVersion();";
                getSoapVersion.setBody(code);
                getSoapVersion.setDocs(QLatin1String("Return the soap version used.n"));
                newClass.addFunction(getSoapVersion);
            }
            // lastErrorCode() method (which mistakenly returns an int, see github issue #166)
            {
                KODE::Function lastError(QLatin1String("lastErrorCode"), QLatin1String("int"));
                lastError.setConst(true);
                KODE::Code code;
                code += "if (d_ptr->m_lastReply.isFault())";
                code.indent();
                code += "return d_ptr->m_lastReply.childValues().child(QLatin1String(\"faultcode\")).value().toInt();";
                code.unindent();
                code += "return 0;";
                lastError.setBody(code);
                lastError.setDocs(QLatin1String("Return the fault code from the last blocking call.\nEmpty if no error."));
                newClass.addFunction(lastError);
            }
            // lastFaultCode() method
            {
                KODE::Function lastFault(QLatin1String("lastFaultCode"), QLatin1String("QString"));
                lastFault.setConst(true);
                KODE::Code code;
                code += "if (d_ptr->m_lastReply.isFault())";
                code.indent();
                code += "return d_ptr->m_lastReply.childValues().child(QLatin1String(\"faultcode\")).value().toString();";
                code.unindent();
                code += "return QString();";
                lastFault.setBody(code);
                lastFault.setDocs(QLatin1String("Return the fault code from the last blocking call.\nEmpty if no error."));
                newClass.addFunction(lastFault);
            }
            // lastError() method
            {
                KODE::Function lastError(QLatin1String("lastError"), QLatin1String("QString"));
                lastError.setConst(true);
                KODE::Code code;
                code += "if (d_ptr->m_lastReply.isFault())";
                code.indent();
                code += "return d_ptr->m_lastReply.faultAsString();";
                code.unindent();
                code += "return QString();";
                lastError.setBody(code);
                lastError.setDocs(QLatin1String("Return the error from the last blocking call.\nEmpty if no error."));
                newClass.addFunction(lastError);
            }
            // soapError() signal
            {
                KODE::Function errorSignal(QLatin1String("soapError"), QLatin1String("void"), KODE::Function::Signal);
                errorSignal.addArgument(QLatin1String("const QString& method"));
                errorSignal.addArgument(QLatin1String("const KDSoapMessage& fault"));
                errorSignal.setDocs(
                    QLatin1String("This signal is emitted whenever a SOAP call failed, for a central processing of all SOAP errors.\nmethod is the "
                                  "name of the method (or operation) that returned the fault, for instance \"addContact\"."));
                newClass.addFunction(errorSignal);
            }

            QUrl webserviceLocation;

            if (binding.type() == Binding::SOAPBinding) {
                const SoapBinding soapBinding(binding.soapBinding());
                const SoapBinding::Address address = soapBinding.address();
                if (address.location().isValid()) {
                    webserviceLocation = address.location();
                }
            } else {
                // ignore non-SOAP bindings, like HTTP GET and HTTP POST
                continue;
            }

            // clientInterface() methods
            {
                KODE::Function clientInterface(QLatin1String("clientInterface"), QLatin1String("const KDSoapClientInterface*"),
                                               KODE::Function::Public);
                clientInterface.setConst(true);
                KODE::Code code;
                code += "if (!d_ptr->m_clientInterface) {";
                code.indent();
                const QByteArray encoded = webserviceLocation.toEncoded();
                code += QLatin1String("const QString endPoint = !d_ptr->m_endPoint.isEmpty() ? d_ptr->m_endPoint : QString::fromLatin1(\"")
                    + QString::fromLatin1(encoded.data(), encoded.size()) + QLatin1String("\");");
                code += QLatin1String("const QString messageNamespace = QString::fromLatin1(\"") + mWSDL.definitions().targetNamespace()
                    + QLatin1String("\");");
                code += "d_ptr->m_clientInterface = new KDSoapClientInterface(endPoint, messageNamespace);";
                if (soapStyle(binding) == SoapBinding::DocumentStyle) {
                    code += "d_ptr->m_clientInterface->setStyle( KDSoapClientInterface::DocumentStyle );";
                }
                if (binding.version() == Binding::SOAP_1_2) {
                    code += "d_ptr->m_clientInterface->setSoapVersion( KDSoapClientInterface::SOAP1_2 );";
                } else {
                    code += "d_ptr->m_clientInterface->setSoapVersion( KDSoapClientInterface::SOAP1_1 );";
                }
                code.unindent();
                code += "}";
                code += "return d_ptr->m_clientInterface;";
                clientInterface.setBody(code);
                clientInterface.setDocs(QLatin1String(
                    "Returns the underlying KDSoapClientInterface instance, which allows to access setCookieJar, lastResponseHeaders, etc."));
                newClass.addFunction(clientInterface);
            }
            {
                KODE::Function mutableClientInterface(QLatin1String("clientInterface"), QLatin1String("KDSoapClientInterface*"),
                                                      KODE::Function::Public);
                KODE::Code code;
                code += QLatin1String("return const_cast<KDSoapClientInterface*>( const_cast< const ") + newClass.name()
                    + QLatin1String("*>( this )->clientInterface() );");
                mutableClientInterface.setBody(code);
                newClass.addFunction(mutableClientInterface);
            }

            SoapBinding::Headers soapHeaders;

            PortType portType = mWSDL.findPortType(binding.portTypeName());
            // qDebug() << portType.name();
            const Operation::List operations = portType.operations();
            for (const Operation &operation : operations) {
                Operation::OperationType opType = operation.operationType();
                switch (opType) {
                case Operation::OneWayOperation:
                case Operation::RequestResponseOperation: // the standard case
                    if (!Settings::self()->skipSync()) {
                        // sync method
                        if (!convertClientCall(operation, binding, newClass)) {
                            return false;
                        }
                    }

                    if (!Settings::self()->skipAsync()) {
                        // async method
                        convertClientInputMessage(operation, binding, newClass);
                        convertClientOutputMessage(operation, binding, newClass);
                        // TODO fault
                    }
                    break;
                case Operation::SolicitResponseOperation:
                    if (!Settings::self()->skipAsync()) {
                        convertClientOutputMessage(operation, binding, newClass);
                        convertClientInputMessage(operation, binding, newClass);
                        // TODO fault
                    }
                    break;
                case Operation::NotificationOperation:
                    if (!Settings::self()->skipAsync()) {
                        convertClientOutputMessage(operation, binding, newClass);
                    }
                    break;
                }

                // Collect message parts used as headers
                const SoapBinding::Headers inputHeaders = getInputHeaders(binding, operation.name());
                for (const SoapBinding::Header &header : inputHeaders) {
                    if (!soapHeaders.contains(header)) {
                        soapHeaders.append(header);
                    }
                }
            } // end of for each operation

            for (const SoapBinding::Header &header : std::as_const(soapHeaders)) {
                createHeader(header, newClass);
            }
            bindingClasses.append(newClass);
            mHeaderMethods.clear();

            if (!Settings::self()->skipAsyncJobs()) {
                QString jobsNamespace = nameSpace;
                if (uniqueBindings.count() > 1) {
                    // Multiple bindings: use <Service>::<Binding>Jobs as namespace for the job classes
                    jobsNamespace += "::" + KODE::Style::className(bindingName.localName()) + "Jobs";
                }

                // for each operation, create a job class
                for (const Operation &operation : std::as_const(operations)) {
                    Operation::OperationType opType = operation.operationType();
                    if (opType != Operation::SolicitResponseOperation && opType != Operation::RequestResponseOperation) {
                        continue;
                    }

                    const QString operationName = operation.name();
                    KODE::Class jobClass(KODE::Style::className(operation.name()) + QLatin1String("Job"), jobsNamespace);
                    if (mClasses.findClass(jobClass.qualifiedName()) != mClasses.constEnd()) {
                        jobClass.setName(KODE::Style::className(className /*binding name*/ + jobClass.name()));
                    }
                    jobClass.addInclude(QString(), fullyQualified(newClass));
                    jobClass.addHeaderInclude(QLatin1String("KDSoapClient/KDSoapJob.h"));
                    if (!Settings::self()->exportDeclaration().isEmpty()) {
                        jobClass.setExportDeclaration(Settings::self()->exportDeclaration());
                    }

                    jobClass.addBaseClass(KODE::Class(QLatin1String("KDSoapJob")));

                    KODE::MemberVariable serviceVar(QLatin1String("service"), fullyQualified(newClass) + QLatin1Char('*'));
                    jobClass.addMemberVariable(serviceVar);

                    KODE::Function ctor(jobClass.name());
                    ctor.addArgument(KODE::Function::Argument(QString::fromLatin1("%1* service").arg(fullyQualified(newClass))));
                    ctor.addArgument(KODE::Function::Argument(QLatin1String("QObject* _parent"), QLatin1String("nullptr")));
                    ctor.setExplicit(true);
                    ctor.addInitializer(QLatin1String("KDSoapJob(_parent)"));
                    ctor.addInitializer(QLatin1String("mService(service)"));

                    const Message message = mWSDL.findMessage(operation.input().message());
                    const Part::List inputParts = selectedParts(binding, message, operation, true /*input*/);
                    for (const Part &part : inputParts) {
                        const QString partName = part.name();
                        ctor.addInitializer(KODE::MemberVariable::memberVariableName(partName) + "()");
                        jobClass.addHeaderIncludes(mTypeMap.headerIncludes(part.type()));
                    }

                    const Message outputMsg = mWSDL.findMessage(operation.output().message());

                    const Part::List outputParts = selectedParts(binding, outputMsg, operation, false /*output*/);
                    for (const Part &part : outputParts) {
                        const QString varName = mNameMapper.escape(QLatin1String("result") + upperlize(part.name()));
                        ctor.addInitializer(KODE::MemberVariable::memberVariableName(varName) + "()");
                        jobClass.addHeaderIncludes(mTypeMap.headerIncludes(part.type()));
                    }

                    jobClass.addFunction(ctor);

                    QStringList inputGetters;

                    for (const Part &part : inputParts) {
                        const QString varType = mTypeMap.localType(part.type(), part.element());
                        const KODE::MemberVariable member(part.name(), varType);
                        jobClass.addMemberVariable(member);

                        KODE::Function setter(QLatin1String("set") + mNameMapper.escape(upperlize(part.name())), QLatin1String("void"));
                        setter.addArgument(mTypeMap.localInputType(part.type(), part.element()) + QLatin1String(" arg0"));
                        KODE::Code sc;
                        sc += QString::fromLatin1("%1 = arg0;").arg(member.name());
                        setter.setBody(sc);
                        jobClass.addFunction(setter);

                        const QString getterName = mNameMapper.escape(lowerlize(part.name()));
                        inputGetters.append(getterName);
                        KODE::Function getter(getterName, varType);
                        getter.setConst(true);
                        KODE::Code gc;
                        gc += QString::fromLatin1("return %1;").arg(member.name());
                        getter.setBody(gc);
                        jobClass.addFunction(getter);
                    }

                    KODE::Function doStart(QLatin1String("doStart"), QLatin1String("void"), KODE::Function::Protected);
                    doStart.setVirtualMode(KODE::Function::Override);
                    KODE::Code doStartCode;
                    const bool hasAction = clientAddAction(doStartCode, binding, operationName);
                    clientGenerateMessage(doStartCode, binding, message, operation, /*use members=*/true);

                    QString callLine =
                        QString::fromLatin1("KDSoapPendingCall pendingCall = mService->clientInterface()->asyncCall(QLatin1String(\"%1\"), message")
                            .arg(operationName);
                    if (hasAction) {
                        callLine += QLatin1String(", action");
                    }
                    callLine += QLatin1String(", requestHeaders()");
                    callLine += QLatin1String(");");
                    doStartCode += callLine;

                    doStartCode += "KDSoapPendingCallWatcher *watcher = new KDSoapPendingCallWatcher(pendingCall, this);";
                    doStartCode += "QObject::connect(watcher, &KDSoapPendingCallWatcher::finished,\n"
                                   "                 this, &"
                        + jobClass.name() + "::slotFinished);";
                    doStart.setBody(doStartCode);
                    jobClass.addFunction(doStart);

                    KODE::Function slot(QLatin1String("slotFinished"), QLatin1String("void"), KODE::Function::Private | KODE::Function::Slot);
                    slot.addArgument(QLatin1String("KDSoapPendingCallWatcher* watcher"));
                    KODE::Code slotCode;
                    slotCode += QLatin1String("watcher->deleteLater();");
                    slotCode += QLatin1String("KDSoapMessage _reply = watcher->returnMessage();");
                    const SoapBinding::Headers outputHeaders = getOutputHeaders(binding, operationName);

                    if (!outputParts.isEmpty() || !outputHeaders.isEmpty()) {
                        slotCode += QLatin1String("if (!_reply.isFault()) {") + COMMENT;
                        slotCode.indent();

                        if (!outputParts.isEmpty()) {
                            if (soapStyle(binding) == SoapBinding::RPCStyle /*adds a wrapper*/) {
                                Q_ASSERT(outputParts.count() == 1);
                                // Protect the call to .at(0) below
                                slotCode += "if (_reply.childValues().isEmpty()) {";
                                slotCode.indent();
                                slotCode += "_reply.setFault(true);" + COMMENT;
                                slotCode += "_reply.addArgument(QString::fromLatin1(\"faultcode\"), QString::fromLatin1(\"Server.EmptyResponse\"));";
                                slotCode += QLatin1String("return;");
                                slotCode.unindent();
                                slotCode += "}";

                                slotCode += QLatin1String("_reply = _reply.childValues().at(0);") + COMMENT;
                            }

                            for (const Part &part : std::as_const(outputParts)) {
                                const QString varName = mNameMapper.escape(QLatin1String("result") + upperlize(part.name()));
                                const KODE::MemberVariable member(varName, QString());
                                slotCode.addBlock(
                                    deserializeRetVal(part, QLatin1String("_reply"), mTypeMap.localType(part.type(), part.element()), member.name()));

                                addJobResultMember(jobClass, part, varName, inputGetters);
                            }
                        }
                        for (const SoapBinding::Header &header : std::as_const(outputHeaders)) {
                            const QName messageName = header.message();
                            const QString partName = header.part();
                            const Message message = mWSDL.findMessage(messageName);
                            const Part part = message.partByName(partName);

                            const QString varName = QLatin1String("resultHeader") + upperlize(part.name());
                            const KODE::MemberVariable member(varName, QString());
                            const QString getHeader =
                                QString::fromLatin1("watcher->returnHeaders().header(QLatin1String(\"%1\"), QLatin1String(\"%2\"))")
                                    .arg(part.element().localName(), part.element().nameSpace());
                            slotCode.addBlock(deserializeRetVal(part, getHeader, mTypeMap.localType(part.type(), part.element()), member.name()));
                            addJobResultMember(jobClass, part, varName, inputGetters);
                        }

                        slotCode.unindent();
                        slotCode += QLatin1String("}");
                    }
                    slotCode += QLatin1String("emitFinished(_reply, watcher->returnHeaders());");
                    slot.setBody(slotCode);
                    jobClass.addFunction(slot);

                    mClasses.addClass(jobClass);
                } // end of for each operation (job creation)
            } // end if async job API creation is not skipped
        } // end of for each port
    } // end of for each service

    // First sort all classes so that the order compiles
    QStringList excludedClasses;
    excludedClasses << QLatin1String("KDSoapServerObjectInterface");
    excludedClasses << QLatin1String("KDSoapJob");
    mClasses.sortByDependencies(excludedClasses);
    // Then add the server base classes
    mServerClasses.sortAlphabetically();
    mClasses += mServerClasses;
    // Then add the client service(s)
    bindingClasses.sortAlphabetically();
    mClasses += bindingClasses;
    return true;
}

void Converter::clientAddOneArgument(KODE::Function &callFunc, const Part &part, KODE::Class &newClass)
{
    const QString lowerName = lowerlize(part.name());
    const QString argType = mTypeMap.localInputType(part.type(), part.element());
    // qDebug() << "localInputType" << part.type().qname() << part.element().qname() << "->" << argType;
    if (argType != QLatin1String("void")) {
        QString def;
        if (part.type().isEmpty()) { // element (document style)
            // If the complex type is empty, we need it for serialization,
            // but we don't need the app to see it, so give it a default value.
            // Example:
            // TNS__LogoutResponse logout( const TNS__Logout& parameters = TNS__Logout() );
            const XSD::Element element = mWSDL.findElement(part.element());
            const XSD::ComplexType ctype = mWSDL.findComplexType(element.type());
            if (!ctype.isNull() && ctype.isEmpty()) {
                def = mTypeMap.localType(part.type(), part.element()) + QLatin1String("()");
                // def += "/* " + element.type().qname() + " is empty */";
            }
        }

        KODE::Function::Argument arg(argType + QLatin1Char(' ') + mNameMapper.escape(lowerName), def);
        callFunc.addArgument(arg);
    }
    newClass.addHeaderIncludes(mTypeMap.headerIncludes(part.type()));
}

void Converter::clientAddArguments(KODE::Function &callFunc, const Message &message, KODE::Class &newClass, const Operation &operation,
                                   const Binding &binding)
{
    const Part::List parts = selectedParts(binding, message, operation, true /*input*/);
    for (const Part &part : parts) {
        clientAddOneArgument(callFunc, part, newClass);
    }
}

bool Converter::clientAddAction(KODE::Code &code, const Binding &binding, const QString &operationName)
{
    bool hasAction = false;
    if (binding.type() == Binding::SOAPBinding) {
        const SoapBinding soapBinding(binding.soapBinding());
        const SoapBinding::Operation op = soapBinding.operations().value(operationName);
        code += QLatin1String("const QString action = QString::fromLatin1(\"") + op.action() + QLatin1String("\");");
        hasAction = true;
    }
    return hasAction;
}

// Maybe the "qualified" bool isn't useful after all, now that we make sure
// that all messages are qualified (it's really only configurable for child elements)
QName Converter::elementNameForPart(const Part &part, bool *qualified, bool *nillable) const
{
    if (part.type().isEmpty()) { // element (document style)
        XSD::Element element = mWSDL.findElement(part.element());
        *qualified = element.isQualified();
        *nillable = element.nillable();
        return element.qualifiedName();
    } else { // type (rpc style)
        *qualified = false;
        *nillable = false;
        return QName(part.nameSpace(), part.name()); // NOTE: part.nameSpace() is usually empty.
    }
}

void Converter::addMessageArgument(KODE::Code &code, SoapBinding::Style bindingStyle, const Part &part, const QString &localVariableName,
                                   const QByteArray &messageName, bool varIsMember)
{
    const QString partname =
        varIsMember ? KODE::MemberVariable::memberVariableName(localVariableName) : mNameMapper.escape(lowerlize(localVariableName));
    const QString nilPartname = varIsMember ? KODE::MemberVariable::memberVariableName(localVariableName + "_nil") : partname + "_nil";
    // In document style, the "part" is directly added as arguments
    // See https://www.ibm.com/developerworks/webservices/library/ws-whichwsdl/
    if (bindingStyle == SoapBinding::DocumentStyle) {
        code.addBlock(serializePart(part, partname, nilPartname, messageName, false));
    } else {
        const QString argType = mTypeMap.localType(part.type(), part.element());
        if (argType != QLatin1String("void")) {
            code.addBlock(serializePart(part, partname, nilPartname, messageName + ".childValues()", true));
        }
    }
}

void Converter::clientGenerateMessage(KODE::Code &code, const Binding &binding, const Message &message, const Operation &operation,
                                      bool varsAreMembers)
{
    code += "KDSoapMessage message;";

    if (binding.type() == Binding::SOAPBinding) {
        const SoapBinding soapBinding = binding.soapBinding();
        const SoapBinding::Operation op = soapBinding.operations().value(operation.name());
        if (op.input().use() == SoapBinding::EncodedUse) {
            code += "message.setUse(KDSoapMessage::EncodedUse);";
        } else {
            code += "message.setUse(KDSoapMessage::LiteralUse);";
        }
        // qDebug() << "input headers:" << op.inputHeaders().count();

        if (!op.input().nameSpace().isEmpty()) {
            code += "message.setNamespaceUri(QString::fromLatin1(\"" + op.input().nameSpace() + "\"));";
        }
    }

    bool isBuiltin = false;

    const Part::List inputParts = selectedParts(binding, message, operation, true /*input*/);
    for (const Part &part : inputParts) {
        isBuiltin = isBuiltin || mTypeMap.isBuiltinType(part.type(), part.element());
        addMessageArgument(code, soapStyle(binding), part, part.name(), "message", varsAreMembers);
    }

    if (soapStyle(binding) == SoapBinding::DocumentStyle && message.parts().size() > 1 && isBuiltin) {
        qWarning(
            "A Document style cannot be formed with multiple parts of builtin type (eg : multiple parts xsd:string), please correct your WSDL file");
    }
}

KODE::Code Converter::deserializeRetVal(const KWSDL::Part &part, const QString &replyMsgName, const QString &qtRetType, const QString &varName) const
{
    // This is the opposite logic as appendElementArg, which does:
    // if builtin -> xml element with basic contents
    // if complex -> serialize and add result
    // else -> serialize

    KODE::Code code;
    const bool isBuiltin = mTypeMap.isBuiltinType(part.type(), part.element());
    const bool isComplex = mTypeMap.isComplexType(part.type(), part.element());
    const bool isPolymorphic = mTypeMap.isPolymorphic(part.type(), part.element());
    if (isBuiltin) {
        code += varName + QLatin1String(" = ") + mTypeMap.deserializeBuiltin(part.type(), part.element(), replyMsgName, qtRetType)
            + QLatin1String(";") + COMMENT;
    } else if (isComplex) {
        const QString op = isPolymorphic ? "->" : ".";
        code += varName + op + QLatin1String("deserialize(") + replyMsgName + QLatin1String(");") + COMMENT;
    } else {
        // testcase: MyWsdlDocument::sendTelegram
        code += varName + QLatin1String(".deserialize(") + replyMsgName + QLatin1String(");") + COMMENT;
    }
    return code;
}

// Generate synchronous call
bool Converter::convertClientCall(const Operation &operation, const Binding &binding, KODE::Class &newClass)
{
    const QString methodName = lowerlize(operation.name());
    KODE::Function callFunc(mNameMapper.escape(methodName), QLatin1String("void"), KODE::Function::Public);
    callFunc.setDocs(QString::fromLatin1("Blocking call to %1.\nNot recommended in a GUI thread.").arg(operation.name()));
    const Message inputMessage = mWSDL.findMessage(operation.input().message());
    Message outputMessage;
    if (operation.operationType() != Operation::OneWayOperation) {
        outputMessage = mWSDL.findMessage(operation.output().message());
    }
    clientAddArguments(callFunc, inputMessage, newClass, operation, binding);
    KODE::Code code;
    const bool hasAction = clientAddAction(code, binding, operation.name());
    clientGenerateMessage(code, binding, inputMessage, operation);
    QString callLine =
        QLatin1String("d_ptr->m_lastReply = clientInterface()->call(QLatin1String(\"") + operation.name() + QLatin1String("\"), message");
    if (hasAction) {
        callLine += QLatin1String(", action");
    }
    callLine += QLatin1String(");");
    code += callLine;

    // Return value(s) :
    const Part::List outParts = selectedParts(binding, outputMessage, operation, false /*output*/);
    const int numReturnValues = outParts.count();

    if (numReturnValues == 1) {
        const Part &retPart = outParts.first();
        const QString retType = mTypeMap.localType(retPart.type(), retPart.element());
        if (retType.isEmpty()) {
            qWarning("Could not generate operation '%s'", qPrintable(operation.name()));
            return false;
        }
        callFunc.setReturnType(retType);
        newClass.addHeaderIncludes(mTypeMap.headerIncludes(retPart.type()));

        code += "if (d_ptr->m_lastReply.isFault())";
        code.indent();
        code += QLatin1String("return ") + retType + QLatin1String("();"); // default-constructed value
        code.unindent();

        // WARNING: if you change the logic below, also adapt the result parsing for async calls

        if (retType != QLatin1String("void")) {
            if (soapStyle(binding) == SoapBinding::DocumentStyle /*no wrapper*/) {
                code += retType + QLatin1String(" ret;"); // local var
                code.addBlock(deserializeRetVal(retPart, QLatin1String("d_ptr->m_lastReply"), retType, QLatin1String("ret")));
                code += QLatin1String("return ret;") + COMMENT;
            } else { // RPC style (adds a wrapper), or simple value
                // Protect the call to .at(0) below
                code += "if (d_ptr->m_lastReply.childValues().isEmpty()) {";
                code.indent();
                code += "d_ptr->m_lastReply.setFault(true);";
                code += "d_ptr->m_lastReply.addArgument(QString::fromLatin1(\"faultcode\"), QString::fromLatin1(\"Server.EmptyResponse\"));";
                code += QLatin1String("return ") + retType + QLatin1String("();"); // default-constructed value
                code.unindent();
                code += "}";

                code += retType + QLatin1String(" ret;"); // local var
                code += QLatin1String("const KDSoapValue val = d_ptr->m_lastReply.childValues().at(0);") + COMMENT;
                ElementArgumentSerializer serializer(mTypeMap, retPart.type(), retPart.element(), QLatin1String("ret"), QString());
                code += serializer.demarshalVariable("val");
                code += "return ret;";
            }
        }

    } else if (numReturnValues > 1) {
        // Add each output argument as a non-const ref to the method. A bit ugly but no other way.

        code += "if (d_ptr->m_lastReply.isFault())";
        code.indent();
        code += QLatin1String("return;") + COMMENT;
        code.unindent();
        Q_ASSERT(soapStyle(binding) == SoapBinding::DocumentStyle); // RPC with multiple return values? impossible, we generate a single wrapper

        for (const Part &part : std::as_const(outParts)) {
            const QString argType = mTypeMap.localType(part.type(), part.element());
            Q_ASSERT(!argType.isEmpty());
            const QString lowerName = lowerlize(part.name());
            KODE::Function::Argument arg(argType + QLatin1String("& ") + mNameMapper.escape(lowerName));
            callFunc.addArgument(arg);
            newClass.addHeaderIncludes(mTypeMap.headerIncludes(part.type()));

            code.addBlock(deserializeRetVal(part, QLatin1String("d_ptr->m_lastReply"), argType, lowerName));
        }
    }

    callFunc.setBody(code);

    newClass.addFunction(callFunc);
    return true;
}

// Generate async call method
void Converter::convertClientInputMessage(const Operation &operation, const Binding &binding, KODE::Class &newClass)
{
    QString operationName = operation.name();
    KODE::Function asyncFunc(QLatin1String("async") + upperlize(operationName), QLatin1String("void"), KODE::Function::Public);
    asyncFunc.setDocs(QString::fromLatin1("Asynchronous call to %1.\n"
                                          "Remember to connect to %2 and %3.\n"
                                          "Deprecated, use %4 instead.")
                          .arg(operation.name(),
                               lowerlize(operationName) + QLatin1String("Done"),
                               lowerlize(operationName) + QLatin1String("Error"),
                               upperlize(operationName) + QLatin1String("Job")));
    const Message message = mWSDL.findMessage(operation.input().message());
    clientAddArguments(asyncFunc, message, newClass, operation, binding);
    KODE::Code code;
    const bool hasAction = clientAddAction(code, binding, operation.name());
    clientGenerateMessage(code, binding, message, operation);

    QString callLine = QLatin1String("KDSoapPendingCall pendingCall = clientInterface()->asyncCall(QLatin1String(\"") + operationName
        + QLatin1String("\"), message");
    if (hasAction) {
        callLine += QLatin1String(", action");
    }
    callLine += QLatin1String(");");
    code += callLine;

    if (operation.operationType() == Operation::RequestResponseOperation || operation.operationType() == Operation::OneWayOperation) {
        const QString finishedSlotName = QLatin1String("_kd_slot") + upperlize(operationName) + QLatin1String("Finished");

        code += "KDSoapPendingCallWatcher *watcher = new KDSoapPendingCallWatcher(pendingCall, this);";
        code += QLatin1String("QObject::connect(watcher, &KDSoapPendingCallWatcher::finished,\n"
                              "                 this, &")
            + newClass.name() + QLatin1String("::")
            + finishedSlotName + QLatin1String(");");
        asyncFunc.setBody(code);
        newClass.addFunction(asyncFunc);
    }
}

// Generate signals and the result slot, for async calls
void Converter::convertClientOutputMessage(const Operation &operation, const Binding &binding, KODE::Class &newClass)
{
    // result signal
    const QString operationName = lowerlize(operation.name());
    const QString &signalBase = operationName;
    const QString callName = QLatin1String("async") + upperlize(operation.name());
    KODE::Function doneSignal(signalBase + QLatin1String("Done"), QLatin1String("void"), KODE::Function::Signal);
    doneSignal.setDocs(QLatin1String("This signal is emitted whenever the asynchronous call ") + callName + QLatin1String("() has succeeded."));

    // error signal
    KODE::Function errorSignal(signalBase + QLatin1String("Error"), QLatin1String("void"), KODE::Function::Signal);
    errorSignal.addArgument(QLatin1String("const KDSoapMessage& fault"));
    errorSignal.setDocs(QLatin1String("This signal is emitted whenever the asynchronous call ") + callName + QLatin1String("() has failed."));

    // finished slot
    const QString finishedSlotName = QLatin1String("_kd_slot") + upperlize(operationName) + QLatin1String("Finished");
    KODE::Function finishedSlot(finishedSlotName, QLatin1String("void"), KODE::Function::Slot | KODE::Function::Private);
    finishedSlot.addArgument(QLatin1String("KDSoapPendingCallWatcher* watcher"));

    // If one output message is used by two input messages, don't define
    // it twice.
    // DF: what if the arguments are different? ...
    // if ( newClass.hasFunction( respSignal.name() ) )
    //  return;

    KODE::Code slotCode;
    slotCode += "const KDSoapMessage reply = watcher->returnMessage();";
    slotCode += "if (reply.isFault()) {";
    slotCode.indent();
    slotCode += QLatin1String("Q_EMIT ") + errorSignal.name() + QLatin1String("(reply);") + COMMENT;
    slotCode += QLatin1String("Q_EMIT soapError(QLatin1String(\"") + operationName + QLatin1String("\"), reply);");
    slotCode.unindent();
    slotCode += "} else {";
    slotCode.indent();

    QStringList partNames;

    if (operation.operationType() != Operation::OneWayOperation) {
        const Message message = mWSDL.findMessage(operation.output().message());

        const Part::List parts = selectedParts(binding, message, operation, false /*output*/);
        for (const Part &part : parts) {
            const QString partType = mTypeMap.localType(part.type(), part.element());
            if (partType.isEmpty()) {
                qWarning("Skipping part '%s'", qPrintable(part.name()));
                continue;
            }

            if (partType == QLatin1String("void")) {
                continue;
            }

            QString lowerName = mNameMapper.escape(lowerlize(part.name()));
            doneSignal.addArgument(mTypeMap.localInputType(part.type(), part.element()) + QLatin1Char(' ') + lowerName);

            // WARNING: if you change the logic below, also adapt the result parsing for sync calls, above

            if (soapStyle(binding) == SoapBinding::DocumentStyle /*no wrapper*/) {
                slotCode += partType + QLatin1String(" ret;"); // local var
                slotCode.addBlock(deserializeRetVal(part, QLatin1String("reply"), partType, QLatin1String("ret")));
                partNames << QLatin1String("ret");
            } else { // RPC style (adds a wrapper) or simple value
                QString value = QLatin1String("reply.childValues().child(QLatin1String(\"") + part.name() + QLatin1String("\"))");
                const bool isBuiltin = mTypeMap.isBuiltinType(part.type(), part.element());
                if (isBuiltin) {
                    partNames << value + QLatin1String(".value().value<") + partType + QLatin1String(">()");
                } else {
                    slotCode += partType + QLatin1String(" ret;"); // local var. TODO ret1/ret2 etc. if more than one.
                    slotCode += QLatin1String("ret.deserialize(") + value + QLatin1String(");") + COMMENT;
                    partNames << QLatin1String("ret");
                }
            }

            // Forward declaration of element class
            // newClass.addIncludes( QStringList(), mTypeMap.forwardDeclarationsForElement( part.element() ) );
        }
    }

    newClass.addFunction(doneSignal);
    newClass.addFunction(errorSignal);

    slotCode += QLatin1String("Q_EMIT ") + doneSignal.name() + QLatin1String("( ") + partNames.join(QLatin1String(",")) + QLatin1String(" );");
    slotCode.unindent();
    slotCode += '}';
    slotCode += "watcher->deleteLater();";

    finishedSlot.setBody(slotCode);

    newClass.addFunction(finishedSlot);
}

void Converter::createHeader(const SoapBinding::Header &header, KODE::Class &newClass)
{
    const QName messageName = header.message();
    const QString partName = header.part();
    const Message message = mWSDL.findMessage(messageName);
    const Part part = message.partByName(partName);

    {
        QString methodName = QLatin1String("set") + upperlize(partName);
        if (!methodName.endsWith(QLatin1String("Header"))) {
            methodName += QLatin1String("Header");
        }

        const QString argType = mTypeMap.localInputType(part.type(), part.element());
        const QString methodSig = methodName + QLatin1Char('(') + argType + QLatin1Char(')');
        if (mHeaderMethods.contains(methodSig)) {
            return; // already have it (testcase: Services.wsdl)
        }

        mHeaderMethods.insert(methodSig);
        KODE::Function headerSetter(methodName, QLatin1String("void"), KODE::Function::Public);
        headerSetter.setDocs(QString::fromLatin1("Sets the header '%1', for all subsequent method calls.").arg(partName));
        clientAddOneArgument(headerSetter, part, newClass);
        KODE::Code code;
        code += "KDSoapMessage message;";
        if (header.use() == SoapBinding::EncodedUse) {
            code += "message.setUse(KDSoapMessage::EncodedUse);";
        } else {
            code += "message.setUse(KDSoapMessage::LiteralUse);";
        }
        if (!header.nameSpace().isEmpty()) {
            code += "message.setNamespaceUri(QString::fromLatin1(\"" + header.nameSpace() + "\"));";
        }
        addMessageArgument(code, SoapBinding::RPCStyle, part, partName, "message");
        code += QLatin1String("clientInterface()->setHeader( QLatin1String(\"") + partName + QLatin1String("\"), message );");
        headerSetter.setBody(code);
        newClass.addFunction(headerSetter);
    }

    // Let's also generate a clear method
    {
        QString methodName = QLatin1String("clear") + upperlize(partName);
        if (!methodName.endsWith(QLatin1String("Header"))) {
            methodName += QLatin1String("Header");
        }
        KODE::Function headerClearer(methodName, QLatin1String("void"), KODE::Function::Public);
        headerClearer.setDocs(QString::fromLatin1("Removes the header '%1', for all subsequent method calls.").arg(partName));
        KODE::Code code;
        code += QLatin1String("clientInterface()->setHeader( QLatin1String(\"") + partName + QLatin1String("\"), KDSoapMessage() );");
        headerClearer.setBody(code);
        newClass.addFunction(headerClearer);
    }
}

void Converter::addJobResultMember(KODE::Class &jobClass, const Part &part, const QString &varName, const QStringList &inputGetters)
{
    const QString varType = mTypeMap.localType(part.type(), part.element());
    const KODE::MemberVariable member(varName, varType);
    jobClass.addMemberVariable(member);

    QString getterName = mNameMapper.escape(lowerlize(part.name()));
    while (inputGetters.contains(getterName)) {
        getterName = mNameMapper.escape(QLatin1String("result") + upperlize(getterName));
    }

    KODE::Function getter(getterName, varType);
    getter.setConst(true);
    KODE::Code gc;
    gc += QString::fromLatin1("return %1;").arg(member.name());
    getter.setBody(gc);
    jobClass.addFunction(getter);
}
