/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
import { Document, ServiceStat, Uri, window, workspace } from 'coc.nvim'
import fs from 'fs'
import os from 'os'
import path from 'path'
import { CancellationToken, CancellationTokenSource, Disposable, Emitter, Event } from 'vscode-languageserver-protocol'
import * as fileSchemes from '../utils/fileSchemes'
import { PluginManager } from '../utils/plugins'
import { CallbackMap } from './callbackMap'
import BufferSyncSupport from './features/bufferSyncSupport'
import { DiagnosticKind, DiagnosticsManager } from './features/diagnostics'
import FileConfigurationManager from './features/fileConfigurationManager'
import * as Proto from './protocol'
import { RequestItem, RequestQueue, RequestQueueingType } from './requestQueue'
import { ExecConfig, ITypeScriptServiceClient, ServerResponse } from './typescriptService'
import API from './utils/api'
import { TsServerLogLevel, TypeScriptServiceConfiguration } from './utils/configuration'
import Logger from './utils/logger'
import { fork, getTempDirectory, createTempDirectory, getTempFile, IForkOptions, makeRandomHexString } from './utils/process'
import Tracer from './utils/tracer'
import { inferredProjectConfig } from './utils/tsconfig'
import { TypeScriptVersion, TypeScriptVersionProvider } from './utils/versionProvider'
import VersionStatus from './utils/versionStatus'
import ForkedTsServerProcess, { ToCancelOnResourceChanged } from './tsServerProcess'

export interface TsDiagnostics {
  readonly kind: DiagnosticKind
  readonly resource: Uri
  readonly diagnostics: Proto.Diagnostic[]
}

export default class TypeScriptServiceClient implements ITypeScriptServiceClient {
  private token: number = 0
  public state = ServiceStat.Initial
  public readonly logger: Logger = new Logger()
  public readonly bufferSyncSupport: BufferSyncSupport
  public readonly diagnosticsManager: DiagnosticsManager

  private fileConfigurationManager: FileConfigurationManager
  private pathSeparator: string
  private tracer: Tracer
  private _configuration: TypeScriptServiceConfiguration
  private versionProvider: TypeScriptVersionProvider
  private tsServerLogFile: string | null = null
  private tsServerProcess: ForkedTsServerProcess | undefined
  private lastStart: number
  private numberRestarts: number
  private cancellationPipeName: string | null = null
  private _callbacks = new CallbackMap<Proto.Response>()
  private _requestQueue = new RequestQueue()
  private _pendingResponses = new Set<number>()
  private _onReady?: { promise: Promise<void>; resolve: () => void; reject: () => void }

  private versionStatus: VersionStatus
  private readonly _onTsServerStarted = new Emitter<API>()
  private readonly _onProjectLanguageServiceStateChanged = new Emitter<Proto.ProjectLanguageServiceStateEventBody>()
  private readonly _onDidBeginInstallTypings = new Emitter<Proto.BeginInstallTypesEventBody>()
  private readonly _onDidEndInstallTypings = new Emitter<Proto.EndInstallTypesEventBody>()
  private readonly _onTypesInstallerInitializationFailed = new Emitter<
    Proto.TypesInstallerInitializationFailedEventBody
  >()
  private _apiVersion: API
  private _tscPath: string
  private readonly disposables: Disposable[] = []
  private isRestarting = false

  constructor(
    public readonly pluginManager: PluginManager,
    public readonly modeIds: string[]
  ) {
    this.pathSeparator = path.sep
    this.lastStart = Date.now()
    this.numberRestarts = 0
    let resolve: () => void
    let reject: () => void
    const p = new Promise<void>((res, rej) => {
      resolve = res
      reject = rej
    })
    this._onReady = { promise: p, resolve: resolve!, reject: reject! }

    this.fileConfigurationManager = new FileConfigurationManager(this)
    this._configuration = TypeScriptServiceConfiguration.loadFromWorkspace()
    this.versionProvider = new TypeScriptVersionProvider(this._configuration)
    this._apiVersion = API.defaultVersion
    this.tracer = new Tracer(this.logger)
    this.versionStatus = new VersionStatus(this.normalizePath.bind(this), this.fileConfigurationManager.enableJavascript())
    pluginManager.onDidUpdateConfig(update => {
      this.configurePlugin(update.pluginId, update.config)
    }, null, this.disposables)

    pluginManager.onDidChangePlugins(() => {
      this.restartTsServer()
    }, null, this.disposables)

    this.bufferSyncSupport = new BufferSyncSupport(this, modeIds)
    this.onReady(() => {
      this.bufferSyncSupport.listen()
    })

    this.diagnosticsManager = new DiagnosticsManager()
    this.bufferSyncSupport.onDelete(resource => {
      this.cancelInflightRequestsForResource(resource)
      this.diagnosticsManager.delete(resource)
    }, null, this.disposables)
    this.bufferSyncSupport.onWillChange(resource => {
      this.cancelInflightRequestsForResource(resource)
    })
  }

  private _onDiagnosticsReceived = new Emitter<TsDiagnostics>()
  public get onDiagnosticsReceived(): Event<TsDiagnostics> {
    return this._onDiagnosticsReceived.event
  }

  private _onConfigDiagnosticsReceived = new Emitter<Proto.ConfigFileDiagnosticEvent>()
  public get onConfigDiagnosticsReceived(): Event<Proto.ConfigFileDiagnosticEvent> {
    return this._onConfigDiagnosticsReceived.event
  }

  private _onResendModelsRequested = new Emitter<void>()
  public get onResendModelsRequested(): Event<void> {
    return this._onResendModelsRequested.event
  }

  public get configuration(): TypeScriptServiceConfiguration {
    return this._configuration
  }

  public onReady(f: () => void): Promise<void> {
    return this._onReady!.promise.then(f)
  }

  public dispose(): void {
    this.tsServerProcess.kill()
    this.bufferSyncSupport.dispose()
    this.logger.dispose()
    this._onTsServerStarted.dispose()
    this._onResendModelsRequested.dispose()
    this.versionStatus.dispose()
  }

  private info(message: string, data?: any): void {
    this.logger.info(message, data)
  }

  private error(message: string, data?: any): void {
    this.logger.error(message, data)
  }

  public restartTsServer(): void {
    if (this.tsServerProcess) {
      this.state = ServiceStat.Stopping
      this.info('Killing TS Server')
      this.isRestarting = true
      this.tsServerProcess.kill()
    }
    this.startService(true)
  }

  public stop(): Promise<void> {
    return new Promise(resolve => {
      let { tsServerProcess } = this
      if (tsServerProcess && this.state == ServiceStat.Running) {
        this.info('Killing TS Server')
        tsServerProcess.onExit(() => {
          resolve()
        })
        tsServerProcess.kill()
      } else {
        resolve()
      }
    })
  }

  public get onTsServerStarted(): Event<API> {
    return this._onTsServerStarted.event
  }

  public get onProjectLanguageServiceStateChanged(): Event<
    Proto.ProjectLanguageServiceStateEventBody
  > {
    return this._onProjectLanguageServiceStateChanged.event
  }

  public get onDidBeginInstallTypings(): Event<Proto.BeginInstallTypesEventBody> {
    return this._onDidBeginInstallTypings.event
  }

  public get onDidEndInstallTypings(): Event<Proto.EndInstallTypesEventBody> {
    return this._onDidEndInstallTypings.event
  }

  public get onTypesInstallerInitializationFailed(): Event<Proto.TypesInstallerInitializationFailedEventBody> {
    return this._onTypesInstallerInitializationFailed.event
  }

  public get apiVersion(): API {
    return this._apiVersion
  }

  public get tscPath(): string {
    return this._tscPath
  }

  public ensureServiceStarted(): void {
    if (!this.tsServerProcess) {
      this.startService()
    }
  }

  private startService(resendModels = false): ForkedTsServerProcess | undefined {
    const { ignoreLocalTsserver } = this.configuration
    let currentVersion: TypeScriptVersion
    if (!ignoreLocalTsserver) currentVersion = this.versionProvider.getLocalVersion()
    if (!currentVersion || !fs.existsSync(currentVersion.tsServerPath)) {
      this.info('Local tsserver not found, using bundled tsserver with coc-tsserver.')
      currentVersion = this.versionProvider.getDefaultVersion()
    }
    if (!currentVersion || !currentVersion.isValid) {
      if (this.configuration.globalTsdk) {
        window.showErrorMessage(`Can not find typescript module, in 'tsserver.tsdk': ${this.configuration.globalTsdk}`)
      } else {
        window.showErrorMessage(`Can not find typescript module, run ':CocInstall coc-tsserver' to fix it!`)
      }
      return
    }
    this._apiVersion = currentVersion.version
    this._tscPath = currentVersion.tscPath
    this.versionStatus.onDidChangeTypeScriptVersion(currentVersion)
    const tsServerForkArgs = this.getTsServerArgs(currentVersion)
    const options = { execArgv: this.getExecArgv() }
    return this.startProcess(currentVersion, tsServerForkArgs, options, resendModels)
  }

  private getExecArgv(): string[] {
    const args: string[] = []
    const debugPort = getDebugPort()
    if (debugPort) {
      const isBreak = process.env[process.env.remoteName ? 'TSS_REMOTE_DEBUG_BRK' : 'TSS_DEBUG_BRK'] !== undefined
      const inspectFlag = isBreak ? '--inspect-brk' : '--inspect'
      args.push(`${inspectFlag}=${debugPort}`)
    }
    const maxTsServerMemory = this._configuration.maxTsServerMemory
    if (maxTsServerMemory) {
      args.push(`--max-old-space-size=${maxTsServerMemory}`)
    }
    return args
  }

  private startProcess(currentVersion: TypeScriptVersion, args: string[], options: IForkOptions, resendModels: boolean): ForkedTsServerProcess {
    const myToken = ++this.token
    this.state = ServiceStat.Starting
    try {
      let childProcess = fork(currentVersion.tsServerPath, args, options, this.logger)
      this.state = ServiceStat.Running
      this.info('Starting TSServer', JSON.stringify(currentVersion, null, 2))
      const handle = new ForkedTsServerProcess(childProcess)
      this.tsServerProcess = handle
      this.lastStart = Date.now()
      handle.onError((err: Error) => {
        if (this.token != myToken) return
        window.showErrorMessage(`TypeScript language server exited with error. Error message is: ${err.message}`)
        this.error('TSServer errored with error.', err)
        this.error(`TSServer log file: ${this.tsServerLogFile || ''}`)
        window.showMessage(`TSServer errored with error. ${err.message}`, 'error')
        this.serviceExited(false)
      })
      handle.onExit((code: any, signal: string) => {
        handle.dispose()
        if (this.token != myToken) return
        if (code == null) {
          this.info(`TSServer exited. Signal: ${signal}`)
        } else {
          this.error(`TSServer exited with code: ${code}. Signal: ${signal}`)
        }
        this.info(`TSServer log file: ${this.tsServerLogFile || ''}`)
        this.serviceExited(!this.isRestarting)
        this.isRestarting = false
      })
      handle.onData(msg => {
        this.dispatchMessage(msg)
      })
      this.serviceStarted(resendModels)
      this._onReady!.resolve()
      this._onTsServerStarted.fire(currentVersion.version)
      return handle
    } catch (err) {
      this.state = ServiceStat.StartFailed
      this.error('Starting TSServer failed with error.', err.stack)
      return undefined
    }
  }

  public async openTsServerLogFile(): Promise<boolean> {
    const isRoot = process.getuid && process.getuid() == 0
    let echoErr = (msg: string) => {
      window.showErrorMessage(msg)
    }
    if (isRoot) {
      echoErr('Log disabled for root user.')
      return false
    }
    if (!this.apiVersion.gte(API.v222)) {
      echoErr('TS Server logging requires TS 2.2.2+')
      return false
    }
    if (this._configuration.tsServerLogLevel === TsServerLogLevel.Off) {
      echoErr(`TS Server logging is off. Change 'tsserver.log' in 'coc-settings.json' to enable`)
      return false
    }
    if (!this.tsServerLogFile) {
      echoErr('TS Server has not started logging.')
      return false
    }
    try {
      await workspace.nvim.command(`edit ${this.tsServerLogFile}`)
      return true
    } catch {
      echoErr('Could not open TS Server log file')
      return false
    }
  }

  private serviceStarted(resendModels: boolean): void {
    this.bufferSyncSupport.reset()
    const watchOptions = this.apiVersion.gte(API.v380)
      ? this.configuration.watchOptions
      : undefined
    const configureOptions: Proto.ConfigureRequestArguments = {
      hostInfo: 'coc-nvim',
      preferences: {
        providePrefixAndSuffixTextForRename: true,
        allowRenameOfImportPath: true,
        includePackageJsonAutoImports: this._configuration.includePackageJsonAutoImports
      },
      watchOptions
    }
    this.executeWithoutWaitingForResponse('configure', configureOptions) // tslint:disable-line
    this.setCompilerOptionsForInferredProjects(this._configuration)
    if (resendModels) {
      this._onResendModelsRequested.fire(void 0)
      this.fileConfigurationManager.reset()
      this.diagnosticsManager.reInitialize()
      this.bufferSyncSupport.reinitialize()
    }
    // Reconfigure any plugins
    for (const [config, pluginName] of this.pluginManager.configurations()) {
      this.configurePlugin(config, pluginName)
    }
  }

  private setCompilerOptionsForInferredProjects(
    configuration: TypeScriptServiceConfiguration
  ): void {
    if (!this.apiVersion.gte(API.v206)) return
    const args: Proto.SetCompilerOptionsForInferredProjectsArgs = {
      options: this.getCompilerOptionsForInferredProjects(configuration)
    }
    this.executeWithoutWaitingForResponse('compilerOptionsForInferredProjects', args) // tslint:disable-line
  }

  private getCompilerOptionsForInferredProjects(
    configuration: TypeScriptServiceConfiguration
  ): Proto.ExternalProjectCompilerOptions {
    return {
      ...inferredProjectConfig(configuration),
      allowJs: true,
      allowSyntheticDefaultImports: true,
      allowNonTsExtensions: true
    }
  }

  private serviceExited(restart: boolean): void {
    this.state = ServiceStat.Stopped
    this.tsServerLogFile = null
    this._callbacks.destroy('Service died.')
    this._callbacks = new CallbackMap<Proto.Response>()
    this._requestQueue = new RequestQueue()
    this._pendingResponses = new Set<number>()
    if (restart) {
      const diff = Date.now() - this.lastStart
      this.numberRestarts++
      let startService = true
      if (this.numberRestarts > 5) {
        this.numberRestarts = 0
        if (diff < 10 * 1000 /* 10 seconds */) {
          this.lastStart = Date.now()
          startService = false
          window.showMessage('The TypeScript language service died 5 times right after it got started.', 'error') // tslint:disable-line
        } else if (diff < 60 * 1000 /* 1 Minutes */) {
          this.lastStart = Date.now()
          window.showMessage('The TypeScript language service died unexpectedly 5 times in the last 5 Minutes.', 'error') // tslint:disable-line
        }
      }
      if (startService) {
        this.startService(true)
      }
    }
  }

  public toPath(uri: string): string {
    return this.normalizePath(Uri.parse(uri))
  }

  public toOpenedFilePath(uri: string, options: { suppressAlertOnFailure?: boolean } = {}): string | undefined {
    if (!this.bufferSyncSupport.ensureHasBuffer(uri)) {
      if (!options.suppressAlertOnFailure) {
        this.error(`Unexpected resource ${uri}`)
      }
      return undefined
    }
    return this.toPath(uri)
  }

  public toResource(filepath: string): string {
    if (this._apiVersion.gte(API.v213)) {
      if (filepath.startsWith(this.inMemoryResourcePrefix + 'untitled:')) {
        let resource = Uri.parse(filepath)
        if (this.inMemoryResourcePrefix) {
          const dirName = path.dirname(resource.path)
          const fileName = path.basename(resource.path)
          if (fileName.startsWith(this.inMemoryResourcePrefix)) {
            resource = resource.with({ path: path.posix.join(dirName, fileName.slice(this.inMemoryResourcePrefix.length)) })
          }
        }
        return resource.toString()
      }
    }
    return Uri.file(filepath).toString()
  }

  public normalizePath(resource: Uri): string | undefined {
    if (fileSchemes.disabledSchemes.has(resource.scheme)) {
      return undefined
    }
    switch (resource.scheme) {
      case fileSchemes.file: {
        let result = resource.fsPath
        if (!result) return undefined
        result = path.normalize(result)
        // Both \ and / must be escaped in regular expressions
        return result.replace(new RegExp('\\' + this.pathSeparator, 'g'), '/')
      }
      default: {
        return this.inMemoryResourcePrefix + resource.toString(true)
      }
    }
  }

  public getDocument(resource: string): Document | undefined {
    if (resource.startsWith('untitled:')) {
      let bufnr = parseInt(resource.split(':', 2)[1], 10)
      return workspace.getDocument(bufnr)
    }
    return workspace.getDocument(resource)
  }

  private get inMemoryResourcePrefix(): string {
    return this._apiVersion.gte(API.v270) ? '^' : ''
  }

  public asUrl(filepath: string): Uri {
    if (this._apiVersion.gte(API.v213)) {
      if (filepath.startsWith(this.inMemoryResourcePrefix + 'untitled:')) {
        let resource = Uri.parse(filepath.slice(this.inMemoryResourcePrefix.length))
        if (this.inMemoryResourcePrefix) {
          const dirName = path.dirname(resource.path)
          const fileName = path.basename(resource.path)
          if (fileName.startsWith(this.inMemoryResourcePrefix)) {
            resource = resource.with({
              path: path.posix.join(
                dirName,
                fileName.slice(this.inMemoryResourcePrefix.length)
              )
            })
          }
        }
        return resource
      }
    }
    return Uri.file(filepath)
  }

  public execute(
    command: string, args: any,
    token: CancellationToken,
    config?: ExecConfig
  ): Promise<ServerResponse.Response<Proto.Response>> {
    let execution: Promise<ServerResponse.Response<Proto.Response>>

    if (config?.cancelOnResourceChange) {
      const source = new CancellationTokenSource()
      token.onCancellationRequested(() => source.cancel())
      const inFlight: ToCancelOnResourceChanged = {
        resource: config.cancelOnResourceChange,
        cancel: () => source.cancel(),
      }
      this.tsServerProcess?.toCancelOnResourceChange.add(inFlight)

      execution = this.executeImpl(command, args, {
        isAsync: false,
        token: source.token,
        expectsResult: true,
        ...config,
      }).finally(() => {
        this.tsServerProcess?.toCancelOnResourceChange.delete(inFlight)
        source.dispose()
      })
    } else {
      execution = this.executeImpl(command, args, {
        isAsync: false,
        token,
        expectsResult: true,
        ...config,
      })
    }

    if (config?.nonRecoverable) {
      execution.catch(err => this.fatalError(command, err))
    }
    return execution
  }

  private fatalError(command: string, error: any): void {
    this.error(`A non-recoverable error occured while executing tsserver command: ${command}`)
    if (this.state === ServiceStat.Running) {
      this.info('Killing TS Server by fatal error:', error)
      let { tsServerProcess } = this
      if (tsServerProcess) {
        this.tsServerProcess = undefined
        tsServerProcess.kill()
      }
    }
  }

  public executeAsync(
    command: string, args: Proto.GeterrRequestArgs,
    token: CancellationToken): Promise<ServerResponse.Response<Proto.Response>> {
    return this.executeImpl(command, args, {
      isAsync: true,
      token,
      expectsResult: true
    })
  }

  public executeWithoutWaitingForResponse(command: string, args: any): void {
    this.executeImpl(command, args, {
      isAsync: false,
      token: undefined,
      expectsResult: false
    })
  }

  private executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: CancellationToken, expectsResult: false, lowPriority?: boolean }): undefined
  private executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>>
  private executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise<ServerResponse.Response<Proto.Response>> | undefined {
    if (!this.tsServerProcess) {
      return Promise.resolve(undefined)
    }
    this.bufferSyncSupport.beforeCommand(command)

    const request = this._requestQueue.createRequest(command, args)
    const requestInfo: RequestItem = {
      request,
      expectsResponse: executeInfo.expectsResult,
      isAsync: executeInfo.isAsync,
      queueingType: getQueueingType(command, executeInfo.lowPriority)
    }
    let result: Promise<ServerResponse.Response<Proto.Response>> | undefined
    if (executeInfo.expectsResult) {
      result = new Promise<ServerResponse.Response<Proto.Response>>((resolve, reject) => {
        this._callbacks.add(request.seq, { onSuccess: resolve, onError: reject, startTime: Date.now(), isAsync: executeInfo.isAsync }, executeInfo.isAsync)

        if (executeInfo.token) {
          executeInfo.token.onCancellationRequested(() => {
            this.tryCancelRequest(request.seq, command)
          })
        }
      }).catch((err: Error) => {
        throw err
      })
    }

    this._requestQueue.enqueue(requestInfo)
    this.sendNextRequests()
    return result
  }

  private sendNextRequests(): void {
    while (this._pendingResponses.size === 0 && this._requestQueue.length > 0) {
      const item = this._requestQueue.dequeue()
      if (item) {
        this.sendRequest(item)
      }
    }
  }

  private sendRequest(requestItem: RequestItem): void {
    const serverRequest = requestItem.request
    this.tracer.traceRequest(serverRequest, requestItem.expectsResponse, this._requestQueue.length)

    if (requestItem.expectsResponse && !requestItem.isAsync) {
      this._pendingResponses.add(requestItem.request.seq)
    }
    if (!this.tsServerProcess) return
    try {
      this.tsServerProcess.write(serverRequest)
    } catch (err) {
      const callback = this.fetchCallback(serverRequest.seq)
      if (callback) {
        callback.onError(err)
      }
    }
  }

  private tryCancelRequest(seq: number, command: string): boolean {
    try {
      if (this._requestQueue.tryDeletePendingRequest(seq)) {
        this.tracer.logTrace(`TypeScript Server: canceled request with sequence number ${seq}`)
        return true
      }

      if (this.cancellationPipeName) {
        this.tracer.logTrace(`TypeScript Server: trying to cancel ongoing request with sequence number ${seq}`)
        try {
          fs.writeFileSync(this.cancellationPipeName + seq, '')
        } catch {
          // noop
        }
        return true
      }

      this.tracer.logTrace(`TypeScript Server: tried to cancel request with sequence number ${seq}. But request got already delivered.`)
      return false
    } finally {
      const callback = this.fetchCallback(seq)
      if (callback) {
        callback.onSuccess(new ServerResponse.Cancelled(`Cancelled request ${seq} - ${command}`))
      }
    }
  }

  private fetchCallback(seq: number): any {
    const callback = this._callbacks.fetch(seq)
    if (!callback) {
      return undefined
    }
    this._pendingResponses.delete(seq)
    return callback
  }

  private dispatchMessage(message: Proto.Message): void {
    try {
      switch (message.type) {
        case 'response':
          this.dispatchResponse(message as Proto.Response)
          break

        case 'event':
          const event = message as Proto.Event
          if (event.event === 'requestCompleted') {
            const seq = (event as Proto.RequestCompletedEvent).body.request_seq
            const p = this._callbacks.fetch(seq)
            if (p) {
              this.tracer.traceRequestCompleted('requestCompleted', seq, p.startTime)
              p.onSuccess(undefined)
            }
          } else {
            this.tracer.traceEvent(event)
            this.dispatchEvent(event)
          }
          break

        default:
          throw new Error(`Unknown message type ${message.type} received`)
      }
    } finally {
      this.sendNextRequests()
    }
  }

  private dispatchResponse(response: Proto.Response): void {
    const callback = this.fetchCallback(response.request_seq)
    if (!callback) {
      return
    }

    this.tracer.traceResponse(response, callback.startTime)
    if (response.success) {
      callback.onSuccess(response)
    } else if (response.message === 'No content available.') {
      // Special case where response itself is successful but there is not any data to return.
      callback.onSuccess(ServerResponse.NoContent)
    } else {
      callback.onError(new Error(response.message))
    }
  }

  private dispatchEvent(event: Proto.Event): void {
    switch (event.event) {
      case 'syntaxDiag':
      case 'semanticDiag':
      case 'suggestionDiag':
        const diagnosticEvent = event as Proto.DiagnosticEvent
        if (diagnosticEvent.body && diagnosticEvent.body.diagnostics) {
          this._onDiagnosticsReceived.fire({
            kind: getDiagnosticsKind(event),
            resource: this.asUrl(diagnosticEvent.body.file),
            diagnostics: diagnosticEvent.body.diagnostics
          })
        }
        break

      case 'configFileDiag':
        this._onConfigDiagnosticsReceived.fire(
          event as Proto.ConfigFileDiagnosticEvent
        )
        break

      case 'projectLanguageServiceState':
        if (event.body) {
          this._onProjectLanguageServiceStateChanged.fire(
            (event as Proto.ProjectLanguageServiceStateEvent).body
          )
        }
        break

      case 'beginInstallTypes':
        if (event.body) {
          this._onDidBeginInstallTypings.fire(
            (event as Proto.BeginInstallTypesEvent).body
          )
        }
        break

      case 'endInstallTypes':
        if (event.body) {
          this._onDidEndInstallTypings.fire(
            (event as Proto.EndInstallTypesEvent).body
          )
        }
        break
      case 'projectsUpdatedInBackground':
        const body = (event as Proto.ProjectsUpdatedInBackgroundEvent).body
        const resources = body.openFiles.map(Uri.file)
        this.bufferSyncSupport.getErr(resources)
        break
      case 'typesInstallerInitializationFailed':
        if (event.body) {
          this._onTypesInstallerInitializationFailed.fire(
            (event as Proto.TypesInstallerInitializationFailedEvent).body
          )
        }
        break
      case 'projectLoadingStart':
        this.versionStatus.loading = true
        break

      case 'projectLoadingFinish':
        this.versionStatus.loading = false
        break
    }
  }

  private getTsServerArgs(currentVersion: TypeScriptVersion): string[] {
    const args: string[] = []

    args.push('--allowLocalPluginLoads')

    if (this.apiVersion.gte(API.v250)) {
      args.push('--useInferredProjectPerProjectRoot')
    } else {
      args.push('--useSingleInferredProject')
    }

    if (this.apiVersion.gte(API.v206) && this._configuration.disableAutomaticTypeAcquisition) {
      args.push('--disableAutomaticTypingAcquisition')
    }

    if (this.apiVersion.gte(API.v222)) {
      this.cancellationPipeName = getTempFile(`tscancellation-${makeRandomHexString(20)}`)
      args.push('--cancellationPipeName', this.cancellationPipeName + '*')
    }

    const logDir = getTempDirectory()
    if (this.apiVersion.gte(API.v222)) {
      const isRoot = process.getuid && process.getuid() == 0
      if (this._configuration.tsServerLogLevel !== TsServerLogLevel.Off && !isRoot) {
        if (logDir) {
          this.tsServerLogFile = path.join(logDir, `tsserver.log`)
          this.info('TSServer log file :', this.tsServerLogFile)
        } else {
          this.tsServerLogFile = null
          this.error('Could not create TSServer log directory')
        }

        if (this.tsServerLogFile) {
          args.push(
            '--logVerbosity',
            TsServerLogLevel.toString(this._configuration.tsServerLogLevel)
          )
          args.push('--logFile', this.tsServerLogFile)
        }
      }
    }

    if (this._configuration.enableTsServerTracing) {
      let tsServerTraceDirectory = createTempDirectory(`tsserver-trace-${makeRandomHexString(5)}`)
      if (tsServerTraceDirectory) {
        args.push('--traceDirectory', tsServerTraceDirectory)
        this.info('TSServer trace directory :', tsServerTraceDirectory)
      } else {
        this.error('Could not create TSServer trace directory')
      }
    }

    if (this.apiVersion.gte(API.v230)) {
      const pluginNames = this.pluginManager.plugins.map(x => x.name)
      let pluginPaths = this._configuration.tsServerPluginPaths
      pluginPaths = pluginPaths.reduce((p, c) => {
        if (path.isAbsolute(c)) {
          p.push(c)
        } else {
          let roots = workspace.workspaceFolders.map(o => Uri.parse(o.uri).fsPath)
          p.push(...roots.map(r => path.join(r, c)))
        }
        return p
      }, [])

      if (pluginNames.length) {
        const isUsingBundledTypeScriptVersion = currentVersion.path == this.versionProvider.bundledVersion.path
        args.push('--globalPlugins', pluginNames.join(','))
        for (const plugin of this.pluginManager.plugins) {
          if (isUsingBundledTypeScriptVersion || plugin.enableForWorkspaceTypeScriptVersions) {
            pluginPaths.push(plugin.path)
          }
        }
      }

      if (pluginPaths.length) {
        args.push('--pluginProbeLocations', pluginPaths.join(','))
      }
    }

    if (this._configuration.locale) {
      args.push('--locale', this._configuration.locale)
    }

    if (this._configuration.typingsCacheLocation) {
      args.push('--globalTypingsCacheLocation', `"${this._configuration.typingsCacheLocation}"`)
    }

    if (this.apiVersion.gte(API.v234)) {
      let { npmLocation } = this._configuration
      if (npmLocation) {
        this.logger.info(`using npm from ${npmLocation}`)
        args.push('--npmLocation', `"${npmLocation}"`)
      }
    }

    if (this.apiVersion.gte(API.v291)) {
      args.push('--noGetErrOnBackgroundUpdate')
    }

    if (this.apiVersion.gte(API.v345)) {
      args.push('--validateDefaultNpmLocation')
    }
    return args
  }

  public getProjectRootPath(uri: string): string | undefined {
    let root = workspace.cwd
    let u = Uri.parse(uri)
    if (u.scheme !== 'file') return undefined
    let folder = workspace.getWorkspaceFolder(uri)
    if (folder) {
      root = Uri.parse(folder.uri).fsPath
    } else {
      let filepath = Uri.parse(uri).fsPath
      if (!filepath.startsWith(root)) {
        root = path.dirname(filepath)
      }
    }
    if (root == os.homedir()) return undefined
    return root
  }

  public configurePlugin(pluginName: string, configuration: {}): any {
    if (this.apiVersion.gte(API.v314)) {
      if (!this.tsServerProcess) return
      this.executeWithoutWaitingForResponse('configurePlugin', { pluginName, configuration })
    }
  }

  public interruptGetErr<R>(f: () => R): R {
    return this.bufferSyncSupport.interuptGetErr(f)
  }

  private cancelInflightRequestsForResource(resource: string): void {
    if (this.state !== ServiceStat.Running || !this.tsServerProcess) {
      return
    }
    for (const request of this.tsServerProcess.toCancelOnResourceChange) {
      if (request.resource.toString() === resource.toString()) {
        request.cancel()
      }
    }
  }
}

function getDiagnosticsKind(event: Proto.Event): DiagnosticKind {
  switch (event.event) {
    case 'syntaxDiag':
      return DiagnosticKind.Syntax
    case 'semanticDiag':
      return DiagnosticKind.Semantic
    case 'suggestionDiag':
      return DiagnosticKind.Suggestion
  }
  throw new Error('Unknown dignostics kind')
}

const fenceCommands = new Set(['change', 'close', 'open'])

function getQueueingType(
  command: string,
  lowPriority?: boolean
): RequestQueueingType {
  if (fenceCommands.has(command)) {
    return RequestQueueingType.Fence
  }
  return lowPriority ? RequestQueueingType.LowPriority : RequestQueueingType.Normal
}

function getDebugPort(): number | undefined {
  let debugBrk = process.env[process.env.remoteName ? 'TSS_REMOTE_DEBUG_BRK' : 'TSS_DEBUG_BRK']
  let value = debugBrk || process.env[process.env.remoteName ? 'TSS_REMOTE_DEBUG_BRK' : 'TSS_DEBUG_BRK']
  if (value) {
    const port = parseInt(value)
    if (!isNaN(port)) {
      return port
    }
  }
  return undefined
}