namespace ts.server {
    export interface SessionClientHost extends LanguageServiceHost {
        writeMessage(message: string): void;
    }

    interface RenameEntry {
        readonly renameInfo: RenameInfo;
        readonly inputs: {
            readonly fileName: string;
            readonly position: number;
            readonly findInStrings: boolean;
            readonly findInComments: boolean;
        };
        readonly locations: RenameLocation[];
    }

    /* @internal */
    export function extractMessage(message: string): string {
        // Read the content length
        const contentLengthPrefix = "Content-Length: ";
        const lines = message.split(/\r?\n/);
        Debug.assert(lines.length >= 2, "Malformed response: Expected 3 lines in the response.");

        const contentLengthText = lines[0];
        Debug.assert(contentLengthText.indexOf(contentLengthPrefix) === 0, "Malformed response: Response text did not contain content-length header.");
        const contentLength = parseInt(contentLengthText.substring(contentLengthPrefix.length));

        // Read the body
        const responseBody = lines[2];

        // Verify content length
        Debug.assert(responseBody.length + 1 === contentLength, "Malformed response: Content length did not match the response's body length.");
        return responseBody;
    }

    export class SessionClient implements LanguageService {
        private sequence = 0;
        private lineMaps = new Map<string, number[]>();
        private messages = createQueue<string>();
        private lastRenameEntry: RenameEntry | undefined;
        private preferences: UserPreferences | undefined;

        constructor(private host: SessionClientHost) {
        }

        public onMessage(message: string): void {
            this.messages.enqueue(message);
        }

        private writeMessage(message: string): void {
            this.host.writeMessage(message);
        }

        private getLineMap(fileName: string): number[] {
            let lineMap = this.lineMaps.get(fileName);
            if (!lineMap) {
                lineMap = computeLineStarts(getSnapshotText(this.host.getScriptSnapshot(fileName)!));
                this.lineMaps.set(fileName, lineMap);
            }
            return lineMap;
        }

        private lineOffsetToPosition(fileName: string, lineOffset: protocol.Location, lineMap?: number[]): number {
            lineMap = lineMap || this.getLineMap(fileName);
            return computePositionOfLineAndCharacter(lineMap, lineOffset.line - 1, lineOffset.offset - 1);
        }

        private positionToOneBasedLineOffset(fileName: string, position: number): protocol.Location {
            const lineOffset = computeLineAndCharacterOfPosition(this.getLineMap(fileName), position);
            return {
                line: lineOffset.line + 1,
                offset: lineOffset.character + 1
            };
        }

        private convertCodeEditsToTextChange(fileName: string, codeEdit: protocol.CodeEdit): TextChange {
            return { span: this.decodeSpan(codeEdit, fileName), newText: codeEdit.newText };
        }

        private processRequest<T extends protocol.Request>(command: string, args: T["arguments"]): T {
            const request: protocol.Request = {
                seq: this.sequence,
                type: "request",
                arguments: args,
                command
            };
            this.sequence++;

            this.writeMessage(JSON.stringify(request));

            return request as T;
        }

        private processResponse<T extends protocol.Response>(request: protocol.Request, expectEmptyBody = false): T {
            let foundResponseMessage = false;
            let response!: T;
            while (!foundResponseMessage) {
                const lastMessage = this.messages.dequeue()!;
                Debug.assert(!!lastMessage, "Did not receive any responses.");
                const responseBody = extractMessage(lastMessage);
                try {
                    response = JSON.parse(responseBody);
                    // the server may emit events before emitting the response. We
                    // want to ignore these events for testing purpose.
                    if (response.type === "response") {
                        foundResponseMessage = true;
                    }
                }
                catch (e) {
                    throw new Error("Malformed response: Failed to parse server response: " + lastMessage + ". \r\n  Error details: " + e.message);
                }
            }

            // unmarshal errors
            if (!response.success) {
                throw new Error("Error " + response.message);
            }

            Debug.assert(response.request_seq === request.seq, "Malformed response: response sequence number did not match request sequence number.");
            Debug.assert(expectEmptyBody || !!response.body, "Malformed response: Unexpected empty response body.");
            Debug.assert(!expectEmptyBody || !response.body, "Malformed response: Unexpected non-empty response body.");

            return response;
        }

        /*@internal*/
        configure(preferences: UserPreferences) {
            this.preferences = preferences;
            const args: protocol.ConfigureRequestArguments = { preferences };
            const request = this.processRequest(CommandNames.Configure, args);
            this.processResponse(request, /*expectEmptyBody*/ true);
        }

        /*@internal*/
        setFormattingOptions(formatOptions: FormatCodeSettings) {
            const args: protocol.ConfigureRequestArguments = { formatOptions };
            const request = this.processRequest(CommandNames.Configure, args);
            this.processResponse(request, /*expectEmptyBody*/ true);
        }

        /*@internal*/
        setCompilerOptionsForInferredProjects(options: protocol.CompilerOptions) {
            const args: protocol.SetCompilerOptionsForInferredProjectsArgs = { options };
            const request = this.processRequest(CommandNames.CompilerOptionsForInferredProjects, args);
            this.processResponse(request, /*expectEmptyBody*/ false);
        }

        openFile(file: string, fileContent?: string, scriptKindName?: "TS" | "JS" | "TSX" | "JSX"): void {
            const args: protocol.OpenRequestArgs = { file, fileContent, scriptKindName };
            this.processRequest(CommandNames.Open, args);
        }

        closeFile(file: string): void {
            const args: protocol.FileRequestArgs = { file };
            this.processRequest(CommandNames.Close, args);
        }

        createChangeFileRequestArgs(fileName: string, start: number, end: number, insertString: string): protocol.ChangeRequestArgs {
            return { ...this.createFileLocationRequestArgsWithEndLineAndOffset(fileName, start, end), insertString };
        }

        changeFile(fileName: string, args: protocol.ChangeRequestArgs): void {
            // clear the line map after an edit
            this.lineMaps.set(fileName, undefined!); // TODO: GH#18217
            this.processRequest(CommandNames.Change, args);
        }

        toLineColumnOffset(fileName: string, position: number) {
            const { line, offset } = this.positionToOneBasedLineOffset(fileName, position);
            return { line, character: offset };
        }

        getQuickInfoAtPosition(fileName: string, position: number): QuickInfo {
            const args = this.createFileLocationRequestArgs(fileName, position);

            const request = this.processRequest<protocol.QuickInfoRequest>(CommandNames.Quickinfo, args);
            const response = this.processResponse<protocol.QuickInfoResponse>(request);
            const body = response.body!; // TODO: GH#18217

            return {
                kind: body.kind,
                kindModifiers: body.kindModifiers,
                textSpan: this.decodeSpan(body, fileName),
                displayParts: [{ kind: "text", text: body.displayString }],
                documentation: typeof body.documentation === "string" ? [{ kind: "text", text: body.documentation }] : body.documentation,
                tags: this.decodeLinkDisplayParts(body.tags)
            };
        }

        getProjectInfo(file: string, needFileNameList: boolean): protocol.ProjectInfo {
            const args: protocol.ProjectInfoRequestArgs = { file, needFileNameList };

            const request = this.processRequest<protocol.ProjectInfoRequest>(CommandNames.ProjectInfo, args);
            const response = this.processResponse<protocol.ProjectInfoResponse>(request);

            return {
                configFileName: response.body!.configFileName, // TODO: GH#18217
                fileNames: response.body!.fileNames
            };
        }

        getCompletionsAtPosition(fileName: string, position: number, _preferences: UserPreferences | undefined): CompletionInfo {
            // Not passing along 'preferences' because server should already have those from the 'configure' command
            const args: protocol.CompletionsRequestArgs = this.createFileLocationRequestArgs(fileName, position);

            const request = this.processRequest<protocol.CompletionsRequest>(CommandNames.CompletionInfo, args);
            const response = this.processResponse<protocol.CompletionInfoResponse>(request);

            return {
                isGlobalCompletion: response.body!.isGlobalCompletion,
                isMemberCompletion: response.body!.isMemberCompletion,
                isNewIdentifierLocation: response.body!.isNewIdentifierLocation,
                entries: response.body!.entries.map<CompletionEntry>(entry => { // TODO: GH#18217
                    if (entry.replacementSpan !== undefined) {
                        const res: CompletionEntry = { ...entry, data: entry.data as any, replacementSpan: this.decodeSpan(entry.replacementSpan, fileName) };
                        return res;
                    }

                    return entry as { name: string, kind: ScriptElementKind, kindModifiers: string, sortText: string }; // TODO: GH#18217
                })
            };
        }

        getCompletionEntryDetails(fileName: string, position: number, entryName: string, _options: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined, _preferences: UserPreferences | undefined, data: unknown): CompletionEntryDetails {
            const args: protocol.CompletionDetailsRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), entryNames: [{ name: entryName, source, data }] };

            const request = this.processRequest<protocol.CompletionDetailsRequest>(CommandNames.CompletionDetailsFull, args);
            const response = this.processResponse<protocol.Response>(request);
            Debug.assert(response.body.length === 1, "Unexpected length of completion details response body.");
            return response.body[0];
        }

        getCompletionEntrySymbol(_fileName: string, _position: number, _entryName: string): Symbol {
            return notImplemented();
        }

        getNavigateToItems(searchValue: string): NavigateToItem[] {
            const args: protocol.NavtoRequestArgs = {
                searchValue,
                file: this.host.getScriptFileNames()[0]
            };

            const request = this.processRequest<protocol.NavtoRequest>(CommandNames.Navto, args);
            const response = this.processResponse<protocol.NavtoResponse>(request);

            return response.body!.map(entry => ({ // TODO: GH#18217
                name: entry.name,
                containerName: entry.containerName || "",
                containerKind: entry.containerKind || ScriptElementKind.unknown,
                kind: entry.kind,
                kindModifiers: entry.kindModifiers || "",
                matchKind: entry.matchKind as keyof typeof PatternMatchKind,
                isCaseSensitive: entry.isCaseSensitive,
                fileName: entry.file,
                textSpan: this.decodeSpan(entry),
            }));
        }

        getFormattingEditsForRange(file: string, start: number, end: number, _options: FormatCodeOptions): TextChange[] {
            const args: protocol.FormatRequestArgs = this.createFileLocationRequestArgsWithEndLineAndOffset(file, start, end);


            // TODO: handle FormatCodeOptions
            const request = this.processRequest<protocol.FormatRequest>(CommandNames.Format, args);
            const response = this.processResponse<protocol.FormatResponse>(request);

            return response.body!.map(entry => this.convertCodeEditsToTextChange(file, entry)); // TODO: GH#18217
        }

        getFormattingEditsForDocument(fileName: string, options: FormatCodeOptions): TextChange[] {
            return this.getFormattingEditsForRange(fileName, 0, this.host.getScriptSnapshot(fileName)!.getLength(), options);
        }

        getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, _options: FormatCodeOptions): TextChange[] {
            const args: protocol.FormatOnKeyRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), key };

            // TODO: handle FormatCodeOptions
            const request = this.processRequest<protocol.FormatOnKeyRequest>(CommandNames.Formatonkey, args);
            const response = this.processResponse<protocol.FormatResponse>(request);

            return response.body!.map(entry => this.convertCodeEditsToTextChange(fileName, entry)); // TODO: GH#18217
        }

        getDefinitionAtPosition(fileName: string, position: number): DefinitionInfo[] {
            const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position);

            const request = this.processRequest<protocol.DefinitionRequest>(CommandNames.Definition, args);
            const response = this.processResponse<protocol.DefinitionResponse>(request);

            return response.body!.map(entry => ({ // TODO: GH#18217
                containerKind: ScriptElementKind.unknown,
                containerName: "",
                fileName: entry.file,
                textSpan: this.decodeSpan(entry),
                kind: ScriptElementKind.unknown,
                name: ""
            }));
        }

        getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan {
            const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position);

            const request = this.processRequest<protocol.DefinitionAndBoundSpanRequest>(CommandNames.DefinitionAndBoundSpan, args);
            const response = this.processResponse<protocol.DefinitionInfoAndBoundSpanResponse>(request);
            const body = Debug.checkDefined(response.body); // TODO: GH#18217

            return {
                definitions: body.definitions.map(entry => ({
                    containerKind: ScriptElementKind.unknown,
                    containerName: "",
                    fileName: entry.file,
                    textSpan: this.decodeSpan(entry),
                    kind: ScriptElementKind.unknown,
                    name: "",
                    unverified: entry.unverified,
                })),
                textSpan: this.decodeSpan(body.textSpan, request.arguments.file)
            };
        }

        getTypeDefinitionAtPosition(fileName: string, position: number): DefinitionInfo[] {
            const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position);

            const request = this.processRequest<protocol.TypeDefinitionRequest>(CommandNames.TypeDefinition, args);
            const response = this.processResponse<protocol.TypeDefinitionResponse>(request);

            return response.body!.map(entry => ({ // TODO: GH#18217
                containerKind: ScriptElementKind.unknown,
                containerName: "",
                fileName: entry.file,
                textSpan: this.decodeSpan(entry),
                kind: ScriptElementKind.unknown,
                name: ""
            }));
        }

        getSourceDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfo[] {
            const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position);
            const request = this.processRequest<protocol.FindSourceDefinitionRequest>(CommandNames.FindSourceDefinition, args);
            const response = this.processResponse<protocol.DefinitionResponse>(request);
            const body = Debug.checkDefined(response.body); // TODO: GH#18217

            return body.map(entry => ({
                containerKind: ScriptElementKind.unknown,
                containerName: "",
                fileName: entry.file,
                textSpan: this.decodeSpan(entry),
                kind: ScriptElementKind.unknown,
                name: "",
                unverified: entry.unverified,
            }));
        }

        getImplementationAtPosition(fileName: string, position: number): ImplementationLocation[] {
            const args = this.createFileLocationRequestArgs(fileName, position);

            const request = this.processRequest<protocol.ImplementationRequest>(CommandNames.Implementation, args);
            const response = this.processResponse<protocol.ImplementationResponse>(request);

            return response.body!.map(entry => ({ // TODO: GH#18217
                fileName: entry.file,
                textSpan: this.decodeSpan(entry),
                kind: ScriptElementKind.unknown,
                displayParts: []
            }));
        }

        findReferences(fileName: string, position: number): ReferencedSymbol[] {
            const args = this.createFileLocationRequestArgs(fileName, position);
            const request = this.processRequest<protocol.ReferencesRequest>(CommandNames.ReferencesFull, args);
            const response = this.processResponse(request);
            return response.body;
        }

        getReferencesAtPosition(fileName: string, position: number): ReferenceEntry[] {
            const args = this.createFileLocationRequestArgs(fileName, position);

            const request = this.processRequest<protocol.ReferencesRequest>(CommandNames.References, args);
            const response = this.processResponse<protocol.ReferencesResponse>(request);

            return response.body!.refs.map(entry => ({ // TODO: GH#18217
                fileName: entry.file,
                textSpan: this.decodeSpan(entry),
                isWriteAccess: entry.isWriteAccess,
                isDefinition: entry.isDefinition,
            }));
        }

        getFileReferences(fileName: string): ReferenceEntry[] {
            const request = this.processRequest<protocol.FileReferencesRequest>(CommandNames.FileReferences, { file: fileName });
            const response = this.processResponse<protocol.FileReferencesResponse>(request);

            return response.body!.refs.map(entry => ({ // TODO: GH#18217
                fileName: entry.file,
                textSpan: this.decodeSpan(entry),
                isWriteAccess: entry.isWriteAccess,
                isDefinition: entry.isDefinition,
            }));
        }

        getEmitOutput(file: string): EmitOutput {
            const request = this.processRequest<protocol.EmitOutputRequest>(protocol.CommandTypes.EmitOutput, { file });
            const response = this.processResponse<protocol.EmitOutputResponse>(request);
            return response.body as EmitOutput;
        }

        getSyntacticDiagnostics(file: string): DiagnosticWithLocation[] {
            return this.getDiagnostics(file, CommandNames.SyntacticDiagnosticsSync);
        }
        getSemanticDiagnostics(file: string): Diagnostic[] {
            return this.getDiagnostics(file, CommandNames.SemanticDiagnosticsSync);
        }
        getSuggestionDiagnostics(file: string): DiagnosticWithLocation[] {
            return this.getDiagnostics(file, CommandNames.SuggestionDiagnosticsSync);
        }

        private getDiagnostics(file: string, command: CommandNames): DiagnosticWithLocation[] {
            const request = this.processRequest<protocol.SyntacticDiagnosticsSyncRequest | protocol.SemanticDiagnosticsSyncRequest | protocol.SuggestionDiagnosticsSyncRequest>(command, { file, includeLinePosition: true });
            const response = this.processResponse<protocol.SyntacticDiagnosticsSyncResponse | protocol.SemanticDiagnosticsSyncResponse | protocol.SuggestionDiagnosticsSyncResponse>(request);
            const sourceText = getSnapshotText(this.host.getScriptSnapshot(file)!);
            const fakeSourceFile = { fileName: file, text: sourceText } as SourceFile; // Warning! This is a huge lie!

            return (response.body as protocol.DiagnosticWithLinePosition[]).map((entry): DiagnosticWithLocation => {
                const category = firstDefined(Object.keys(DiagnosticCategory), id =>
                    isString(id) && entry.category === id.toLowerCase() ? (DiagnosticCategory as any)[id] : undefined);
                return {
                    file: fakeSourceFile,
                    start: entry.start,
                    length: entry.length,
                    messageText: entry.message,
                    category: Debug.checkDefined(category, "convertDiagnostic: category should not be undefined"),
                    code: entry.code,
                    reportsUnnecessary: entry.reportsUnnecessary,
                    reportsDeprecated: entry.reportsDeprecated,
                };
            });
        }

        getCompilerOptionsDiagnostics(): Diagnostic[] {
            return notImplemented();
        }

        getRenameInfo(fileName: string, position: number, _preferences: UserPreferences, findInStrings?: boolean, findInComments?: boolean): RenameInfo {
            // Not passing along 'options' because server should already have those from the 'configure' command
            const args: protocol.RenameRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), findInStrings, findInComments };

            const request = this.processRequest<protocol.RenameRequest>(CommandNames.Rename, args);
            const response = this.processResponse<protocol.RenameResponse>(request);
            const body = response.body!; // TODO: GH#18217
            const locations: RenameLocation[] = [];
            for (const entry of body.locs) {
                const fileName = entry.file;
                for (const { start, end, contextStart, contextEnd, ...prefixSuffixText } of entry.locs) {
                    locations.push({
                        textSpan: this.decodeSpan({ start, end }, fileName),
                        fileName,
                        ...(contextStart !== undefined ?
                            { contextSpan: this.decodeSpan({ start: contextStart, end: contextEnd! }, fileName) } :
                            undefined),
                        ...prefixSuffixText
                    });
                }
            }

            const renameInfo = body.info.canRename
                ? identity<RenameInfoSuccess>({
                    canRename: body.info.canRename,
                    fileToRename: body.info.fileToRename,
                    displayName: body.info.displayName,
                    fullDisplayName: body.info.fullDisplayName,
                    kind: body.info.kind,
                    kindModifiers: body.info.kindModifiers,
                    triggerSpan: createTextSpanFromBounds(position, position),
                })
                : identity<RenameInfoFailure>({ canRename: false, localizedErrorMessage: body.info.localizedErrorMessage });
            this.lastRenameEntry = {
                renameInfo,
                inputs: {
                    fileName,
                    position,
                    findInStrings: !!findInStrings,
                    findInComments: !!findInComments,
                },
                locations,
            };
            return renameInfo;
        }

        getSmartSelectionRange() {
            return notImplemented();
        }

        findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): RenameLocation[] {
            if (!this.lastRenameEntry ||
                this.lastRenameEntry.inputs.fileName !== fileName ||
                this.lastRenameEntry.inputs.position !== position ||
                this.lastRenameEntry.inputs.findInStrings !== findInStrings ||
                this.lastRenameEntry.inputs.findInComments !== findInComments) {
                if (providePrefixAndSuffixTextForRename !== undefined) {
                    // User preferences have to be set through the `Configure` command
                    this.configure({ providePrefixAndSuffixTextForRename });
                    // Options argument is not used, so don't pass in options
                    this.getRenameInfo(fileName, position, /*preferences*/{}, findInStrings, findInComments);
                    // Restore previous user preferences
                    if (this.preferences) {
                        this.configure(this.preferences);
                    }
                }
                else {
                    this.getRenameInfo(fileName, position, /*preferences*/{}, findInStrings, findInComments);
                }
            }

            return this.lastRenameEntry!.locations;
        }

        private decodeNavigationBarItems(items: protocol.NavigationBarItem[] | undefined, fileName: string, lineMap: number[]): NavigationBarItem[] {
            if (!items) {
                return [];
            }

            return items.map(item => ({
                text: item.text,
                kind: item.kind,
                kindModifiers: item.kindModifiers || "",
                spans: item.spans.map(span => this.decodeSpan(span, fileName, lineMap)),
                childItems: this.decodeNavigationBarItems(item.childItems, fileName, lineMap),
                indent: item.indent,
                bolded: false,
                grayed: false
            }));
        }

        getNavigationBarItems(file: string): NavigationBarItem[] {
            const request = this.processRequest<protocol.NavBarRequest>(CommandNames.NavBar, { file });
            const response = this.processResponse<protocol.NavBarResponse>(request);

            const lineMap = this.getLineMap(file);
            return this.decodeNavigationBarItems(response.body, file, lineMap);
        }

        private decodeNavigationTree(tree: protocol.NavigationTree, fileName: string, lineMap: number[]): NavigationTree {
            return {
                text: tree.text,
                kind: tree.kind,
                kindModifiers: tree.kindModifiers,
                spans: tree.spans.map(span => this.decodeSpan(span, fileName, lineMap)),
                nameSpan: tree.nameSpan && this.decodeSpan(tree.nameSpan, fileName, lineMap),
                childItems: map(tree.childItems, item => this.decodeNavigationTree(item, fileName, lineMap))
            };
        }

        getNavigationTree(file: string): NavigationTree {
            const request = this.processRequest<protocol.NavTreeRequest>(CommandNames.NavTree, { file });
            const response = this.processResponse<protocol.NavTreeResponse>(request);

            const lineMap = this.getLineMap(file);
            return this.decodeNavigationTree(response.body!, file, lineMap); // TODO: GH#18217
        }

        private decodeSpan(span: protocol.TextSpan & { file: string }): TextSpan;
        private decodeSpan(span: protocol.TextSpan, fileName: string, lineMap?: number[]): TextSpan;
        private decodeSpan(span: protocol.TextSpan & { file: string }, fileName?: string, lineMap?: number[]): TextSpan {
            if (span.start.line === 1 && span.start.offset === 1 && span.end.line === 1 && span.end.offset === 1) {
                return { start: 0, length: 0 };
            }
            fileName = fileName || span.file;
            lineMap = lineMap || this.getLineMap(fileName);
            return createTextSpanFromBounds(
                this.lineOffsetToPosition(fileName, span.start, lineMap),
                this.lineOffsetToPosition(fileName, span.end, lineMap));
        }

        private decodeLinkDisplayParts(tags: (protocol.JSDocTagInfo | JSDocTagInfo)[]): JSDocTagInfo[] {
            return tags.map(tag => typeof tag.text === "string" ? {
                ...tag,
                text: [textPart(tag.text)]
            } : (tag as JSDocTagInfo));
        }

        getNameOrDottedNameSpan(_fileName: string, _startPos: number, _endPos: number): TextSpan {
            return notImplemented();
        }

        getBreakpointStatementAtPosition(_fileName: string, _position: number): TextSpan {
            return notImplemented();
        }

        getSignatureHelpItems(fileName: string, position: number): SignatureHelpItems | undefined {
            const args: protocol.SignatureHelpRequestArgs = this.createFileLocationRequestArgs(fileName, position);

            const request = this.processRequest<protocol.SignatureHelpRequest>(CommandNames.SignatureHelp, args);
            const response = this.processResponse<protocol.SignatureHelpResponse>(request);

            if (!response.body) {
                return undefined;
            }

            const { items: encodedItems, applicableSpan: encodedApplicableSpan, selectedItemIndex, argumentIndex, argumentCount } = response.body;

            const applicableSpan = encodedApplicableSpan as unknown as TextSpan;
            const items = (encodedItems as (SignatureHelpItem | protocol.SignatureHelpItem)[]).map(item => ({ ...item, tags: this.decodeLinkDisplayParts(item.tags) }));

            return { items, applicableSpan, selectedItemIndex, argumentIndex, argumentCount };
        }

        getOccurrencesAtPosition(fileName: string, position: number): ReferenceEntry[] {
            const args = this.createFileLocationRequestArgs(fileName, position);

            const request = this.processRequest<protocol.OccurrencesRequest>(CommandNames.Occurrences, args);
            const response = this.processResponse<protocol.OccurrencesResponse>(request);

            return response.body!.map(entry => ({ // TODO: GH#18217
                fileName: entry.file,
                textSpan: this.decodeSpan(entry),
                isWriteAccess: entry.isWriteAccess,
            }));
        }

        getDocumentHighlights(fileName: string, position: number, filesToSearch: string[]): DocumentHighlights[] {
            const args: protocol.DocumentHighlightsRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), filesToSearch };

            const request = this.processRequest<protocol.DocumentHighlightsRequest>(CommandNames.DocumentHighlights, args);
            const response = this.processResponse<protocol.DocumentHighlightsResponse>(request);

            return response.body!.map(item => ({ // TODO: GH#18217
                fileName: item.file,
                highlightSpans: item.highlightSpans.map(span => ({
                    textSpan: this.decodeSpan(span, item.file),
                    kind: span.kind
                })),
            }));
        }

        getOutliningSpans(file: string): OutliningSpan[] {
            const request = this.processRequest<protocol.OutliningSpansRequest>(CommandNames.GetOutliningSpans, { file });
            const response = this.processResponse<protocol.OutliningSpansResponse>(request);

            return response.body!.map<OutliningSpan>(item => ({
                textSpan: this.decodeSpan(item.textSpan, file),
                hintSpan: this.decodeSpan(item.hintSpan, file),
                bannerText: item.bannerText,
                autoCollapse: item.autoCollapse,
                kind: item.kind
            }));
        }

        getTodoComments(_fileName: string, _descriptors: TodoCommentDescriptor[]): TodoComment[] {
            return notImplemented();
        }

        getDocCommentTemplateAtPosition(_fileName: string, _position: number, _options?: DocCommentTemplateOptions): TextInsertion {
            return notImplemented();
        }

        isValidBraceCompletionAtPosition(_fileName: string, _position: number, _openingBrace: number): boolean {
            return notImplemented();
        }

        getJsxClosingTagAtPosition(_fileName: string, _position: number): never {
            return notImplemented();
        }

        getSpanOfEnclosingComment(_fileName: string, _position: number, _onlyMultiLine: boolean): TextSpan {
            return notImplemented();
        }

        getCodeFixesAtPosition(file: string, start: number, end: number, errorCodes: readonly number[]): readonly CodeFixAction[] {
            const args: protocol.CodeFixRequestArgs = { ...this.createFileRangeRequestArgs(file, start, end), errorCodes };

            const request = this.processRequest<protocol.CodeFixRequest>(CommandNames.GetCodeFixes, args);
            const response = this.processResponse<protocol.CodeFixResponse>(request);

            return response.body!.map<CodeFixAction>(({ fixName, description, changes, commands, fixId, fixAllDescription }) => // TODO: GH#18217
                ({ fixName, description, changes: this.convertChanges(changes, file), commands: commands as CodeActionCommand[], fixId, fixAllDescription }));
        }

        getCombinedCodeFix = notImplemented;

        applyCodeActionCommand = notImplemented;

        provideInlayHints(file: string, span: TextSpan): InlayHint[] {
            const { start, length } = span;
            const args: protocol.InlayHintsRequestArgs = { file, start, length };

            const request = this.processRequest<protocol.InlayHintsRequest>(CommandNames.ProvideInlayHints, args);
            const response = this.processResponse<protocol.InlayHintsResponse>(request);

            return response.body!.map(item => ({ // TODO: GH#18217
                ...item,
                kind: item.kind as InlayHintKind,
                position: this.lineOffsetToPosition(file, item.position),
            }));
        }

        private createFileLocationOrRangeRequestArgs(positionOrRange: number | TextRange, fileName: string): protocol.FileLocationOrRangeRequestArgs {
            return typeof positionOrRange === "number"
                ? this.createFileLocationRequestArgs(fileName, positionOrRange)
                : this.createFileRangeRequestArgs(fileName, positionOrRange.pos, positionOrRange.end);
        }

        private createFileLocationRequestArgs(file: string, position: number): protocol.FileLocationRequestArgs {
            const { line, offset } = this.positionToOneBasedLineOffset(file, position);
            return { file, line, offset };
        }

        private createFileRangeRequestArgs(file: string, start: number, end: number): protocol.FileRangeRequestArgs {
            const { line: startLine, offset: startOffset } = this.positionToOneBasedLineOffset(file, start);
            const { line: endLine, offset: endOffset } = this.positionToOneBasedLineOffset(file, end);
            return { file, startLine, startOffset, endLine, endOffset };
        }

        private createFileLocationRequestArgsWithEndLineAndOffset(file: string, start: number, end: number): protocol.FileLocationRequestArgs & { endLine: number, endOffset: number } {
            const { line, offset } = this.positionToOneBasedLineOffset(file, start);
            const { line: endLine, offset: endOffset } = this.positionToOneBasedLineOffset(file, end);
            return { file, line, offset, endLine, endOffset };
        }

        getApplicableRefactors(fileName: string, positionOrRange: number | TextRange): ApplicableRefactorInfo[] {
            const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName);

            const request = this.processRequest<protocol.GetApplicableRefactorsRequest>(CommandNames.GetApplicableRefactors, args);
            const response = this.processResponse<protocol.GetApplicableRefactorsResponse>(request);
            return response.body!; // TODO: GH#18217
        }

        getEditsForRefactor(
            fileName: string,
            _formatOptions: FormatCodeSettings,
            positionOrRange: number | TextRange,
            refactorName: string,
            actionName: string): RefactorEditInfo {

            const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName) as protocol.GetEditsForRefactorRequestArgs;
            args.refactor = refactorName;
            args.action = actionName;

            const request = this.processRequest<protocol.GetEditsForRefactorRequest>(CommandNames.GetEditsForRefactor, args);
            const response = this.processResponse<protocol.GetEditsForRefactorResponse>(request);

            if (!response.body) {
                return { edits: [], renameFilename: undefined, renameLocation: undefined };
            }

            const edits: FileTextChanges[] = this.convertCodeEditsToTextChanges(response.body.edits);

            const renameFilename: string | undefined = response.body.renameFilename;
            let renameLocation: number | undefined;
            if (renameFilename !== undefined) {
                renameLocation = this.lineOffsetToPosition(renameFilename, response.body.renameLocation!); // TODO: GH#18217
            }

            return {
                edits,
                renameFilename,
                renameLocation
            };
        }

        organizeImports(_args: OrganizeImportsArgs, _formatOptions: FormatCodeSettings): readonly FileTextChanges[] {
            return notImplemented();
        }

        getEditsForFileRename() {
            return notImplemented();
        }

        private convertCodeEditsToTextChanges(edits: protocol.FileCodeEdits[]): FileTextChanges[] {
            return edits.map(edit => {
                const fileName = edit.fileName;
                return {
                    fileName,
                    textChanges: edit.textChanges.map(t => this.convertTextChangeToCodeEdit(t, fileName))
                };
            });
        }

        private convertChanges(changes: protocol.FileCodeEdits[], fileName: string): FileTextChanges[] {
            return changes.map(change => ({
                fileName: change.fileName,
                textChanges: change.textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, fileName))
            }));
        }

        convertTextChangeToCodeEdit(change: protocol.CodeEdit, fileName: string): TextChange {
            return {
                span: this.decodeSpan(change, fileName),
                newText: change.newText ? change.newText : ""
            };
        }

        getBraceMatchingAtPosition(fileName: string, position: number): TextSpan[] {
            const args = this.createFileLocationRequestArgs(fileName, position);

            const request = this.processRequest<protocol.BraceRequest>(CommandNames.Brace, args);
            const response = this.processResponse<protocol.BraceResponse>(request);

            return response.body!.map(entry => this.decodeSpan(entry, fileName)); // TODO: GH#18217
        }

        configurePlugin(pluginName: string, configuration: any): void {
            const request = this.processRequest<protocol.ConfigurePluginRequest>("configurePlugin", { pluginName, configuration });
            this.processResponse<protocol.ConfigurePluginResponse>(request, /*expectEmptyBody*/ true);
        }

        getIndentationAtPosition(_fileName: string, _position: number, _options: EditorOptions): number {
            return notImplemented();
        }

        getSyntacticClassifications(_fileName: string, _span: TextSpan): ClassifiedSpan[] {
            return notImplemented();
        }

        getSemanticClassifications(_fileName: string, _span: TextSpan): ClassifiedSpan[] {
            return notImplemented();
        }

        getEncodedSyntacticClassifications(_fileName: string, _span: TextSpan): Classifications {
            return notImplemented();
        }

        getEncodedSemanticClassifications(file: string, span: TextSpan, format?: SemanticClassificationFormat): Classifications {
            const request = this.processRequest<protocol.EncodedSemanticClassificationsRequest>(protocol.CommandTypes.EncodedSemanticClassificationsFull, { file, start: span.start, length: span.length, format });
            const r = this.processResponse<protocol.EncodedSemanticClassificationsResponse>(request);
            return r.body!;
        }

        private convertCallHierarchyItem(item: protocol.CallHierarchyItem): CallHierarchyItem {
            return {
                file: item.file,
                name: item.name,
                kind: item.kind,
                kindModifiers: item.kindModifiers,
                containerName: item.containerName,
                span: this.decodeSpan(item.span, item.file),
                selectionSpan: this.decodeSpan(item.selectionSpan, item.file)
            };
        }

        prepareCallHierarchy(fileName: string, position: number): CallHierarchyItem | CallHierarchyItem[] | undefined {
            const args = this.createFileLocationRequestArgs(fileName, position);
            const request = this.processRequest<protocol.PrepareCallHierarchyRequest>(CommandNames.PrepareCallHierarchy, args);
            const response = this.processResponse<protocol.PrepareCallHierarchyResponse>(request);
            return response.body && mapOneOrMany(response.body, item => this.convertCallHierarchyItem(item));
        }

        private convertCallHierarchyIncomingCall(item: protocol.CallHierarchyIncomingCall): CallHierarchyIncomingCall {
            return {
                from: this.convertCallHierarchyItem(item.from),
                fromSpans: item.fromSpans.map(span => this.decodeSpan(span, item.from.file))
            };
        }

        provideCallHierarchyIncomingCalls(fileName: string, position: number) {
            const args = this.createFileLocationRequestArgs(fileName, position);
            const request = this.processRequest<protocol.ProvideCallHierarchyIncomingCallsRequest>(CommandNames.ProvideCallHierarchyIncomingCalls, args);
            const response = this.processResponse<protocol.ProvideCallHierarchyIncomingCallsResponse>(request);
            return response.body.map(item => this.convertCallHierarchyIncomingCall(item));
        }

        private convertCallHierarchyOutgoingCall(file: string, item: protocol.CallHierarchyOutgoingCall): CallHierarchyOutgoingCall {
            return {
                to: this.convertCallHierarchyItem(item.to),
                fromSpans: item.fromSpans.map(span => this.decodeSpan(span, file))
            };
        }

        provideCallHierarchyOutgoingCalls(fileName: string, position: number) {
            const args = this.createFileLocationRequestArgs(fileName, position);
            const request = this.processRequest<protocol.ProvideCallHierarchyOutgoingCallsRequest>(CommandNames.ProvideCallHierarchyOutgoingCalls, args);
            const response = this.processResponse<protocol.ProvideCallHierarchyOutgoingCallsResponse>(request);
            return response.body.map(item => this.convertCallHierarchyOutgoingCall(fileName, item));
        }

        getProgram(): Program {
            throw new Error("Program objects are not serializable through the server protocol.");
        }

        getCurrentProgram(): Program | undefined {
            throw new Error("Program objects are not serializable through the server protocol.");
        }

        getAutoImportProvider(): Program | undefined {
            throw new Error("Program objects are not serializable through the server protocol.");
        }

        updateIsDefinitionOfReferencedSymbols(_referencedSymbols: readonly ReferencedSymbol[], _knownSymbolSpans: Set<DocumentSpan>): boolean {
            return notImplemented();
        }

        getNonBoundSourceFile(_fileName: string): SourceFile {
            throw new Error("SourceFile objects are not serializable through the server protocol.");
        }

        getSourceFile(_fileName: string): SourceFile {
            throw new Error("SourceFile objects are not serializable through the server protocol.");
        }

        cleanupSemanticCache(): void {
            throw new Error("cleanupSemanticCache is not available through the server layer.");
        }

        getSourceMapper(): never {
            return notImplemented();
        }

        clearSourceMapperCache(): never {
            return notImplemented();
        }

        toggleLineComment(): TextChange[] {
            return notImplemented();
        }

        toggleMultilineComment(): TextChange[] {
            return notImplemented();
        }

        commentSelection(): TextChange[] {
            return notImplemented();
        }

        uncommentSelection(): TextChange[] {
            return notImplemented();
        }

        dispose(): void {
            throw new Error("dispose is not available through the server layer.");
        }
    }
}
