import path from 'path'

import * as ts from 'typescript'

import { ProjectOptions } from './CommandLineOptions'
import { Counter } from './Counter'
import {
  metaDescriptor,
  methodDescriptor,
  packageDescriptor,
  parameterDescriptor,
  termDescriptor,
  typeDescriptor,
  typeParameterDescriptor,
} from './Descriptor'
import { Input } from './Input'
import { Packages } from './Packages'
import { formatByteSizeAsHumanReadable } from './parseHumanByteSizeIntoNumber'
import { Range } from './Range'
import * as scip from './scip'
import { ScipSymbol } from './ScipSymbol'
import * as ts_inline from './TypeScriptInternal'

export class FileIndexer {
  private localCounter = new Counter()
  private propertyCounters: Map<string, Counter> = new Map()
  private localSymbolTable: Map<ts.Node, ScipSymbol> = new Map()
  private workingDirectoryRegExp: RegExp
  constructor(
    public readonly checker: ts.TypeChecker,
    public readonly options: ProjectOptions,
    public readonly input: Input,
    public readonly document: scip.scip.Document,
    public readonly globalSymbolTable: Map<ts.Node, ScipSymbol>,
    public readonly globalConstructorTable: Map<ts.ClassDeclaration, boolean>,
    public readonly packages: Packages,
    public readonly sourceFile: ts.SourceFile
  ) {
    this.workingDirectoryRegExp = new RegExp(options.cwd, 'g')
  }
  public index(): void {
    // Uncomment below if you want to skip certain files for local development.
    // if (!this.sourceFile.fileName.includes('constructor')) {
    //   return
    // }

    const byteSize = Buffer.from(this.sourceFile.getText()).length
    if (
      this.options.maxFileByteSizeNumber &&
      byteSize > this.options.maxFileByteSizeNumber
    ) {
      const humanSize = formatByteSizeAsHumanReadable(byteSize)
      const humanMaxSize = formatByteSizeAsHumanReadable(
        this.options.maxFileByteSizeNumber
      )
      console.log(
        `info: skipping file '${this.sourceFile.fileName}' because it has byte size ${humanSize} that exceeds the maximum threshold ${humanMaxSize}. ` +
          'If you intended to index this file, use the flag --max-file-byte-size to configure the maximum file size threshold.'
      )
      return
    }


    this.emitSourceFileOccurrence()
    this.visit(this.sourceFile)
  }
  private emitSourceFileOccurrence(): void {
    const symbol = this.scipSymbol(this.sourceFile)
    if (symbol.isEmpty()) {
      return
    }
    this.pushOccurrence(
      new scip.scip.Occurrence({
        range: [0, 0, 0],
        symbol: symbol.value,
        symbol_roles: scip.scip.SymbolRole.Definition,
      })
    )
    const moduleName =
      this.sourceFile.moduleName || path.basename(this.sourceFile.fileName)
    this.document.symbols.push(
      new scip.scip.SymbolInformation({
        symbol: symbol.value,
        documentation: ['```ts\nmodule "' + moduleName + '"\n```'],
      })
    )
  }
  private visit(node: ts.Node): void {
    if (
      ts.isConstructorDeclaration(node) ||
      ts.isIdentifier(node) ||
      ts.isPrivateIdentifier(node) ||
      ts.isStringLiteralLike(node)
    ) {
      const sym = this.getTSSymbolAtLocation(node)
      if (sym) {
        this.visitSymbolOccurrence(node, sym)
      }
    }

    ts.forEachChild(node, node => this.visit(node))
  }

  // Get the ts.Symbol corresponding to the current node, potentially de-aliasing
  // the direct symbol to account for imports.
  //
  // This code is directly based off src/services/goToDefinition.ts.
  private getTSSymbolAtLocation(node: ts.Node): ts.Symbol | undefined {
    const rangeNode: ts.Node = ts.isConstructorDeclaration(node)
      ? node.getFirstToken() ?? node
      : node
    const symbol = this.checker.getSymbolAtLocation(rangeNode)

    // If this is an alias, and the request came at the declaration location
    // get the aliased symbol instead. This allows for goto def on an import e.g.
    //   import {A, B} from "mod";
    // to jump to the implementation directly.
    if (
      symbol?.declarations &&
      symbol.flags & ts.SymbolFlags.Alias &&
      node.kind === ts.SyntaxKind.Identifier &&
      (node.parent === symbol.declarations[0] ||
        ts_inline.shouldSkipAlias(symbol.declarations[0]))
    ) {
      const aliased = this.checker.getAliasedSymbol(symbol)
      if (aliased.declarations) {
        return aliased
      }
    }
    return symbol
  }

  private hasConstructor(classDeclaration: ts.ClassDeclaration): boolean {
    const cached = this.globalConstructorTable.get(classDeclaration)
    if (cached !== undefined) {
      return cached
    }

    for (const member of classDeclaration.members) {
      if (ts.isConstructorDeclaration(member)) {
        this.globalConstructorTable.set(classDeclaration, true)
        return true
      }
    }

    this.globalConstructorTable.set(classDeclaration, false)
    return false
  }

  private visitSymbolOccurrence(node: ts.Node, sym: ts.Symbol): void {
    const range = Range.fromNode(node).toLsif()
    let role = 0
    const isDefinitionNode = isDefinition(node)
    if (isDefinitionNode) {
      role |= scip.scip.SymbolRole.Definition
    }
    const declarations = ts.isConstructorDeclaration(node)
      ? [node]
      : isDefinitionNode
        ? // Don't emit ambiguous definition at definition-site. You can reproduce
          // ambiguous results by triggering "Go to definition" in VS Code on `Conflict`
          // in the example below:
          // export const Conflict = 42
          // export interface Conflict {}
          //                  ^^^^^^^^ "Go to definition" shows two results: const and interface.
          // See https://github.com/sourcegraph/scip-typescript/pull/206 for more details.
          [node.parent]
        : sym?.declarations || []
    for (const declaration of declarations) {
      let scipSymbol = this.scipSymbol(declaration)

      if (
        ((ts.isIdentifier(node) && ts.isNewExpression(node.parent)) ||
          (ts.isPropertyAccessExpression(node.parent) &&
            ts.isNewExpression(node.parent.parent))) &&
        ts.isClassDeclaration(declaration) &&
        this.hasConstructor(declaration)
      ) {
        scipSymbol = ScipSymbol.global(
          scipSymbol,
          methodDescriptor('<constructor>')
        )
      }

      if (scipSymbol.isEmpty()) {
        // Skip empty symbols
        continue
      }
      this.pushOccurrence(
        new scip.scip.Occurrence({
          range,
          symbol: scipSymbol.value,
          symbol_roles: role,
        })
      )
      if (isDefinitionNode) {
        this.addSymbolInformation(node, sym, declaration, scipSymbol)
        this.handleShorthandPropertyDefinition(declaration, range)
        this.handleObjectBindingPattern(node, range)
        // Only emit one symbol for definitions sites, see https://github.com/sourcegraph/lsif-typescript/issues/45
        break
      }
    }
  }

  /**
   * Emits an additional definition occurrence when destructuring an object
   * pattern. For example:
   * ```
   * interface Props { property: number}
   * const props: Props[] = [{ property: 42 }]
   * props.map(({property}) => property) = {a}
   * //          ^^^^^^^^ references `Props.property` and defines a local parameter `property`
   * ```
   */
  private handleObjectBindingPattern(node: ts.Node, range: number[]): void {
    const isObjectBindingPatternProperty =
      ts.isIdentifier(node) &&
      ts.isBindingElement(node.parent) &&
      ts.isObjectBindingPattern(node.parent.parent)
    if (!isObjectBindingPatternProperty) {
      return
    }
    const tpe = this.checker.getTypeAtLocation(node.parent.parent)
    const property = tpe.getProperty(node.getText())
    for (const declaration of property?.declarations || []) {
      const scipSymbol = this.scipSymbol(declaration)
      if (scipSymbol.isEmpty()) {
        continue
      }
      this.pushOccurrence(
        new scip.scip.Occurrence({
          range,
          symbol: scipSymbol.value,
        })
      )
    }
  }

  /**
   * Handles the special-case around shorthand property syntax so that we emit two occurrences instead of only one.
   * Shorthand properties need two symbols because they both define a symbol and reference a symbol. For example:
   * ```
   * const a = 42
   * const b = {a}
   * //         ^ both references the local const `a` and defines a new property
   * const c = b.a
   * //          ^ reference to the property `a`, not the local const
   * ```
   */
  private handleShorthandPropertyDefinition(
    declaration: ts.Node,
    range: number[]
  ): void {
    if (declaration.kind !== ts.SyntaxKind.ShorthandPropertyAssignment) {
      return
    }
    const valueSymbol =
      this.checker.getShorthandAssignmentValueSymbol(declaration)
    if (!valueSymbol) {
      return
    }
    for (const symbol of valueSymbol?.declarations || []) {
      const scipSymbol = this.scipSymbol(symbol)
      if (scipSymbol.isEmpty()) {
        continue
      }
      this.pushOccurrence(
        new scip.scip.Occurrence({
          range,
          symbol: scipSymbol.value,
        })
      )
    }
  }

  private hideWorkingDirectory(value: string): string {
    return value.replace(this.workingDirectoryRegExp, '')
  }
  private addSymbolInformation(
    node: ts.Node,
    sym: ts.Symbol,
    declaration: ts.Node,
    symbol: ScipSymbol
  ): void {
    const documentation = [
      '```ts\n' +
        this.hideWorkingDirectory(this.signatureForDocumentation(node, sym)) +
        '\n```',
    ]
    const docstring = sym.getDocumentationComment(this.checker)
    if (docstring.length > 0) {
      documentation.push(ts.displayPartsToString(docstring))
    }

    this.document.symbols.push(
      new scip.scip.SymbolInformation({
        symbol: symbol.value,
        documentation,
        relationships: this.relationships(declaration, symbol),
      })
    )
  }

  private pushOccurrence(occurrence: scip.scip.Occurrence): void {
    const lastOccurrence = this.document.occurrences.at(-1)
    if (lastOccurrence) {
      if (isEqualOccurrence(lastOccurrence, occurrence)) {
        return
      }
    }
    this.document.occurrences.push(occurrence)
  }

  private relationships(
    declaration: ts.Node,
    declarationSymbol: ScipSymbol
  ): scip.scip.Relationship[] {
    const relationships: scip.scip.Relationship[] = []
    const isAddedSymbol = new Set<string>()
    const pushImplementation = (
      node: ts.NamedDeclaration,
      isReferences: boolean
    ): void => {
      const symbol = this.scipSymbol(node)
      if (symbol.isEmpty()) {
        return
      }
      if (symbol.value === declarationSymbol.value) {
        return
      }
      if (isAddedSymbol.has(symbol.value)) {
        // Avoid duplicate relationships. This can happen for overloaded methods
        // that have different ts.Symbol but the same SCIP symbol.
        return
      }
      isAddedSymbol.add(symbol.value)
      relationships.push(
        new scip.scip.Relationship({
          symbol: symbol.value,
          is_implementation: true,
          is_reference: isReferences,
        })
      )
    }
    if (ts.isClassDeclaration(declaration)) {
      this.forEachAncestor(declaration, ancestor => {
        pushImplementation(ancestor, false)
      })
    } else if (
      ts.isMethodDeclaration(declaration) ||
      ts.isMethodSignature(declaration) ||
      ts.isPropertyAssignment(declaration) ||
      ts.isPropertyDeclaration(declaration)
    ) {
      const declarationName = declaration.name.getText()
      this.forEachAncestor(declaration.parent, ancestor => {
        for (const member of ancestor.members) {
          if (declarationName === member.name?.getText()) {
            pushImplementation(member, true)
          }
        }
      })
    }
    return relationships
  }
  private scipSymbol(node: ts.Node): ScipSymbol {
    const fromCache: ScipSymbol | undefined =
      this.globalSymbolTable.get(node) || this.localSymbolTable.get(node)
    if (fromCache) {
      return fromCache
    }
    if (ts.isBlock(node)) {
      return ScipSymbol.empty()
    }
    if (ts.isSourceFile(node)) {
      const package_ = this.packages.symbol(node.fileName)
      if (package_.isEmpty()) {
        return this.cached(node, ScipSymbol.anonymousPackage())
      }
      return this.cached(node, package_)
    }
    if (
      ts.isPropertyAssignment(node) ||
      ts.isShorthandPropertyAssignment(node)
    ) {
      const name = node.name.getText()
      let counter = this.propertyCounters.get(name)
      if (!counter) {
        counter = new Counter()
        this.propertyCounters.set(name, counter)
      }
      return this.cached(
        node,
        ScipSymbol.global(
          this.scipSymbol(node.getSourceFile()),
          metaDescriptor(`${node.name.getText()}${counter.next()}`)
        )
      )
    }

    if (ts.isJsxAttribute(node)) {
      // NOTE(olafurpg): the logic below is a bit convoluted but I spent several
      // hours and failed to come up with a cleaner solution. JSX attributes
      // have custom typechecking rules, as documented here
      // https://www.typescriptlang.org/docs/handbook/jsx.html#type-checking The
      // only way to access the actual symbol we want to reference appears to go
      // through the JSX opening element, which is the grandparent of the JSX
      // attribute node. Through the signature of the opening element, we get
      // the permitted attributes by querying the type of the first parameter.
      const jsxElement = node.parent.parent
      const props = this.checker
        .getResolvedSignature(jsxElement)
        ?.getParameters()?.[0]
      if (props) {
        try {
          const tpe = this.checker.getTypeOfSymbolAtLocation(props, node)
          const property = tpe.getProperty(
            ts_inline.getTextOfJsxAttributeName(node.name)
          )
          for (const decl of property?.declarations || []) {
            return this.scipSymbol(decl)
          }
        } catch {
          // TODO: https://github.com/sourcegraph/lsif-typescript/issues/34
          // continue regardless of error, the TypeScript compiler tends to
          // trigger stack overflows in getTypeOfSymbolAtLocation and we
          // don't know why yet.
        }
      }
    }

    const owner = this.scipSymbol(node.parent)
    if (owner.isEmpty() || owner.isLocal()) {
      return this.newLocalSymbol(node)
    }

    if (isAnonymousContainerOfSymbols(node)) {
      return this.cached(node, this.scipSymbol(node.parent))
    }

    if (
      ts.isImportSpecifier(node) ||
      ts.isImportClause(node) ||
      ts.isNamespaceImport(node)
    ) {
      const tpe = this.checker.getTypeAtLocation(node)
      for (const declaration of tpe.symbol?.declarations || []) {
        return this.scipSymbol(declaration)
      }
    }

    const desc = this.descriptor(node)
    if (desc) {
      return this.cached(node, ScipSymbol.global(owner, desc))
    }

    // Fallback case: generate a local symbol. It's not a bug when this case
    // happens. For example, we hit this case for block `{}` that are local
    // symbols, which are direct children of global symbols (toplevel
    // functions).
    return this.newLocalSymbol(node)
  }

  private newLocalSymbol(node: ts.Node): ScipSymbol {
    const symbol = ScipSymbol.local(this.localCounter.next())
    this.localSymbolTable.set(node, symbol)
    return symbol
  }
  private cached(node: ts.Node, symbol: ScipSymbol): ScipSymbol {
    this.globalSymbolTable.set(node, symbol)
    return symbol
  }
  private descriptor(node: ts.Node): scip.scip.Descriptor | undefined {
    if (
      ts.isInterfaceDeclaration(node) ||
      ts.isEnumDeclaration(node) ||
      ts.isTypeAliasDeclaration(node)
    ) {
      return typeDescriptor(node.name.getText())
    }
    if (ts.isClassLike(node)) {
      const name = node.name?.getText()
      if (name) {
        return typeDescriptor(name)
      }
    }
    if (
      ts.isFunctionDeclaration(node) ||
      ts.isMethodSignature(node) ||
      ts.isMethodDeclaration(node)
    ) {
      const name = node.name?.getText()
      if (name) {
        return methodDescriptor(name)
      }
    }
    if (ts.isConstructorDeclaration(node)) {
      return methodDescriptor('<constructor>')
    }
    if (
      ts.isPropertyDeclaration(node) ||
      ts.isPropertySignature(node) ||
      ts.isEnumMember(node) ||
      ts.isVariableDeclaration(node)
    ) {
      return termDescriptor(node.name.getText())
    }
    if (ts.isAccessor(node)) {
      const prefix = ts.isGetAccessor(node) ? '<get>' : '<set>'
      return methodDescriptor(prefix + node.name.getText())
    }
    if (ts.isModuleDeclaration(node)) {
      return packageDescriptor(node.name.getText())
    }
    if (ts.isParameter(node)) {
      return parameterDescriptor(node.name.getText())
    }
    if (ts.isTypeParameterDeclaration(node)) {
      return typeParameterDescriptor(node.name.getText())
    }
    if (ts.isTypeReferenceNode(node)) {
      return metaDescriptor(node.typeName.getText())
    }
    if (ts.isTypeLiteralNode(node)) {
      return metaDescriptor('typeLiteral' + this.localCounter.next().toString())
    }
    return undefined
  }

  private signatureForDocumentation(node: ts.Node, sym: ts.Symbol): string {
    const kind = scriptElementKind(node, sym)
    const type = (): string =>
      this.checker.typeToString(this.checker.getTypeAtLocation(node))
    const asSignatureDeclaration = (
      node: ts.Node,
      sym: ts.Symbol
    ): ts.SignatureDeclaration | undefined => {
      const declaration = sym.declarations?.[0]
      if (!declaration) {
        return undefined
      }
      return ts.isConstructorDeclaration(node)
        ? node
        : ts.isFunctionDeclaration(declaration)
          ? declaration
          : ts.isMethodDeclaration(declaration)
            ? declaration
            : undefined
    }
    const signature = (): string | undefined => {
      const signatureDeclaration = asSignatureDeclaration(node, sym)
      if (!signatureDeclaration) {
        return undefined
      }
      const signature =
        this.checker.getSignatureFromDeclaration(signatureDeclaration)
      return signature ? this.checker.signatureToString(signature) : undefined
    }
    switch (kind) {
      case ts.ScriptElementKind.localVariableElement:
      case ts.ScriptElementKind.variableElement: {
        return 'var ' + node.getText() + ': ' + type()
      }
      case ts.ScriptElementKind.memberVariableElement: {
        return '(property) ' + node.getText() + ': ' + type()
      }
      case ts.ScriptElementKind.parameterElement: {
        return '(parameter) ' + node.getText() + ': ' + type()
      }
      case ts.ScriptElementKind.constElement: {
        return 'const ' + node.getText() + ': ' + type()
      }
      case ts.ScriptElementKind.letElement: {
        return 'let ' + node.getText() + ': ' + type()
      }
      case ts.ScriptElementKind.alias: {
        return 'type ' + node.getText()
      }
      case ts.ScriptElementKind.classElement:
      case ts.ScriptElementKind.localClassElement: {
        if (ts.isConstructorDeclaration(node)) {
          return 'constructor' + (signature() || '')
        }
        return 'class ' + node.getText()
      }
      case ts.ScriptElementKind.interfaceElement: {
        return 'interface ' + node.getText()
      }
      case ts.ScriptElementKind.enumElement: {
        return 'enum ' + node.getText()
      }
      case ts.ScriptElementKind.enumMemberElement: {
        let suffix = ''
        const declaration = sym.declarations?.[0]
        if (declaration && ts.isEnumMember(declaration)) {
          const constantValue = this.checker.getConstantValue(declaration)
          if (constantValue) {
            suffix = ' = ' + constantValue.toString()
          }
        }
        return '(enum member) ' + node.getText() + suffix
      }
      case ts.ScriptElementKind.functionElement: {
        return 'function ' + node.getText() + (signature() || type())
      }
      case ts.ScriptElementKind.memberFunctionElement: {
        return '(method) ' + node.getText() + (signature() || type())
      }
      case ts.ScriptElementKind.memberGetAccessorElement: {
        return 'get ' + node.getText() + ': ' + type()
      }
      case ts.ScriptElementKind.memberSetAccessorElement: {
        return 'set ' + node.getText() + type()
      }
      case ts.ScriptElementKind.constructorImplementationElement: {
        return ''
      }
    }
    return node.getText() + ': ' + type()
  }

  // Invokes the `onAncestor` callback for all "ancestors" of the provided node,
  // where "ancestor" is loosely defined as the superclass or superinterface of
  // that node. The callback is invoked on the `node` parameter itself if it's
  // class-like or an interface.
  private forEachAncestor(
    node: ts.Node,
    onAncestor: (
      ancestor: ts.ClassLikeDeclaration | ts.InterfaceDeclaration
    ) => void
  ): void {
    const isVisited = new Set<ts.Node>()
    const loop = (declaration: ts.Node): void => {
      if (isVisited.has(declaration)) {
        return
      }
      isVisited.add(declaration)
      if (
        ts.isClassLike(declaration) ||
        ts.isInterfaceDeclaration(declaration)
      ) {
        onAncestor(declaration)
      }
      if (ts.isObjectLiteralExpression(declaration)) {
        const tpe = this.inferredTypeOfObjectLiteral(
          declaration.parent,
          declaration
        )
        for (const symbolDeclaration of tpe.symbol?.declarations || []) {
          loop(symbolDeclaration)
        }
      } else if (
        ts.isClassLike(declaration) ||
        ts.isInterfaceDeclaration(declaration)
      ) {
        for (const heritageClause of declaration?.heritageClauses || []) {
          for (const tpe of heritageClause.types) {
            const ancestorSymbol = this.getTSSymbolAtLocation(tpe.expression)
            if (ancestorSymbol) {
              for (const ancestorDecl of ancestorSymbol.declarations || []) {
                loop(ancestorDecl)
              }
            }
          }
        }
      }
    }
    loop(node)
  }

  // Returns the "inferred" type of the provided object literal, where
  // "inferred" is loosely defined as the type that is expected in the position
  // where the object literal appears.  For example, the object literal in
  // `const x: SomeInterface = {y: 42}` has the inferred type `SomeInterface`
  // even if `this.checker.getTypeAtLocation({y: 42})` does not return
  // `SomeInterface`. The object literal could satisfy many types, but in this
  // particular location must only satisfy `SomeInterface`.
  private inferredTypeOfObjectLiteral(
    node: ts.Node,
    literal: ts.ObjectLiteralExpression
  ): ts.Type {
    if (
      ts.isIfStatement(node) ||
      ts.isForStatement(node) ||
      ts.isForInStatement(node) ||
      ts.isForOfStatement(node) ||
      ts.isWhileStatement(node) ||
      ts.isDoStatement(node) ||
      ts.isReturnStatement(node) ||
      ts.isBlock(node)
    ) {
      return this.inferredTypeOfObjectLiteral(node.parent, literal)
    }

    if (ts.isVariableDeclaration(node)) {
      // Example, return `SomeInterface` from `const x: SomeInterface = {y: 42}`.
      return this.checker.getTypeAtLocation(node.name)
    }

    if (ts.isFunctionLike(node)) {
      const functionType = this.checker.getTypeAtLocation(node)
      const callSignatures = functionType.getCallSignatures()
      if (callSignatures.length > 0) {
        return callSignatures[0].getReturnType()
      }
    }

    if (ts.isCallOrNewExpression(node)) {
      // Example: return the type of the second parameter of `someMethod` from
      // the expression `someMethod(someParameter, {y: 42})`.
      const signature = this.checker.getResolvedSignature(node)
      for (const [index, argument] of (node.arguments || []).entries()) {
        if (argument === literal) {
          const parameterSymbol = signature?.getParameters()[index]
          if (parameterSymbol) {
            return this.checker.getTypeOfSymbolAtLocation(parameterSymbol, node)
          }
        }
      }
    }

    return this.checker.getTypeAtLocation(literal)
  }
}

function isAnonymousContainerOfSymbols(node: ts.Node): boolean {
  return (
    ts.isModuleBlock(node) ||
    ts.isImportDeclaration(node) ||
    (ts.isImportClause(node) && !node.name) ||
    ts.isNamedImports(node) ||
    ts.isVariableStatement(node) ||
    ts.isVariableDeclarationList(node)
  )
}

function scriptElementKind(
  node: ts.Node,
  sym: ts.Symbol
): ts.ScriptElementKind {
  const flags = sym.getFlags()
  if (flags & ts.SymbolFlags.TypeAlias) {
    return ts.ScriptElementKind.alias
  }
  if (flags & ts.SymbolFlags.Class) {
    return ts.ScriptElementKind.classElement
  }
  if (flags & ts.SymbolFlags.Interface) {
    return ts.ScriptElementKind.interfaceElement
  }
  if (flags & ts.SymbolFlags.Enum) {
    return ts.ScriptElementKind.enumElement
  }
  if (flags & ts.SymbolFlags.EnumMember) {
    return ts.ScriptElementKind.enumMemberElement
  }
  if (flags & ts.SymbolFlags.Method) {
    return ts.ScriptElementKind.memberFunctionElement
  }
  if (flags & ts.SymbolFlags.GetAccessor) {
    return ts.ScriptElementKind.memberGetAccessorElement
  }
  if (flags & ts.SymbolFlags.SetAccessor) {
    return ts.ScriptElementKind.memberSetAccessorElement
  }
  if (flags & ts.SymbolFlags.Constructor) {
    return ts.ScriptElementKind.constructorImplementationElement
  }
  if (flags & ts.SymbolFlags.Function) {
    return ts.ScriptElementKind.functionElement
  }
  if (flags & ts.SymbolFlags.Variable) {
    if (ts_inline.isParameter(sym)) {
      return ts.ScriptElementKind.parameterElement
    }
    if (node.flags & ts.NodeFlags.Const) {
      return ts.ScriptElementKind.constElement
    }
    if (node.flags & ts.NodeFlags.Let) {
      return ts.ScriptElementKind.letElement
    }
    return ts.ScriptElementKind.variableElement
  }
  if (flags & ts.SymbolFlags.ClassMember) {
    return ts.ScriptElementKind.memberVariableElement
  }
  return ts.ScriptElementKind.unknown
}

function isEqualOccurrence(
  a: scip.scip.Occurrence,
  b: scip.scip.Occurrence
): boolean {
  return (
    a.symbol_roles === b.symbol_roles &&
    a.symbol === b.symbol &&
    isEqualArray(a.range, b.range)
  )
}

function isEqualArray<T>(a: T[], b: T[]): boolean {
  if (a.length !== b.length) {
    return false
  }
  for (let index = 0; index < a.length; index++) {
    if (a[index] !== b[index]) {
      return false
    }
  }
  return true
}

function declarationName(node: ts.Node): ts.Node | undefined {
  if (
    ts.isBindingElement(node) ||
    ts.isEnumDeclaration(node) ||
    ts.isEnumMember(node) ||
    ts.isVariableDeclaration(node) ||
    ts.isPropertyDeclaration(node) ||
    ts.isAccessor(node) ||
    ts.isMethodSignature(node) ||
    ts.isMethodDeclaration(node) ||
    ts.isPropertySignature(node) ||
    ts.isFunctionDeclaration(node) ||
    ts.isModuleDeclaration(node) ||
    ts.isPropertyAssignment(node) ||
    ts.isShorthandPropertyAssignment(node) ||
    ts.isParameter(node) ||
    ts.isTypeParameterDeclaration(node) ||
    ts.isTypeAliasDeclaration(node) ||
    ts.isInterfaceDeclaration(node) ||
    ts.isClassDeclaration(node)
  ) {
    return node.name
  }
  return undefined
}

/**
 * For example:
 *
 * const a = 1
 *       ^ node
 *       ^ node.parent.name
 *       ^^^^^ node.parent
 *
 * function a(): void {}
 *          ^ node
 *          ^ node.parent.name
 * ^^^^^^^^^^^^^^^^^^^^^ node.parent
 */
function isDefinition(node: ts.Node): boolean {
  return (
    declarationName(node.parent) === node || ts.isConstructorDeclaration(node)
  )
}
