/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
import { DiagnosticKind, disposeAll, workspace } from 'coc.nvim'
import { Range, Diagnostic, DiagnosticSeverity, Disposable, Position } from 'vscode-languageserver-protocol'
import Uri from 'vscode-uri'
import LanguageProvider from './languageProvider'
import * as Proto from './protocol'
import * as PConst from './protocol.const'
import TypeScriptServiceClient from './typescriptServiceClient'
import { LanguageDescription } from './utils/languageDescription'
import * as typeConverters from './utils/typeConverters'
import TypingsStatus, { AtaProgressReporter } from './utils/typingsStatus'

// Style check diagnostics that can be reported as warnings
const styleCheckDiagnostics = [
  6133, // variable is declared but never used
  6138, // property is declared but its value is never read
  7027, // unreachable code detected
  7028, // unused label
  7029, // fall through case in switch
  7030 // not all code paths return a value
]

export default class TypeScriptServiceClientHost implements Disposable {
  private readonly ataProgressReporter: AtaProgressReporter
  private readonly typingsStatus: TypingsStatus
  private readonly client: TypeScriptServiceClient
  private readonly languages: LanguageProvider[] = []
  private readonly languagePerId = new Map<string, LanguageProvider>()
  private readonly disposables: Disposable[] = []
  private reportStyleCheckAsWarnings = true

  constructor(descriptions: LanguageDescription[]) {
    const handleProjectChange = () => {
      setTimeout(() => {
        this.triggerAllDiagnostics()
      }, 1500)
    }

    const configFileWatcher = workspace.createFileSystemWatcher('**/[tj]sconfig.json')
    this.disposables.push(configFileWatcher)
    configFileWatcher.onDidCreate(
      this.reloadProjects,
      this,
      this.disposables
    )
    configFileWatcher.onDidDelete(
      this.reloadProjects,
      this,
      this.disposables
    )
    configFileWatcher.onDidChange(handleProjectChange, this, this.disposables)

    this.client = new TypeScriptServiceClient()
    this.disposables.push(this.client)
    this.client.onDiagnosticsReceived(({ kind, resource, diagnostics }) => {
      this.diagnosticsReceived(kind, resource, diagnostics)
    }, null, this.disposables)

    this.client.onConfigDiagnosticsReceived(diag => {
      let { body } = diag
      if (body) {
        let { configFile, diagnostics } = body
        let uri = Uri.file(configFile)
        let language = this.findLanguage(uri)
        if (!language) return
        if (diagnostics.length == 0) {
          language.configFileDiagnosticsReceived(uri, [])
        } else {
          let range = Range.create(Position.create(0, 0), Position.create(0, 1))
          let { text, code, category } = diagnostics[0]
          let severity = category == 'error' ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning
          let diagnostic = Diagnostic.create(range, text, severity, code)
          language.configFileDiagnosticsReceived(uri, [diagnostic])
        }
      }
    }, null, this.disposables)

    this.typingsStatus = new TypingsStatus(this.client)
    this.ataProgressReporter = new AtaProgressReporter(this.client)
    for (const description of descriptions) { // tslint:disable-line
      const manager = new LanguageProvider(
        this.client,
        description,
        this.typingsStatus
      )
      this.languages.push(manager)
      this.disposables.push(manager)
      this.languagePerId.set(description.id, manager)
    }

    this.client.ensureServiceStarted()
    this.client.onTsServerStarted(() => {
      this.triggerAllDiagnostics()
    })
    this.configurationChanged()
  }

  public dispose(): void {
    disposeAll(this.disposables)
    this.typingsStatus.dispose()
    this.ataProgressReporter.dispose()
  }

  public reset(): void {
    for (let lang of this.languages) {
      lang.fileConfigurationManager.reset()
    }
  }

  public get serviceClient(): TypeScriptServiceClient {
    return this.client
  }

  public reloadProjects(): void {
    this.client.execute('reloadProjects', null, false) // tslint:disable-line
    this.triggerAllDiagnostics()
  }

  // typescript or javascript
  public getProvider(languageId: string): LanguageProvider {
    return this.languagePerId.get(languageId)
  }

  private configurationChanged(): void {
    const config = workspace.getConfiguration('tsserver')
    this.reportStyleCheckAsWarnings = config.get('reportStyleChecksAsWarnings', true)
  }

  public findLanguage(resource: Uri): LanguageProvider | null {
    try {
      return this.languages.find(language => language.handles(resource))
    } catch {
      return null
    }
  }

  public handles(uri: string): boolean {
    return this.findLanguage(Uri.parse(uri)) != null
  }

  private triggerAllDiagnostics(): void {
    for (const language of this.languagePerId.values()) {
      language.triggerAllDiagnostics()
    }
  }

  private diagnosticsReceived(
    kind: DiagnosticKind,
    resource: Uri,
    diagnostics: Proto.Diagnostic[]
  ): void {
    const language = this.findLanguage(resource)
    if (language) {
      language.diagnosticsReceived(
        kind,
        resource,
        this.createMarkerDatas(diagnostics))
    }
  }

  private createMarkerDatas(diagnostics: Proto.Diagnostic[]): Diagnostic[] {
    return diagnostics.map(tsDiag => this.tsDiagnosticToLspDiagnostic(tsDiag))
  }

  private tsDiagnosticToLspDiagnostic(diagnostic: Proto.Diagnostic): Diagnostic {
    const { start, end, text } = diagnostic
    const range = {
      start: typeConverters.Position.fromLocation(start),
      end: typeConverters.Position.fromLocation(end)
    }
    return {
      range,
      message: text,
      code: diagnostic.code ? diagnostic.code : null,
      severity: this.getDiagnosticSeverity(diagnostic),
      source: diagnostic.source || 'tsserver',
    }
  }

  private getDiagnosticSeverity(diagnostic: Proto.Diagnostic): DiagnosticSeverity {
    if (
      this.reportStyleCheckAsWarnings &&
      this.isStyleCheckDiagnostic(diagnostic.code) &&
      diagnostic.category === PConst.DiagnosticCategory.error
    ) {
      return DiagnosticSeverity.Warning
    }

    switch (diagnostic.category) {
      case PConst.DiagnosticCategory.error:
        return DiagnosticSeverity.Error

      case PConst.DiagnosticCategory.warning:
        return DiagnosticSeverity.Warning

      case PConst.DiagnosticCategory.suggestion:
        return DiagnosticSeverity.Information

      default:
        return DiagnosticSeverity.Error
    }
  }

  private isStyleCheckDiagnostic(code: number | undefined): boolean {
    return code ? styleCheckDiagnostics.indexOf(code) !== -1 : false
  }
}