/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
import { disposeAll, languages, TextDocument, Uri, workspace } from 'coc.nvim'
import path from 'path'
import { CodeActionKind, Diagnostic, DiagnosticSeverity, Disposable } from 'vscode-languageserver-protocol'
import { CachedNavTreeResponse } from './features/baseCodeLensProvider'
import CallHierarchyProvider from './features/callHierarchy'
import CompletionItemProvider from './features/completionItemProvider'
import DefinitionProvider from './features/definitionProvider'
import { DiagnosticKind } from './features/diagnostics'
import DirectiveCommentCompletionProvider from './features/directiveCommentCompletions'
import DocumentHighlight from './features/documentHighlight'
import DocumentSymbolProvider from './features/documentSymbol'
import FileConfigurationManager from './features/fileConfigurationManager'
import Folding from './features/folding'
import FormattingProvider from './features/formatting'
import HoverProvider from './features/hover'
import ImplementationsCodeLensProvider from './features/implementationsCodeLens'
import ImportfixProvider from './features/importFix'
import InstallModuleProvider from './features/moduleInstall'
import QuickfixProvider from './features/quickfix'
import RefactorProvider from './features/refactor'
import ReferenceProvider from './features/references'
import ReferencesCodeLensProvider from './features/referencesCodeLens'
import RenameProvider from './features/rename'
import SignatureHelpProvider from './features/signatureHelp'
import SemanticTokensProvider from './features/semanticTokens'
import SmartSelection from './features/smartSelect'
import TagClosing from './features/tagClosing'
import TypeScriptInlayHintsProvider from './features/inlayHints'
import UpdateImportsOnFileRenameHandler from './features/updatePathOnRename'
import { OrganizeImportsCodeActionProvider } from './organizeImports'
import TypeScriptServiceClient from './typescriptServiceClient'
import API from './utils/api'
import { LanguageDescription } from './utils/languageDescription'
import TypingsStatus from './utils/typingsStatus'

const suggestionSetting = 'suggestionActions.enabled'

export default class LanguageProvider {
  private readonly disposables: Disposable[] = []

  constructor(
    public client: TypeScriptServiceClient,
    private readonly fileConfigurationManager: FileConfigurationManager,
    private description: LanguageDescription,
    typingsStatus: TypingsStatus
  ) {
    workspace.onDidChangeConfiguration(this.configurationChanged, this, this.disposables)
    this.configurationChanged()
    client.onReady(() => {
      this.registerProviders(client, typingsStatus)
    })
  }

  private configurationChanged(): void {
    const config = workspace.getConfiguration(this.id, null)
    this.client.diagnosticsManager.setEnableSuggestions(this.id, config.get(suggestionSetting, true))
  }

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

  private _register(disposable: Disposable): void {
    this.disposables.push(disposable)
  }

  private registerProviders(
    client: TypeScriptServiceClient,
    typingsStatus: TypingsStatus
  ): void {
    let languageIds = this.description.modeIds
    let clientId = `tsserver-${this.description.id}`
    this._register(
      languages.registerCompletionItemProvider(clientId, 'TSC', languageIds,
        new CompletionItemProvider(client, typingsStatus, this.fileConfigurationManager, this.description.id),
        CompletionItemProvider.triggerCharacters
      )
    )
    if (this.client.apiVersion.gte(API.v230)) {
      this._register(languages.registerCompletionItemProvider(
        `${this.description.id}-directive`,
        'TSC', languageIds, new DirectiveCommentCompletionProvider(client), ['@']
      ))
    }

    let definitionProvider = new DefinitionProvider(client)
    this._register(languages.registerDefinitionProvider(languageIds, definitionProvider))
    this._register(languages.registerTypeDefinitionProvider(languageIds, definitionProvider))
    this._register(languages.registerImplementationProvider(languageIds, definitionProvider))
    this._register(languages.registerReferencesProvider(languageIds, new ReferenceProvider(client)))
    this._register(languages.registerHoverProvider(languageIds, new HoverProvider(client)))
    this._register(languages.registerDocumentHighlightProvider(languageIds, new DocumentHighlight(this.client)))
    this._register(languages.registerSignatureHelpProvider(languageIds, new SignatureHelpProvider(client), ['(', ',', '<', ')']))
    this._register(languages.registerDocumentSymbolProvider(languageIds, new DocumentSymbolProvider(client)))
    this._register(languages.registerRenameProvider(languageIds, new RenameProvider(client, this.fileConfigurationManager)))
    let formatProvider = new FormattingProvider(client, this.fileConfigurationManager)
    this._register(languages.registerDocumentFormatProvider(languageIds, formatProvider))
    this._register(languages.registerDocumentRangeFormatProvider(languageIds, formatProvider))
    this._register(languages.registerOnTypeFormattingEditProvider(languageIds, formatProvider, [';', '}', '\n', String.fromCharCode(27)]))
    this._register(languages.registerCodeActionProvider(languageIds, new InstallModuleProvider(client), 'tsserver'))
    if (this.client.apiVersion.gte(API.v380) && typeof languages['registerCallHierarchyProvider'] === 'function') {
      this._register(languages.registerCallHierarchyProvider(languageIds, new CallHierarchyProvider(client)))
    }
    if (this.client.apiVersion.gte(API.v370)) {
      const provider = new SemanticTokensProvider(client)
      if (typeof languages['registerDocumentSemanticTokensProvider'] === 'function') {
        this._register(languages.registerDocumentSemanticTokensProvider(languageIds, provider, provider.getLegend()))
      }
      if (typeof languages['registerDocumentRangeSemanticTokensProvider'] === 'function') {
        this._register(languages.registerDocumentRangeSemanticTokensProvider(languageIds, provider, provider.getLegend()))
      }
    }

    let { fileConfigurationManager } = this
    let conf = fileConfigurationManager.getLanguageConfiguration(this.id)
    if (['javascript', 'typescript'].includes(this.id)) {
      if (this.client.apiVersion.gte(API.v290) && conf.get<boolean>('updateImportsOnFileMove.enable')) {
        this._register(new UpdateImportsOnFileRenameHandler(client, this.fileConfigurationManager, this.id))
      }
    }

    if (this.client.apiVersion.gte(API.v280)) {
      this._register(languages.registerFoldingRangeProvider(languageIds, new Folding(this.client)))
      this._register(
        languages.registerCodeActionProvider(languageIds,
          new OrganizeImportsCodeActionProvider(this.client, this.fileConfigurationManager),
          'tsserver', [CodeActionKind.SourceOrganizeImports])
      )
    }
    if (this.client.apiVersion.gte(API.v240)) {
      this._register(
        languages.registerCodeActionProvider(
          languageIds,
          new RefactorProvider(client, this.fileConfigurationManager),
          'tsserver',
          [CodeActionKind.Refactor]))
    }
    this._register(
      languages.registerCodeActionProvider(
        languageIds, new QuickfixProvider(client, this.fileConfigurationManager),
        'tsserver', [CodeActionKind.QuickFix]))
    this._register(
      languages.registerCodeActionProvider(
        languageIds, new ImportfixProvider(this.client.bufferSyncSupport),
        'tsserver', [CodeActionKind.QuickFix]))
    let cachedResponse = new CachedNavTreeResponse()
    if (this.client.apiVersion.gte(API.v206) && conf.get<boolean>('referencesCodeLens.enable')) {
      this._register(languages.registerCodeLensProvider(languageIds, new ReferencesCodeLensProvider(client, cachedResponse)))
    }
    if (this.client.apiVersion.gte(API.v220) && conf.get<boolean>('implementationsCodeLens.enable')) {
      this._register(languages.registerCodeLensProvider(languageIds, new ImplementationsCodeLensProvider(client, cachedResponse)))
    }
    if (this.client.apiVersion.gte(API.v350)) {
      this._register(languages.registerSelectionRangeProvider(languageIds, new SmartSelection(this.client)))
    }
    if (this.client.apiVersion.gte(API.v300)) {
      this._register(new TagClosing(this.client, this.description.id))
    }
    if (this.client.apiVersion.gte(API.v440) && workspace.isNvim) {
      this._register(new TypeScriptInlayHintsProvider(this.client, this.fileConfigurationManager, languageIds))
    }
  }

  public handles(resource: string, doc: TextDocument): boolean {
    if (doc && this.description.modeIds.includes(doc.languageId)) {
      return true
    }
    return this.handlesConfigFile(Uri.parse(resource))
  }

  private handlesConfigFile(uri: Uri): boolean {
    const base = path.basename(uri.fsPath)
    return !!base && (!!this.description.configFilePattern && this.description.configFilePattern.test(base))
  }

  public handlesUri(resource: Uri): boolean {
    const ext = path.extname(resource.path).slice(1).toLowerCase()
    return this.description.standardFileExtensions.includes(ext) || this.handlesConfigFile(resource)
  }

  private get id(): string { // tslint:disable-line
    return this.description.id
  }

  public get diagnosticSource(): string {
    return this.description.diagnosticSource
  }

  public triggerAllDiagnostics(): void {
    this.client.bufferSyncSupport.requestAllDiagnostics()
  }

  public diagnosticsReceived(
    diagnosticsKind: DiagnosticKind,
    file: Uri,
    diagnostics: (Diagnostic & { reportUnnecessary: any, reportDeprecated: any })[]
  ): void {
    const config = workspace.getConfiguration(this.id, file.toString())
    const reportUnnecessary = config.get<boolean>('showUnused', true)
    const reportDeprecated = config.get<boolean>('showDeprecated', true)
    this.client.diagnosticsManager.diagnosticsReceived(diagnosticsKind, file.toString(), diagnostics.filter(diag => {
      if (!reportUnnecessary) {
        if (diag.reportUnnecessary && diag.severity === DiagnosticSeverity.Information) {
          return false
        }
      }
      if (!reportDeprecated) {
        if (diag.reportDeprecated && diag.severity === DiagnosticSeverity.Hint) {
          return false
        }
      }
      return true
    }))
  }
}