/*
    SPDX-FileCopyrightText: 2019 Mark Nauwelaerts <mark.nauwelaerts@gmail.com>

    SPDX-License-Identifier: MIT
*/

#include "lspclientserver.h"
#include "lspclientplugin.h"

#include "lspclient_debug.h"

#include <QProcess>
#include <QScopedPointer>
#include <QVariantMap>

#include <QCoreApplication>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QTime>
#include <QtEndian>
#include <utility>

// good/bad old school; allows easier concatenate
#define CONTENT_LENGTH "Content-Length"

static const QString MEMBER_ID = QStringLiteral("id");
static const QString MEMBER_METHOD = QStringLiteral("method");
static const QString MEMBER_ERROR = QStringLiteral("error");
static const QString MEMBER_CODE = QStringLiteral("code");
static const QString MEMBER_MESSAGE = QStringLiteral("message");
static const QString MEMBER_PARAMS = QStringLiteral("params");
static const QString MEMBER_RESULT = QStringLiteral("result");
static const QString MEMBER_URI = QStringLiteral("uri");
static const QString MEMBER_VERSION = QStringLiteral("version");
static const QString MEMBER_START = QStringLiteral("start");
static const QString MEMBER_END = QStringLiteral("end");
static const QString MEMBER_POSITION = QStringLiteral("position");
static const QString MEMBER_LOCATION = QStringLiteral("location");
static const QString MEMBER_RANGE = QStringLiteral("range");
static const QString MEMBER_LINE = QStringLiteral("line");
static const QString MEMBER_CHARACTER = QStringLiteral("character");
static const QString MEMBER_KIND = QStringLiteral("kind");
static const QString MEMBER_TEXT = QStringLiteral("text");
static const QString MEMBER_LANGID = QStringLiteral("languageId");
static const QString MEMBER_LABEL = QStringLiteral("label");
static const QString MEMBER_DOCUMENTATION = QStringLiteral("documentation");
static const QString MEMBER_DETAIL = QStringLiteral("detail");
static const QString MEMBER_COMMAND = QStringLiteral("command");
static const QString MEMBER_EDIT = QStringLiteral("edit");
static const QString MEMBER_TITLE = QStringLiteral("title");
static const QString MEMBER_ARGUMENTS = QStringLiteral("arguments");
static const QString MEMBER_DIAGNOSTICS = QStringLiteral("diagnostics");
static const QString MEMBER_TARGET_URI = QStringLiteral("targetUri");
static const QString MEMBER_TARGET_RANGE = QStringLiteral("targetRange");
static const QString MEMBER_TARGET_SELECTION_RANGE = QStringLiteral("targetSelectionRange");

// message construction helpers
static QJsonObject to_json(const LSPPosition &pos)
{
    return QJsonObject{{MEMBER_LINE, pos.line()}, {MEMBER_CHARACTER, pos.column()}};
}

static QJsonObject to_json(const LSPRange &range)
{
    return QJsonObject{{MEMBER_START, to_json(range.start())}, {MEMBER_END, to_json(range.end())}};
}

static QJsonValue to_json(const LSPLocation &location)
{
    if (location.uri.isValid()) {
        return QJsonObject{{MEMBER_URI, location.uri.toString()}, {MEMBER_RANGE, to_json(location.range)}};
    }
    return QJsonValue();
}

static QJsonValue to_json(const LSPDiagnosticRelatedInformation &related)
{
    auto loc = to_json(related.location);
    if (loc.isObject()) {
        return QJsonObject{{MEMBER_LOCATION, to_json(related.location)}, {MEMBER_MESSAGE, related.message}};
    }
    return QJsonValue();
}

static QJsonObject to_json(const LSPDiagnostic &diagnostic)
{
    // required
    auto result = QJsonObject();
    result[MEMBER_RANGE] = to_json(diagnostic.range);
    result[MEMBER_MESSAGE] = diagnostic.message;
    // optional
    if (!diagnostic.code.isEmpty()) {
        result[QStringLiteral("code")] = diagnostic.code;
    }
    if (diagnostic.severity != LSPDiagnosticSeverity::Unknown) {
        result[QStringLiteral("severity")] = static_cast<int>(diagnostic.severity);
    }
    if (!diagnostic.source.isEmpty()) {
        result[QStringLiteral("source")] = diagnostic.source;
    }
    QJsonArray relatedInfo;
    for (const auto &vrelated : diagnostic.relatedInformation) {
        auto related = to_json(vrelated);
        if (related.isObject()) {
            relatedInfo.push_back(related);
        }
    }
    result[QStringLiteral("relatedInformation")] = relatedInfo;
    return result;
}

static QJsonArray to_json(const QList<LSPTextDocumentContentChangeEvent> &changes)
{
    QJsonArray result;
    for (const auto &change : changes) {
        result.push_back(QJsonObject{{MEMBER_RANGE, to_json(change.range)}, {MEMBER_TEXT, change.text}});
    }
    return result;
}

static QJsonObject versionedTextDocumentIdentifier(const QUrl &document, int version = -1)
{
    QJsonObject map{{MEMBER_URI, document.toString()}};
    if (version >= 0) {
        map[MEMBER_VERSION] = version;
    }
    return map;
}

static QJsonObject textDocumentItem(const QUrl &document, const QString &lang, const QString &text, int version)
{
    auto map = versionedTextDocumentIdentifier(document, version);
    map[MEMBER_TEXT] = text;
    map[MEMBER_LANGID] = lang;
    return map;
}

static QJsonObject textDocumentParams(const QJsonObject &m)
{
    return QJsonObject{{QStringLiteral("textDocument"), m}};
}

static QJsonObject textDocumentParams(const QUrl &document, int version = -1)
{
    return textDocumentParams(versionedTextDocumentIdentifier(document, version));
}

static QJsonObject textDocumentPositionParams(const QUrl &document, LSPPosition pos)
{
    auto params = textDocumentParams(document);
    params[MEMBER_POSITION] = to_json(pos);
    return params;
}

static QJsonObject referenceParams(const QUrl &document, LSPPosition pos, bool decl)
{
    auto params = textDocumentPositionParams(document, pos);
    params[QStringLiteral("context")] = QJsonObject{{QStringLiteral("includeDeclaration"), decl}};
    return params;
}

static QJsonObject formattingOptions(const LSPFormattingOptions &_options)
{
    auto options = _options.extra;
    options[QStringLiteral("tabSize")] = _options.tabSize;
    options[QStringLiteral("insertSpaces")] = _options.insertSpaces;
    return options;
}

static QJsonObject documentRangeFormattingParams(const QUrl &document, const LSPRange *range, const LSPFormattingOptions &_options)
{
    auto params = textDocumentParams(document);
    if (range) {
        params[MEMBER_RANGE] = to_json(*range);
    }
    params[QStringLiteral("options")] = formattingOptions(_options);
    return params;
}

static QJsonObject documentOnTypeFormattingParams(const QUrl &document, const LSPPosition &pos, const QChar &lastChar, const LSPFormattingOptions &_options)
{
    auto params = textDocumentPositionParams(document, pos);
    params[QStringLiteral("ch")] = QString(lastChar);
    params[QStringLiteral("options")] = formattingOptions(_options);
    return params;
}

static QJsonObject renameParams(const QUrl &document, const LSPPosition &pos, const QString &newName)
{
    auto params = textDocumentPositionParams(document, pos);
    params[QStringLiteral("newName")] = newName;
    return params;
}

static QJsonObject codeActionParams(const QUrl &document, const LSPRange &range, const QList<QString> &kinds, const QList<LSPDiagnostic> &diagnostics)
{
    auto params = textDocumentParams(document);
    params[MEMBER_RANGE] = to_json(range);
    QJsonObject context;
    QJsonArray diags;
    for (const auto &diagnostic : diagnostics) {
        diags.push_back(to_json(diagnostic));
    }
    context[MEMBER_DIAGNOSTICS] = diags;
    if (kinds.length()) {
        context[QStringLiteral("only")] = QJsonArray::fromStringList(kinds);
    }
    params[QStringLiteral("context")] = context;
    return params;
}

static QJsonObject executeCommandParams(const QString &command, const QJsonValue &args)
{
    return QJsonObject{{MEMBER_COMMAND, command}, {MEMBER_ARGUMENTS, args}};
}

static QJsonObject applyWorkspaceEditResponse(const LSPApplyWorkspaceEditResponse &response)
{
    return QJsonObject{{QStringLiteral("applied"), response.applied}, {QStringLiteral("failureReason"), response.failureReason}};
}

static QJsonObject changeConfigurationParams(const QJsonValue &settings)
{
    return QJsonObject{{QStringLiteral("settings"), settings}};
}

static void from_json(QVector<QChar> &trigger, const QJsonValue &json)
{
    for (const auto &t : json.toArray()) {
        auto st = t.toString();
        if (st.length()) {
            trigger.push_back(st.at(0));
        }
    }
}

static void from_json(LSPCompletionOptions &options, const QJsonValue &json)
{
    if (json.isObject()) {
        auto ob = json.toObject();
        options.provider = true;
        options.resolveProvider = ob.value(QStringLiteral("resolveProvider")).toBool();
        from_json(options.triggerCharacters, ob.value(QStringLiteral("triggerCharacters")));
    }
}

static void from_json(LSPSignatureHelpOptions &options, const QJsonValue &json)
{
    if (json.isObject()) {
        auto ob = json.toObject();
        options.provider = true;
        from_json(options.triggerCharacters, ob.value(QStringLiteral("triggerCharacters")));
    }
}

static void from_json(LSPDocumentOnTypeFormattingOptions &options, const QJsonValue &json)
{
    if (json.isObject()) {
        auto ob = json.toObject();
        options.provider = true;
        from_json(options.triggerCharacters, ob.value(QStringLiteral("moreTriggerCharacter")));
        auto trigger = ob.value(QStringLiteral("firstTriggerCharacter")).toString();
        if (trigger.size()) {
            options.triggerCharacters.insert(0, trigger.at(0));
        }
    }
}

static void from_json(LSPSemanticHighlightingOptions &options, const QJsonValue &json)
{
    if (!json.isObject()) {
        return;
    }
    const auto scopes = json.toObject().value(QStringLiteral("scopes"));
    options.scopes.clear();
    QVector<QString> entries;
    for (const auto &scope_entry : scopes.toArray()) {
        const auto json_entries = scope_entry.toArray();
        entries.reserve(json_entries.size());
        for (const auto &inner_json_entry : json_entries) {
            entries.push_back(inner_json_entry.toString());
        }
    }
    options.scopes.scopesToAttrVector(entries);
}

static void from_json(LSPServerCapabilities &caps, const QJsonObject &json)
{
    auto sync = json.value(QStringLiteral("textDocumentSync"));
    caps.textDocumentSync = static_cast<LSPDocumentSyncKind>(
        (sync.isObject() ? sync.toObject().value(QStringLiteral("change")) : sync).toInt(static_cast<int>(LSPDocumentSyncKind::None)));
    caps.hoverProvider = json.value(QStringLiteral("hoverProvider")).toBool();
    from_json(caps.completionProvider, json.value(QStringLiteral("completionProvider")));
    from_json(caps.signatureHelpProvider, json.value(QStringLiteral("signatureHelpProvider")));
    caps.definitionProvider = json.value(QStringLiteral("definitionProvider")).toBool();
    caps.declarationProvider = json.value(QStringLiteral("declarationProvider")).toBool();
    caps.referencesProvider = json.value(QStringLiteral("referencesProvider")).toBool();
    caps.implementationProvider = json.value(QStringLiteral("implementationProvider")).toBool();
    caps.documentSymbolProvider = json.value(QStringLiteral("documentSymbolProvider")).toBool();
    caps.documentHighlightProvider = json.value(QStringLiteral("documentHighlightProvider")).toBool();
    caps.documentFormattingProvider = json.value(QStringLiteral("documentFormattingProvider")).toBool();
    caps.documentRangeFormattingProvider = json.value(QStringLiteral("documentRangeFormattingProvider")).toBool();
    from_json(caps.documentOnTypeFormattingProvider, json.value(QStringLiteral("documentOnTypeFormattingProvider")));
    caps.renameProvider = json.value(QStringLiteral("renameProvider")).toBool();
    auto codeActionProvider = json.value(QStringLiteral("codeActionProvider"));
    caps.codeActionProvider = codeActionProvider.toBool() || codeActionProvider.isObject();
    from_json(caps.semanticHighlightingProvider, json.value(QStringLiteral("semanticHighlighting")).toObject());
}

// follow suit; as performed in kate docmanager
// normalize at this stage/layer to avoid surprises elsewhere
// sadly this is not a single QUrl method as one might hope ...
static QUrl normalizeUrl(const QUrl &url)
{
    // Resolve symbolic links for local files (done anyway in KTextEditor)
    if (url.isLocalFile()) {
        QString normalizedUrl = QFileInfo(url.toLocalFile()).canonicalFilePath();
        if (!normalizedUrl.isEmpty()) {
            return QUrl::fromLocalFile(normalizedUrl);
        }
    }

    // else: cleanup only the .. stuff
    return url.adjusted(QUrl::NormalizePathSegments);
}

static LSPResponseError parseResponseError(const QJsonValue &v)
{
    LSPResponseError ret;
    if (v.isObject()) {
        const auto &vm = v.toObject();
        ret.code = LSPErrorCode(vm.value(MEMBER_CODE).toInt());
        ret.message = vm.value(MEMBER_MESSAGE).toString();
        ret.data = vm.value(QStringLiteral("data"));
    }
    return ret;
}

static LSPMarkupContent parseMarkupContent(const QJsonValue &v)
{
    LSPMarkupContent ret;
    if (v.isObject()) {
        const auto &vm = v.toObject();
        ret.value = vm.value(QStringLiteral("value")).toString();
        auto kind = vm.value(MEMBER_KIND).toString();
        if (kind == QLatin1String("plaintext")) {
            ret.kind = LSPMarkupKind::PlainText;
        } else if (kind == QLatin1String("markdown")) {
            ret.kind = LSPMarkupKind::MarkDown;
        }
    } else if (v.isString()) {
        ret.kind = LSPMarkupKind::PlainText;
        ret.value = v.toString();
    }
    return ret;
}

static LSPPosition parsePosition(const QJsonObject &m)
{
    auto line = m.value(MEMBER_LINE).toInt(-1);
    auto column = m.value(MEMBER_CHARACTER).toInt(-1);
    return {line, column};
}

static bool isPositionValid(const LSPPosition &pos)
{
    return pos.isValid();
}

static LSPRange parseRange(const QJsonObject &range)
{
    auto startpos = parsePosition(range.value(MEMBER_START).toObject());
    auto endpos = parsePosition(range.value(MEMBER_END).toObject());
    return {startpos, endpos};
}

static LSPLocation parseLocation(const QJsonObject &loc)
{
    auto uri = normalizeUrl(QUrl(loc.value(MEMBER_URI).toString()));
    auto range = parseRange(loc.value(MEMBER_RANGE).toObject());
    return {QUrl(uri), range};
}

static LSPLocation parseLocationLink(const QJsonObject &loc)
{
    auto uri = normalizeUrl(QUrl(loc.value(MEMBER_TARGET_URI).toString()));
    // both should be present, selection contained by the other
    // so let's preferentially pick the smallest one
    auto vrange = loc.value(MEMBER_TARGET_SELECTION_RANGE);
    if (vrange.isUndefined()) {
        vrange = loc.value(MEMBER_TARGET_RANGE);
    }
    auto range = parseRange(vrange.toObject());
    return {QUrl(uri), range};
}

static LSPDocumentHighlight parseDocumentHighlight(const QJsonValue &result)
{
    auto hover = result.toObject();
    auto range = parseRange(hover.value(MEMBER_RANGE).toObject());
    // default is DocumentHighlightKind.Text
    auto kind = static_cast<LSPDocumentHighlightKind>(hover.value(MEMBER_KIND).toInt(static_cast<int>(LSPDocumentHighlightKind::Text)));
    return {range, kind};
}

static QList<LSPDocumentHighlight> parseDocumentHighlightList(const QJsonValue &result)
{
    QList<LSPDocumentHighlight> ret;
    // could be array
    if (result.isArray()) {
        for (const auto &def : result.toArray()) {
            ret.push_back(parseDocumentHighlight(def));
        }
    } else if (result.isObject()) {
        // or a single value
        ret.push_back(parseDocumentHighlight(result));
    }
    return ret;
}

static LSPMarkupContent parseHoverContentElement(const QJsonValue &contents)
{
    LSPMarkupContent result;
    if (contents.isString()) {
        result.value = contents.toString();
    } else {
        // should be object, pretend so
        auto cont = contents.toObject();
        auto text = cont.value(QStringLiteral("value")).toString();
        if (text.isEmpty()) {
            // nothing to lose, try markdown
            result = parseMarkupContent(contents);
        } else {
            result.value = text;
        }
    }
    if (result.value.length()) {
        result.kind = LSPMarkupKind::PlainText;
    }
    return result;
}

static LSPHover parseHover(const QJsonValue &result)
{
    LSPHover ret;
    auto hover = result.toObject();
    // normalize content which can be of many forms
    ret.range = parseRange(hover.value(MEMBER_RANGE).toObject());
    auto contents = hover.value(QStringLiteral("contents"));

    // support the deprecated MarkedString[] variant, used by e.g. Rust rls
    if (contents.isArray()) {
        for (const auto &c : contents.toArray()) {
            ret.contents.push_back(parseHoverContentElement(c));
        }
    } else {
        ret.contents.push_back(parseHoverContentElement(contents));
    }

    return ret;
}

static QList<LSPSymbolInformation> parseDocumentSymbols(const QJsonValue &result)
{
    // the reply could be old SymbolInformation[] or new (hierarchical) DocumentSymbol[]
    // try to parse it adaptively in any case
    // if new style, hierarchy is specified clearly in reply
    // if old style, it is assumed the values enter linearly, that is;
    // * a parent/container is listed before its children
    // * if a name is defined/declared several times and then used as a parent,
    //   then we try to find such a parent whose range contains current range
    //   (otherwise fall back to using the last instance as a parent)

    QList<LSPSymbolInformation> ret;
    QMultiMap<QString, LSPSymbolInformation *> index;

    std::function<void(const QJsonObject &symbol, LSPSymbolInformation *parent)> parseSymbol = [&](const QJsonObject &symbol, LSPSymbolInformation *parent) {
        const auto &location = symbol.value(MEMBER_LOCATION).toObject();
        const auto &mrange = symbol.contains(MEMBER_RANGE) ? symbol.value(MEMBER_RANGE) : location.value(MEMBER_RANGE);
        auto range = parseRange(mrange.toObject());
        // if flat list, try to find parent by name
        if (!parent) {
            auto container = symbol.value(QStringLiteral("containerName")).toString();
            auto it = index.find(container);
            // default to last inserted
            if (it != index.end()) {
                parent = it.value();
            }
            // but prefer a containing range
            while (it != index.end() && it.key() == container) {
                if (it.value()->range.contains(range)) {
                    parent = it.value();
                    break;
                }
                ++it;
            }
        }
        auto list = parent ? &parent->children : &ret;
        if (isPositionValid(range.start()) && isPositionValid(range.end())) {
            auto name = symbol.value(QStringLiteral("name")).toString();
            auto kind = static_cast<LSPSymbolKind>(symbol.value(MEMBER_KIND).toInt());
            auto detail = symbol.value(MEMBER_DETAIL).toString();
            list->push_back({name, kind, range, detail});
            index.insert(name, &list->back());
            // proceed recursively
            for (const auto &child : symbol.value(QStringLiteral("children")).toArray()) {
                parseSymbol(child.toObject(), &list->back());
            }
        }
    };

    for (const auto &info : result.toArray()) {
        parseSymbol(info.toObject(), nullptr);
    }
    return ret;
}

static QList<LSPLocation> parseDocumentLocation(const QJsonValue &result)
{
    QList<LSPLocation> ret;
    // could be array
    if (result.isArray()) {
        for (const auto &def : result.toArray()) {
            const auto &ob = def.toObject();
            ret.push_back(parseLocation(ob));
            // bogus server might have sent LocationLink[] instead
            // let's try to handle it, but not announce in capabilities
            if (ret.back().uri.isEmpty()) {
                ret.back() = parseLocationLink(ob);
            }
        }
    } else if (result.isObject()) {
        // or a single value
        ret.push_back(parseLocation(result.toObject()));
    }
    return ret;
}

static QList<LSPCompletionItem> parseDocumentCompletion(const QJsonValue &result)
{
    QList<LSPCompletionItem> ret;
    QJsonArray items = result.toArray();
    // might be CompletionList
    if (items.empty()) {
        items = result.toObject().value(QStringLiteral("items")).toArray();
    }
    for (const auto &vitem : items) {
        const auto &item = vitem.toObject();
        auto label = item.value(MEMBER_LABEL).toString();
        auto detail = item.value(MEMBER_DETAIL).toString();
        auto doc = parseMarkupContent(item.value(MEMBER_DOCUMENTATION));
        auto sortText = item.value(QStringLiteral("sortText")).toString();
        if (sortText.isEmpty()) {
            sortText = label;
        }
        auto insertText = item.value(QStringLiteral("insertText")).toString();
        if (insertText.isEmpty()) {
            insertText = label;
        }
        auto kind = static_cast<LSPCompletionItemKind>(item.value(MEMBER_KIND).toInt());
        ret.push_back({label, kind, detail, doc, sortText, insertText});
    }
    return ret;
}

static LSPSignatureInformation parseSignatureInformation(const QJsonObject &json)
{
    LSPSignatureInformation info;

    info.label = json.value(MEMBER_LABEL).toString();
    info.documentation = parseMarkupContent(json.value(MEMBER_DOCUMENTATION));
    for (const auto &rpar : json.value(QStringLiteral("parameters")).toArray()) {
        auto par = rpar.toObject();
        auto label = par.value(MEMBER_LABEL);
        int begin = -1, end = -1;
        if (label.isArray()) {
            auto range = label.toArray();
            if (range.size() == 2) {
                begin = range.at(0).toInt(-1);
                end = range.at(1).toInt(-1);
                if (begin > info.label.length()) {
                    begin = -1;
                }
                if (end > info.label.length()) {
                    end = -1;
                }
            }
        } else {
            auto sub = label.toString();
            if (sub.length()) {
                begin = info.label.indexOf(sub);
                if (begin >= 0) {
                    end = begin + sub.length();
                }
            }
        }
        info.parameters.push_back({begin, end});
    }
    return info;
}

static LSPSignatureHelp parseSignatureHelp(const QJsonValue &result)
{
    LSPSignatureHelp ret;
    QJsonObject sig = result.toObject();

    for (const auto &info : sig.value(QStringLiteral("signatures")).toArray()) {
        ret.signatures.push_back(parseSignatureInformation(info.toObject()));
    }
    ret.activeSignature = sig.value(QStringLiteral("activeSignature")).toInt(0);
    ret.activeParameter = sig.value(QStringLiteral("activeParameter")).toInt(0);
    ret.activeSignature = qMin(qMax(ret.activeSignature, 0), ret.signatures.size());
    ret.activeParameter = qMax(ret.activeParameter, 0);
    if (!ret.signatures.isEmpty()) {
        ret.activeParameter = qMin(ret.activeParameter, ret.signatures.at(ret.activeSignature).parameters.size());
    }
    return ret;
}

static QString parseClangdSwitchSourceHeader(const QJsonValue &result)
{
    return result.toString();
}

static QList<LSPTextEdit> parseTextEdit(const QJsonValue &result)
{
    QList<LSPTextEdit> ret;
    for (const auto &redit : result.toArray()) {
        auto edit = redit.toObject();
        auto text = edit.value(QStringLiteral("newText")).toString();
        auto range = parseRange(edit.value(MEMBER_RANGE).toObject());
        ret.push_back({range, text});
    }
    return ret;
}

static LSPWorkspaceEdit parseWorkSpaceEdit(const QJsonValue &result)
{
    QHash<QUrl, QList<LSPTextEdit>> ret;
    auto changes = result.toObject().value(QStringLiteral("changes")).toObject();
    for (auto it = changes.begin(); it != changes.end(); ++it) {
        ret.insert(normalizeUrl(QUrl(it.key())), parseTextEdit(it.value()));
    }
    return {ret};
}

static LSPCommand parseCommand(const QJsonObject &result)
{
    auto title = result.value(MEMBER_TITLE).toString();
    auto command = result.value(MEMBER_COMMAND).toString();
    auto args = result.value(MEMBER_ARGUMENTS).toArray();
    return {title, command, args};
}

static QList<LSPDiagnostic> parseDiagnostics(const QJsonArray &result)
{
    QList<LSPDiagnostic> ret;
    for (const auto &vdiag : result) {
        auto diag = vdiag.toObject();
        auto range = parseRange(diag.value(MEMBER_RANGE).toObject());
        auto severity = static_cast<LSPDiagnosticSeverity>(diag.value(QStringLiteral("severity")).toInt());
        auto code = diag.value(QStringLiteral("code")).toString();
        auto source = diag.value(QStringLiteral("source")).toString();
        auto message = diag.value(MEMBER_MESSAGE).toString();
        auto relatedInfo = diag.value(QStringLiteral("relatedInformation")).toArray();
        QList<LSPDiagnosticRelatedInformation> relatedInfoList;
        for (const auto &vrelated : relatedInfo) {
            auto related = vrelated.toObject();
            auto relLocation = parseLocation(related.value(MEMBER_LOCATION).toObject());
            auto relMessage = related.value(MEMBER_MESSAGE).toString();
            relatedInfoList.push_back({relLocation, relMessage});
        }
        ret.push_back({range, severity, code, source, message, relatedInfoList});
    }
    return ret;
}

static QList<LSPCodeAction> parseCodeAction(const QJsonValue &result)
{
    QList<LSPCodeAction> ret;
    for (const auto &vaction : result.toArray()) {
        auto action = vaction.toObject();
        // entry could be Command or CodeAction
        if (!action.value(MEMBER_COMMAND).isString()) {
            // CodeAction
            auto title = action.value(MEMBER_TITLE).toString();
            auto kind = action.value(MEMBER_KIND).toString();
            auto command = parseCommand(action.value(MEMBER_COMMAND).toObject());
            auto edit = parseWorkSpaceEdit(action.value(MEMBER_EDIT));
            auto diagnostics = parseDiagnostics(action.value(MEMBER_DIAGNOSTICS).toArray());
            ret.push_back({title, kind, diagnostics, edit, command});
        } else {
            // Command
            auto command = parseCommand(action);
            ret.push_back({command.title, QString(), {}, {}, command});
        }
    }
    return ret;
}

static LSPPublishDiagnosticsParams parseDiagnostics(const QJsonObject &result)
{
    LSPPublishDiagnosticsParams ret;

    ret.uri = normalizeUrl(QUrl(result.value(MEMBER_URI).toString()));
    ret.diagnostics = parseDiagnostics(result.value(MEMBER_DIAGNOSTICS).toArray());
    return ret;
}

static LSPApplyWorkspaceEditParams parseApplyWorkspaceEditParams(const QJsonObject &result)
{
    LSPApplyWorkspaceEditParams ret;

    ret.label = result.value(MEMBER_LABEL).toString();
    ret.edit = parseWorkSpaceEdit(result.value(MEMBER_EDIT));
    return ret;
}

static LSPVersionedTextDocumentIdentifier parseVersionedTextDocumentIdentifier(const QJsonObject &result)
{
    LSPVersionedTextDocumentIdentifier ret;
    ret.uri = normalizeUrl(QUrl(result.value(MEMBER_URI).toString()));
    ret.version = result.value(QStringLiteral("version")).toInt(-1);
    return ret;
}

static LSPShowMessageParams parseMessage(const QJsonObject &result)
{
    LSPShowMessageParams ret;

    ret.type = static_cast<LSPMessageType>(result.value(QStringLiteral("type")).toInt());
    ret.message = result.value(MEMBER_MESSAGE).toString();
    return ret;
}

static LSPSemanticHighlightingParams parseSemanticHighlighting(const QJsonObject &result)
{
    LSPSemanticHighlightingParams ret;
    ret.textDocument = parseVersionedTextDocumentIdentifier(result.value(QStringLiteral("textDocument")).toObject());
    for (const auto &line_json : result.value(QStringLiteral("lines")).toArray()) {
        const auto line_obj = line_json.toObject();
        LSPSemanticHighlightingInformation info;
        info.line = line_obj.value(QStringLiteral("line")).toInt(-1);

        const auto tokenString = line_obj.value(QStringLiteral("tokens"));
        constexpr auto TokenSize = sizeof(LSPSemanticHighlightingToken);
        // the raw tokens are in big endian, we may need to convert that to little endian
        const auto rawTokens = QByteArray::fromBase64(tokenString.toString().toUtf8());
        if (rawTokens.size() % TokenSize != 0) {
            qCWarning(LSPCLIENT) << "unexpected raw token size" << rawTokens.size() << "for string" << tokenString << "in line" << info.line;
            continue;
        }
        const auto numTokens = rawTokens.size() / TokenSize;
        const auto *begin = reinterpret_cast<const LSPSemanticHighlightingToken *>(rawTokens.constData());
        const auto *end = begin + numTokens;
        info.tokens.resize(numTokens);
        std::transform(begin, end, info.tokens.begin(), [](const LSPSemanticHighlightingToken &rawToken) {
            LSPSemanticHighlightingToken token;
            token.character = qFromBigEndian(rawToken.character);
            token.length = qFromBigEndian(rawToken.length);
            token.scope = qFromBigEndian(rawToken.scope);
            return token;
        });
        ret.lines.push_back(info);
    }
    return ret;
}

using GenericReplyType = QJsonValue;
using GenericReplyHandler = ReplyHandler<GenericReplyType>;

class LSPClientServer::LSPClientServerPrivate
{
    typedef LSPClientServerPrivate self_type;

    LSPClientServer *q;
    // server cmd line
    QStringList m_server;
    // workspace root to pass along
    QUrl m_root;
    // language id
    QString m_langId;
    // user provided init
    QJsonValue m_init;
    // server process
    QProcess m_sproc;
    // server declared capabilities
    LSPServerCapabilities m_capabilities;
    // server state
    State m_state = State::None;
    // last msg id
    int m_id = 0;
    // receive buffer
    QByteArray m_receive;
    // registered reply handlers
    // (result handler, error result handler)
    QHash<int, std::pair<GenericReplyHandler, GenericReplyHandler>> m_handlers;
    // pending request responses
    static constexpr int MAX_REQUESTS = 5;
    QVector<int> m_requests{MAX_REQUESTS + 1};

public:
    LSPClientServerPrivate(LSPClientServer *_q, const QStringList &server, const QUrl &root, const QString &langId, const QJsonValue &init)
        : q(_q)
        , m_server(server)
        , m_root(root)
        , m_langId(langId)
        , m_init(init)
    {
        // setup async reading
        QObject::connect(&m_sproc, &QProcess::readyRead, utils::mem_fun(&self_type::read, this));
        QObject::connect(&m_sproc, &QProcess::stateChanged, utils::mem_fun(&self_type::onStateChanged, this));
    }

    ~LSPClientServerPrivate()
    {
        stop(TIMEOUT_SHUTDOWN, TIMEOUT_SHUTDOWN);
    }

    const QStringList &cmdline() const
    {
        return m_server;
    }

    const QUrl &root() const
    {
        return m_root;
    }

    const QString &langId() const
    {
        return m_langId;
    }

    State state()
    {
        return m_state;
    }

    const LSPServerCapabilities &capabilities()
    {
        return m_capabilities;
    }

    int cancel(int reqid)
    {
        if (m_handlers.remove(reqid) > 0) {
            auto params = QJsonObject{{MEMBER_ID, reqid}};
            write(init_request(QStringLiteral("$/cancelRequest"), params));
        }
        return -1;
    }

private:
    void setState(State s)
    {
        if (m_state != s) {
            m_state = s;
            Q_EMIT q->stateChanged(q);
        }
    }

    RequestHandle write(const QJsonObject &msg, const GenericReplyHandler &h = nullptr, const GenericReplyHandler &eh = nullptr, const int *id = nullptr)
    {
        RequestHandle ret;
        ret.m_server = q;

        if (!running()) {
            return ret;
        }

        auto ob = msg;
        ob.insert(QStringLiteral("jsonrpc"), QStringLiteral("2.0"));
        // notification == no handler
        if (h) {
            ob.insert(MEMBER_ID, ++m_id);
            ret.m_id = m_id;
            m_handlers[m_id] = {h, eh};
        } else if (id) {
            ob.insert(MEMBER_ID, *id);
        }

        QJsonDocument json(ob);
        auto sjson = json.toJson();

        qCInfo(LSPCLIENT) << "calling" << msg[MEMBER_METHOD].toString();
        qCDebug(LSPCLIENT) << "sending message:\n" << QString::fromUtf8(sjson);
        // some simple parsers expect length header first
        auto hdr = QStringLiteral(CONTENT_LENGTH ": %1\r\n").arg(sjson.length());
        // write is async, so no blocking wait occurs here
        m_sproc.write(hdr.toLatin1());
        m_sproc.write("\r\n");
        m_sproc.write(sjson);

        return ret;
    }

    RequestHandle send(const QJsonObject &msg, const GenericReplyHandler &h = nullptr, const GenericReplyHandler &eh = nullptr)
    {
        if (m_state == State::Running) {
            return write(msg, h, eh);
        } else {
            qCWarning(LSPCLIENT) << "send for non-running server";
        }
        return RequestHandle();
    }

    void read()
    {
        // accumulate in buffer
        m_receive.append(m_sproc.readAllStandardOutput());

        // try to get one (or more) message
        QByteArray &buffer = m_receive;

        while (true) {
            qCDebug(LSPCLIENT) << "buffer size" << buffer.length();
            auto header = QByteArray(CONTENT_LENGTH ":");
            int index = buffer.indexOf(header);
            if (index < 0) {
                // avoid collecting junk
                if (buffer.length() > 1 << 20) {
                    buffer.clear();
                }
                break;
            }
            index += header.length();
            int endindex = buffer.indexOf("\r\n", index);
            auto msgstart = buffer.indexOf("\r\n\r\n", index);
            if (endindex < 0 || msgstart < 0) {
                break;
            }
            msgstart += 4;
            bool ok = false;
            auto length = buffer.mid(index, endindex - index).toInt(&ok, 10);
            // FIXME perhaps detect if no reply for some time
            // then again possibly better left to user to restart in such case
            if (!ok) {
                qCWarning(LSPCLIENT) << "invalid " CONTENT_LENGTH;
                // flush and try to carry on to some next header
                buffer.remove(0, msgstart);
                continue;
            }
            // sanity check to avoid extensive buffering
            if (length > 1 << 29) {
                qCWarning(LSPCLIENT) << "excessive size";
                buffer.clear();
                continue;
            }
            if (msgstart + length > buffer.length()) {
                break;
            }
            // now onto payload
            auto payload = buffer.mid(msgstart, length);
            buffer.remove(0, msgstart + length);
            qCInfo(LSPCLIENT) << "got message payload size " << length;
            qCDebug(LSPCLIENT) << "message payload:\n" << payload;
            QJsonParseError error{};
            auto msg = QJsonDocument::fromJson(payload, &error);
            if (error.error != QJsonParseError::NoError || !msg.isObject()) {
                qCWarning(LSPCLIENT) << "invalid response payload";
                continue;
            }
            auto result = msg.object();
            // check if it is the expected result
            int msgid = -1;
            if (result.contains(MEMBER_ID)) {
                // allow id to be returned as a string value, happens e.g. for Perl LSP server
                const auto idValue = result[MEMBER_ID];
                if (idValue.isString()) {
                    msgid = idValue.toString().toInt();
                } else {
                    msgid = idValue.toInt();
                }
            } else {
                processNotification(result);
                continue;
            }
            // could be request
            if (result.contains(MEMBER_METHOD)) {
                processRequest(result);
                continue;
            }

            // a valid reply; what to do with it now
            auto it = m_handlers.find(msgid);
            if (it != m_handlers.end()) {
                // copy handler to local storage
                const auto handler = *it;

                // remove handler from our set, do this pre handler execution to avoid races
                m_handlers.erase(it);

                // run handler, might e.g. trigger some new LSP actions for this server
                // process and provide error if caller interested,
                // otherwise reply will resolve to 'empty' response
                auto &h = handler.first;
                auto &eh = handler.second;
                if (result.contains(MEMBER_ERROR) && eh) {
                    eh(result.value(MEMBER_ERROR));
                } else {
                    h(result.value(MEMBER_RESULT));
                }
            } else {
                // could have been canceled
                qCDebug(LSPCLIENT) << "unexpected reply id" << msgid;
            }
        }
    }

    static QJsonObject init_error(const LSPErrorCode code, const QString &msg)
    {
        return QJsonObject{{MEMBER_ERROR, QJsonObject{{MEMBER_CODE, static_cast<int>(code)}, {MEMBER_MESSAGE, msg}}}};
    }

    static QJsonObject init_request(const QString &method, const QJsonObject &params = QJsonObject())
    {
        return QJsonObject{{MEMBER_METHOD, method}, {MEMBER_PARAMS, params}};
    }

    static QJsonObject init_response(const QJsonValue &result = QJsonValue())
    {
        return QJsonObject{{MEMBER_RESULT, result}};
    }

    bool running()
    {
        return m_sproc.state() == QProcess::Running;
    }

    void onStateChanged(QProcess::ProcessState nstate)
    {
        if (nstate == QProcess::NotRunning) {
            setState(State::None);
        }
    }

    void shutdown()
    {
        if (m_state == State::Running) {
            qCInfo(LSPCLIENT) << "shutting down" << m_server;
            // cancel all pending
            m_handlers.clear();
            // shutdown sequence
            send(init_request(QStringLiteral("shutdown")));
            // maybe we will get/see reply on the above, maybe not
            // but not important or useful either way
            send(init_request(QStringLiteral("exit")));
            // no longer fit for regular use
            setState(State::Shutdown);
        }
    }

    void onInitializeReply(const QJsonValue &value)
    {
        // only parse parts that we use later on
        from_json(m_capabilities, value.toObject().value(QStringLiteral("capabilities")).toObject());
        // finish init
        initialized();
    }

    void initialize(LSPClientPlugin *plugin)
    {
        QJsonObject codeAction{{QStringLiteral("codeActionLiteralSupport"),
                                QJsonObject{{QStringLiteral("codeActionKind"), QJsonObject{{QStringLiteral("valueSet"), QJsonArray()}}}}}};
        QJsonObject capabilities{{QStringLiteral("textDocument"),
                                  QJsonObject{{
                                                  QStringLiteral("documentSymbol"),
                                                  QJsonObject{{QStringLiteral("hierarchicalDocumentSymbolSupport"), true}},
                                              },
                                              {QStringLiteral("publishDiagnostics"), QJsonObject{{QStringLiteral("relatedInformation"), true}}},
                                              {QStringLiteral("codeAction"), codeAction},
                                              {QStringLiteral("semanticHighlightingCapabilities"),
                                               QJsonObject{{QStringLiteral("semanticHighlighting"), !plugin || plugin->m_semanticHighlighting}}}}}};
        // NOTE a typical server does not use root all that much,
        // other than for some corner case (in) requests
        QJsonObject params{{QStringLiteral("processId"), QCoreApplication::applicationPid()},
                           {QStringLiteral("rootPath"), m_root.toLocalFile()},
                           {QStringLiteral("rootUri"), m_root.toString()},
                           {QStringLiteral("capabilities"), capabilities},
                           {QStringLiteral("initializationOptions"), m_init}};
        //
        write(init_request(QStringLiteral("initialize"), params), utils::mem_fun(&self_type::onInitializeReply, this));
    }

    void initialized()
    {
        write(init_request(QStringLiteral("initialized")));
        setState(State::Running);
    }

public:
    bool start(LSPClientPlugin *plugin)
    {
        if (m_state != State::None) {
            return true;
        }

        auto program = m_server.front();
        auto args = m_server;
        args.pop_front();
        qCInfo(LSPCLIENT) << "starting" << m_server << "with root" << m_root;

        // start LSP server in project root
        m_sproc.setWorkingDirectory(m_root.toLocalFile());

        // at least we see some errors somewhere then
        m_sproc.setProcessChannelMode(QProcess::ForwardedErrorChannel);
        m_sproc.setReadChannel(QProcess::QProcess::StandardOutput);
        m_sproc.start(program, args);
        bool result = m_sproc.waitForStarted();
        if (!result) {
            qCWarning(LSPCLIENT) << m_sproc.error();
        } else {
            setState(State::Started);
            // perform initial handshake
            initialize(plugin);
        }
        return result;
    }

    void stop(int to_term, int to_kill)
    {
        if (running()) {
            shutdown();
            if ((to_term >= 0) && !m_sproc.waitForFinished(to_term)) {
                m_sproc.terminate();
            }
            if ((to_kill >= 0) && !m_sproc.waitForFinished(to_kill)) {
                m_sproc.kill();
            }
        }
    }

    RequestHandle documentSymbols(const QUrl &document, const GenericReplyHandler &h, const GenericReplyHandler &eh)
    {
        auto params = textDocumentParams(document);
        return send(init_request(QStringLiteral("textDocument/documentSymbol"), params), h, eh);
    }

    RequestHandle documentDefinition(const QUrl &document, const LSPPosition &pos, const GenericReplyHandler &h)
    {
        auto params = textDocumentPositionParams(document, pos);
        return send(init_request(QStringLiteral("textDocument/definition"), params), h);
    }

    RequestHandle documentDeclaration(const QUrl &document, const LSPPosition &pos, const GenericReplyHandler &h)
    {
        auto params = textDocumentPositionParams(document, pos);
        return send(init_request(QStringLiteral("textDocument/declaration"), params), h);
    }

    RequestHandle documentImplementation(const QUrl &document, const LSPPosition &pos, const GenericReplyHandler &h)
    {
        auto params = textDocumentPositionParams(document, pos);
        return send(init_request(QStringLiteral("textDocument/implementation"), params), h);
    }

    RequestHandle documentHover(const QUrl &document, const LSPPosition &pos, const GenericReplyHandler &h)
    {
        auto params = textDocumentPositionParams(document, pos);
        return send(init_request(QStringLiteral("textDocument/hover"), params), h);
    }

    RequestHandle documentHighlight(const QUrl &document, const LSPPosition &pos, const GenericReplyHandler &h)
    {
        auto params = textDocumentPositionParams(document, pos);
        return send(init_request(QStringLiteral("textDocument/documentHighlight"), params), h);
    }

    RequestHandle documentReferences(const QUrl &document, const LSPPosition &pos, bool decl, const GenericReplyHandler &h)
    {
        auto params = referenceParams(document, pos, decl);
        return send(init_request(QStringLiteral("textDocument/references"), params), h);
    }

    RequestHandle documentCompletion(const QUrl &document, const LSPPosition &pos, const GenericReplyHandler &h)
    {
        auto params = textDocumentPositionParams(document, pos);
        return send(init_request(QStringLiteral("textDocument/completion"), params), h);
    }

    RequestHandle signatureHelp(const QUrl &document, const LSPPosition &pos, const GenericReplyHandler &h)
    {
        auto params = textDocumentPositionParams(document, pos);
        return send(init_request(QStringLiteral("textDocument/signatureHelp"), params), h);
    }

    RequestHandle clangdSwitchSourceHeader(const QUrl &document, const GenericReplyHandler &h)
    {
        auto params = QJsonObject{{MEMBER_URI, document.toString()}};
        return send(init_request(QStringLiteral("textDocument/switchSourceHeader"), params), h);
    }

    RequestHandle documentFormatting(const QUrl &document, const LSPFormattingOptions &options, const GenericReplyHandler &h)
    {
        auto params = documentRangeFormattingParams(document, nullptr, options);
        return send(init_request(QStringLiteral("textDocument/formatting"), params), h);
    }

    RequestHandle documentRangeFormatting(const QUrl &document, const LSPRange &range, const LSPFormattingOptions &options, const GenericReplyHandler &h)
    {
        auto params = documentRangeFormattingParams(document, &range, options);
        return send(init_request(QStringLiteral("textDocument/rangeFormatting"), params), h);
    }

    RequestHandle
    documentOnTypeFormatting(const QUrl &document, const LSPPosition &pos, QChar lastChar, const LSPFormattingOptions &options, const GenericReplyHandler &h)
    {
        auto params = documentOnTypeFormattingParams(document, pos, lastChar, options);
        return send(init_request(QStringLiteral("textDocument/onTypeFormatting"), params), h);
    }

    RequestHandle documentRename(const QUrl &document, const LSPPosition &pos, const QString &newName, const GenericReplyHandler &h)
    {
        auto params = renameParams(document, pos, newName);
        return send(init_request(QStringLiteral("textDocument/rename"), params), h);
    }

    RequestHandle
    documentCodeAction(const QUrl &document, const LSPRange &range, const QList<QString> &kinds, QList<LSPDiagnostic> diagnostics, const GenericReplyHandler &h)
    {
        auto params = codeActionParams(document, range, kinds, std::move(diagnostics));
        return send(init_request(QStringLiteral("textDocument/codeAction"), params), h);
    }

    void executeCommand(const QString &command, const QJsonValue &args)
    {
        auto params = executeCommandParams(command, args);
        send(init_request(QStringLiteral("workspace/executeCommand"), params));
    }

    void didOpen(const QUrl &document, int version, const QString &langId, const QString &text)
    {
        auto params = textDocumentParams(textDocumentItem(document, langId, text, version));
        send(init_request(QStringLiteral("textDocument/didOpen"), params));
    }

    void didChange(const QUrl &document, int version, const QString &text, const QList<LSPTextDocumentContentChangeEvent> &changes)
    {
        Q_ASSERT(text.isEmpty() || changes.empty());
        auto params = textDocumentParams(document, version);
        params[QStringLiteral("contentChanges")] = text.size() ? QJsonArray{QJsonObject{{MEMBER_TEXT, text}}} : to_json(changes);
        send(init_request(QStringLiteral("textDocument/didChange"), params));
    }

    void didSave(const QUrl &document, const QString &text)
    {
        auto params = textDocumentParams(document);
        params[QStringLiteral("text")] = text;
        send(init_request(QStringLiteral("textDocument/didSave"), params));
    }

    void didClose(const QUrl &document)
    {
        auto params = textDocumentParams(document);
        send(init_request(QStringLiteral("textDocument/didClose"), params));
    }

    void didChangeConfiguration(const QJsonValue &settings)
    {
        auto params = changeConfigurationParams(settings);
        send(init_request(QStringLiteral("workspace/didChangeConfiguration"), params));
    }

    void processNotification(const QJsonObject &msg)
    {
        auto method = msg[MEMBER_METHOD].toString();
        if (method == QLatin1String("textDocument/publishDiagnostics")) {
            Q_EMIT q->publishDiagnostics(parseDiagnostics(msg[MEMBER_PARAMS].toObject()));
        } else if (method == QLatin1String("textDocument/semanticHighlighting")) {
            Q_EMIT q->semanticHighlighting(parseSemanticHighlighting(msg[MEMBER_PARAMS].toObject()));
        } else if (method == QLatin1String("window/showMessage")) {
            Q_EMIT q->showMessage(parseMessage(msg[MEMBER_PARAMS].toObject()));
        } else if (method == QLatin1String("window/logMessage")) {
            Q_EMIT q->logMessage(parseMessage(msg[MEMBER_PARAMS].toObject()));
        } else {
            qCWarning(LSPCLIENT) << "discarding notification" << method;
        }
    }

    GenericReplyHandler prepareResponse(int msgid)
    {
        // allow limited number of outstanding requests
        auto ctx = QPointer<LSPClientServer>(q);
        m_requests.push_back(msgid);
        if (m_requests.size() > MAX_REQUESTS) {
            m_requests.pop_front();
        }
        auto h = [ctx, this, msgid](const GenericReplyType &response) {
            if (!ctx) {
                return;
            }
            auto index = m_requests.indexOf(msgid);
            if (index >= 0) {
                m_requests.remove(index);
                write(init_response(response), nullptr, nullptr, &msgid);
            } else {
                qCWarning(LSPCLIENT) << "discarding response" << msgid;
            }
        };
        return h;
    }

    template<typename ReplyType>
    static ReplyHandler<ReplyType> responseHandler(const GenericReplyHandler &h,
                                                   typename utils::identity<std::function<GenericReplyType(const ReplyType &)>>::type c)
    {
        return [h, c](const ReplyType &m) {
            h(c(m));
        };
    }

    // pretty rare and limited use, but anyway
    void processRequest(const QJsonObject &msg)
    {
        auto method = msg[MEMBER_METHOD].toString();
        auto msgid = msg[MEMBER_ID].toInt();
        auto params = msg[MEMBER_PARAMS];
        bool handled = false;
        if (method == QLatin1String("workspace/applyEdit")) {
            auto h = responseHandler<LSPApplyWorkspaceEditResponse>(prepareResponse(msgid), applyWorkspaceEditResponse);
            Q_EMIT q->applyEdit(parseApplyWorkspaceEditParams(params.toObject()), h, handled);
        } else {
            write(init_error(LSPErrorCode::MethodNotFound, method), nullptr, nullptr, &msgid);
            qCWarning(LSPCLIENT) << "discarding request" << method;
        }
    }
};

// generic convert handler
// sprinkle some connection-like context safety
// not so likely relevant/needed due to typical sequence of events,
// but in case the latter would be changed in surprising ways ...
template<typename ReplyType>
static GenericReplyHandler
make_handler(const ReplyHandler<ReplyType> &h, const QObject *context, typename utils::identity<std::function<ReplyType(const GenericReplyType &)>>::type c)
{
    // empty provided handler leads to empty handler
    if (!h || !c) {
        return nullptr;
    }

    QPointer<const QObject> ctx(context);
    return [ctx, h, c](const GenericReplyType &m) {
        if (ctx) {
            h(c(m));
        }
    };
}

LSPClientServer::LSPClientServer(const QStringList &server, const QUrl &root, const QString &langId, const QJsonValue &init)
    : d(new LSPClientServerPrivate(this, server, root, langId, init))
{
}

LSPClientServer::~LSPClientServer()
{
    delete d;
}

const QStringList &LSPClientServer::cmdline() const
{
    return d->cmdline();
}

const QUrl &LSPClientServer::root() const
{
    return d->root();
}

const QString &LSPClientServer::langId() const
{
    return d->langId();
}

LSPClientServer::State LSPClientServer::state() const
{
    return d->state();
}

const LSPServerCapabilities &LSPClientServer::capabilities() const
{
    return d->capabilities();
}

bool LSPClientServer::start(LSPClientPlugin *plugin)
{
    return d->start(plugin);
}

void LSPClientServer::stop(int to_t, int to_k)
{
    return d->stop(to_t, to_k);
}

int LSPClientServer::cancel(int reqid)
{
    return d->cancel(reqid);
}

LSPClientServer::RequestHandle
LSPClientServer::documentSymbols(const QUrl &document, const QObject *context, const DocumentSymbolsReplyHandler &h, const ErrorReplyHandler &eh)
{
    return d->documentSymbols(document, make_handler(h, context, parseDocumentSymbols), make_handler(eh, context, parseResponseError));
}

LSPClientServer::RequestHandle
LSPClientServer::documentDefinition(const QUrl &document, const LSPPosition &pos, const QObject *context, const DocumentDefinitionReplyHandler &h)
{
    return d->documentDefinition(document, pos, make_handler(h, context, parseDocumentLocation));
}

LSPClientServer::RequestHandle
LSPClientServer::documentImplementation(const QUrl &document, const LSPPosition &pos, const QObject *context, const DocumentDefinitionReplyHandler &h)
{
    return d->documentImplementation(document, pos, make_handler(h, context, parseDocumentLocation));
}

LSPClientServer::RequestHandle
LSPClientServer::documentDeclaration(const QUrl &document, const LSPPosition &pos, const QObject *context, const DocumentDefinitionReplyHandler &h)
{
    return d->documentDeclaration(document, pos, make_handler(h, context, parseDocumentLocation));
}

LSPClientServer::RequestHandle
LSPClientServer::documentHover(const QUrl &document, const LSPPosition &pos, const QObject *context, const DocumentHoverReplyHandler &h)
{
    return d->documentHover(document, pos, make_handler(h, context, parseHover));
}

LSPClientServer::RequestHandle
LSPClientServer::documentHighlight(const QUrl &document, const LSPPosition &pos, const QObject *context, const DocumentHighlightReplyHandler &h)
{
    return d->documentHighlight(document, pos, make_handler(h, context, parseDocumentHighlightList));
}

LSPClientServer::RequestHandle
LSPClientServer::documentReferences(const QUrl &document, const LSPPosition &pos, bool decl, const QObject *context, const DocumentDefinitionReplyHandler &h)
{
    return d->documentReferences(document, pos, decl, make_handler(h, context, parseDocumentLocation));
}

LSPClientServer::RequestHandle
LSPClientServer::documentCompletion(const QUrl &document, const LSPPosition &pos, const QObject *context, const DocumentCompletionReplyHandler &h)
{
    return d->documentCompletion(document, pos, make_handler(h, context, parseDocumentCompletion));
}

LSPClientServer::RequestHandle
LSPClientServer::signatureHelp(const QUrl &document, const LSPPosition &pos, const QObject *context, const SignatureHelpReplyHandler &h)
{
    return d->signatureHelp(document, pos, make_handler(h, context, parseSignatureHelp));
}

LSPClientServer::RequestHandle LSPClientServer::clangdSwitchSourceHeader(const QUrl &document, const QObject *context, const SwitchSourceHeaderHandler &h)
{
    return d->clangdSwitchSourceHeader(document, make_handler(h, context, parseClangdSwitchSourceHeader));
}

LSPClientServer::RequestHandle
LSPClientServer::documentFormatting(const QUrl &document, const LSPFormattingOptions &options, const QObject *context, const FormattingReplyHandler &h)
{
    return d->documentFormatting(document, options, make_handler(h, context, parseTextEdit));
}

LSPClientServer::RequestHandle LSPClientServer::documentRangeFormatting(const QUrl &document,
                                                                        const LSPRange &range,
                                                                        const LSPFormattingOptions &options,
                                                                        const QObject *context,
                                                                        const FormattingReplyHandler &h)
{
    return d->documentRangeFormatting(document, range, options, make_handler(h, context, parseTextEdit));
}

LSPClientServer::RequestHandle LSPClientServer::documentOnTypeFormatting(const QUrl &document,
                                                                         const LSPPosition &pos,
                                                                         const QChar lastChar,
                                                                         const LSPFormattingOptions &options,
                                                                         const QObject *context,
                                                                         const FormattingReplyHandler &h)
{
    return d->documentOnTypeFormatting(document, pos, lastChar, options, make_handler(h, context, parseTextEdit));
}

LSPClientServer::RequestHandle LSPClientServer::documentRename(const QUrl &document,
                                                               const LSPPosition &pos,
                                                               const QString &newName,
                                                               const QObject *context,
                                                               const WorkspaceEditReplyHandler &h)
{
    return d->documentRename(document, pos, newName, make_handler(h, context, parseWorkSpaceEdit));
}

LSPClientServer::RequestHandle LSPClientServer::documentCodeAction(const QUrl &document,
                                                                   const LSPRange &range,
                                                                   const QList<QString> &kinds,
                                                                   QList<LSPDiagnostic> diagnostics,
                                                                   const QObject *context,
                                                                   const CodeActionReplyHandler &h)
{
    return d->documentCodeAction(document, range, kinds, std::move(diagnostics), make_handler(h, context, parseCodeAction));
}

void LSPClientServer::executeCommand(const QString &command, const QJsonValue &args)
{
    return d->executeCommand(command, args);
}

void LSPClientServer::didOpen(const QUrl &document, int version, const QString &langId, const QString &text)
{
    return d->didOpen(document, version, langId, text);
}

void LSPClientServer::didChange(const QUrl &document, int version, const QString &text, const QList<LSPTextDocumentContentChangeEvent> &changes)
{
    return d->didChange(document, version, text, changes);
}

void LSPClientServer::didSave(const QUrl &document, const QString &text)
{
    return d->didSave(document, text);
}

void LSPClientServer::didClose(const QUrl &document)
{
    return d->didClose(document);
}

void LSPClientServer::didChangeConfiguration(const QJsonValue &settings)
{
    return d->didChangeConfiguration(settings);
}
