const { getScope } = require('../scope')
const { findVariable } = require('@eslint-community/eslint-utils')
const { inferRuntimeTypeFromTypeNode } = require('./ts-types')
/**
 * @typedef {import('@typescript-eslint/types').TSESTree.TypeNode} TSESTreeTypeNode
 * @typedef {import('@typescript-eslint/types').TSESTree.TSInterfaceBody} TSESTreeTSInterfaceBody
 * @typedef {import('@typescript-eslint/types').TSESTree.TSTypeLiteral} TSESTreeTSTypeLiteral
 * @typedef {import('@typescript-eslint/types').TSESTree.TSFunctionType} TSESTreeTSFunctionType
 * @typedef {import('@typescript-eslint/types').TSESTree.Parameter} TSESTreeParameter
 * @typedef {import('@typescript-eslint/types').TSESTree.Node} TSESTreeNode
 *
 */
/**
 * @typedef {import('../index').ComponentTypeProp} ComponentTypeProp
 * @typedef {import('../index').ComponentUnknownProp} ComponentUnknownProp
 * @typedef {import('../index').ComponentTypeEmit} ComponentTypeEmit
 * @typedef {import('../index').ComponentUnknownEmit} ComponentUnknownEmit
 */

const noop = Function.prototype

module.exports = {
  isTypeNode,
  flattenTypeNodes,
  isTSInterfaceBody,
  isTSTypeLiteral,
  isTSTypeLiteralOrTSFunctionType,
  extractRuntimeProps,
  extractRuntimeEmits
}

/**
 * @param {ASTNode} node
 * @returns {node is TypeNode}
 */
function isTypeNode(node) {
  if (
    node.type === 'TSAbstractKeyword' ||
    node.type === 'TSAnyKeyword' ||
    node.type === 'TSAsyncKeyword' ||
    node.type === 'TSArrayType' ||
    node.type === 'TSBigIntKeyword' ||
    node.type === 'TSBooleanKeyword' ||
    node.type === 'TSConditionalType' ||
    node.type === 'TSConstructorType' ||
    node.type === 'TSDeclareKeyword' ||
    node.type === 'TSExportKeyword' ||
    node.type === 'TSFunctionType' ||
    node.type === 'TSImportType' ||
    node.type === 'TSIndexedAccessType' ||
    node.type === 'TSInferType' ||
    node.type === 'TSIntersectionType' ||
    node.type === 'TSIntrinsicKeyword' ||
    node.type === 'TSLiteralType' ||
    node.type === 'TSMappedType' ||
    node.type === 'TSNamedTupleMember' ||
    node.type === 'TSNeverKeyword' ||
    node.type === 'TSNullKeyword' ||
    node.type === 'TSNumberKeyword' ||
    node.type === 'TSObjectKeyword' ||
    node.type === 'TSOptionalType' ||
    node.type === 'TSQualifiedName' ||
    node.type === 'TSPrivateKeyword' ||
    node.type === 'TSProtectedKeyword' ||
    node.type === 'TSPublicKeyword' ||
    node.type === 'TSReadonlyKeyword' ||
    node.type === 'TSRestType' ||
    node.type === 'TSStaticKeyword' ||
    node.type === 'TSStringKeyword' ||
    node.type === 'TSSymbolKeyword' ||
    node.type === 'TSTemplateLiteralType' ||
    node.type === 'TSThisType' ||
    node.type === 'TSTupleType' ||
    node.type === 'TSTypeLiteral' ||
    node.type === 'TSTypeOperator' ||
    node.type === 'TSTypePredicate' ||
    node.type === 'TSTypeQuery' ||
    node.type === 'TSTypeReference' ||
    node.type === 'TSUndefinedKeyword' ||
    node.type === 'TSUnionType' ||
    node.type === 'TSUnknownKeyword' ||
    node.type === 'TSVoidKeyword'
  ) {
    /** @type {TypeNode['type']} for type check */
    const type = node.type
    noop(type)
    return true
  }
  /** @type {Exclude<ASTNode['type'], TypeNode['type']>} for type check */
  const type = node.type
  noop(type)
  return false
}

/**
 * @param {TSESTreeTypeNode|TSESTreeTSInterfaceBody} node
 * @returns {node is TSESTreeTSInterfaceBody}
 */
function isTSInterfaceBody(node) {
  return node.type === 'TSInterfaceBody'
}
/**
 * @param {TSESTreeTypeNode} node
 * @returns {node is TSESTreeTSTypeLiteral}
 */
function isTSTypeLiteral(node) {
  return node.type === 'TSTypeLiteral'
}
/**
 * @param {TSESTreeTypeNode} node
 * @returns {node is TSESTreeTSFunctionType}
 */
function isTSFunctionType(node) {
  return node.type === 'TSFunctionType'
}
/**
 * @param {TSESTreeTypeNode} node
 * @returns {node is TSESTreeTSTypeLiteral | TSESTreeTSFunctionType}
 */
function isTSTypeLiteralOrTSFunctionType(node) {
  return isTSTypeLiteral(node) || isTSFunctionType(node)
}

/**
 * @see https://github.com/vuejs/vue-next/blob/253ca2729d808fc051215876aa4af986e4caa43c/packages/compiler-sfc/src/compileScript.ts#L1512
 * @param {RuleContext} context The ESLint rule context object.
 * @param {TSESTreeTSTypeLiteral | TSESTreeTSInterfaceBody} node
 * @returns {IterableIterator<ComponentTypeProp | ComponentUnknownProp>}
 */
function* extractRuntimeProps(context, node) {
  const members = node.type === 'TSTypeLiteral' ? node.members : node.body
  for (const member of members) {
    if (
      member.type === 'TSPropertySignature' ||
      member.type === 'TSMethodSignature'
    ) {
      if (member.key.type !== 'Identifier' && member.key.type !== 'Literal') {
        yield {
          type: 'unknown',
          propName: null,
          node: /** @type {Expression} */ (member.key)
        }
        continue
      }
      /** @type {string[]|undefined} */
      let types
      if (member.type === 'TSMethodSignature') {
        types = ['Function']
      } else if (member.typeAnnotation) {
        types = inferRuntimeType(context, member.typeAnnotation.typeAnnotation)
      }
      yield {
        type: 'type',
        key: /** @type {Identifier | Literal} */ (member.key),
        propName:
          member.key.type === 'Identifier'
            ? member.key.name
            : `${member.key.value}`,
        node: /** @type {TSPropertySignature | TSMethodSignature} */ (member),

        required: !member.optional,
        types: types || [`null`]
      }
    }
  }
}

/**
 * @param {TSESTreeTSTypeLiteral | TSESTreeTSInterfaceBody | TSESTreeTSFunctionType} node
 * @returns {IterableIterator<ComponentTypeEmit | ComponentUnknownEmit>}
 */
function* extractRuntimeEmits(node) {
  if (node.type === 'TSFunctionType') {
    yield* extractEventNames(
      node.params[0],
      /** @type {TSFunctionType} */ (node)
    )
    return
  }
  const members = node.type === 'TSTypeLiteral' ? node.members : node.body
  for (const member of members) {
    if (member.type === 'TSCallSignatureDeclaration') {
      yield* extractEventNames(
        member.params[0],
        /** @type {TSCallSignatureDeclaration} */ (member)
      )
    } else if (
      member.type === 'TSPropertySignature' ||
      member.type === 'TSMethodSignature'
    ) {
      if (member.key.type !== 'Identifier' && member.key.type !== 'Literal') {
        yield {
          type: 'unknown',
          emitName: null,
          node: /** @type {Expression} */ (member.key)
        }
        continue
      }
      yield {
        type: 'type',
        key: /** @type {Identifier | Literal} */ (member.key),
        emitName:
          member.key.type === 'Identifier'
            ? member.key.name
            : `${member.key.value}`,
        node: /** @type {TSPropertySignature | TSMethodSignature} */ (member)
      }
    }
  }
}

/**
 * @param {TSESTreeParameter} eventName
 * @param {TSCallSignatureDeclaration | TSFunctionType} member
 * @returns {IterableIterator<ComponentTypeEmit>}
 */
function* extractEventNames(eventName, member) {
  if (
    eventName &&
    eventName.type === 'Identifier' &&
    eventName.typeAnnotation &&
    eventName.typeAnnotation.type === 'TSTypeAnnotation'
  ) {
    const typeNode = eventName.typeAnnotation.typeAnnotation
    if (
      typeNode.type === 'TSLiteralType' &&
      typeNode.literal.type === 'Literal'
    ) {
      const emitName = String(typeNode.literal.value)
      yield {
        type: 'type',
        key: /** @type {TSLiteralType} */ (typeNode),
        emitName,
        node: member
      }
    } else if (typeNode.type === 'TSUnionType') {
      for (const t of typeNode.types) {
        if (t.type === 'TSLiteralType' && t.literal.type === 'Literal') {
          const emitName = String(t.literal.value)
          yield {
            type: 'type',
            key: /** @type {TSLiteralType} */ (t),
            emitName,
            node: member
          }
        }
      }
    }
  }
}

/**
 * @param {RuleContext} context The ESLint rule context object.
 * @param {TSESTreeTypeNode} node
 * @returns {(TSESTreeTypeNode|TSESTreeTSInterfaceBody)[]}
 */
function flattenTypeNodes(context, node) {
  /**
   * @typedef {object} TraversedData
   * @property {Set<TSESTreeTypeNode|TSESTreeTSInterfaceBody>} nodes
   * @property {boolean} finished
   */
  /** @type {Map<TSESTreeTypeNode,TraversedData>} */
  const traversed = new Map()

  return [...flattenImpl(node)]
  /**
   * @param {TSESTreeTypeNode} node
   * @returns {Iterable<TSESTreeTypeNode|TSESTreeTSInterfaceBody>}
   */
  function* flattenImpl(node) {
    if (node.type === 'TSUnionType' || node.type === 'TSIntersectionType') {
      for (const typeNode of node.types) {
        yield* flattenImpl(typeNode)
      }
      return
    }
    if (
      node.type === 'TSTypeReference' &&
      node.typeName.type === 'Identifier'
    ) {
      const refName = node.typeName.name
      const variable = findVariable(
        getScope(context, /** @type {any} */ (node)),
        refName
      )
      if (variable && variable.defs.length === 1) {
        const defNode = /** @type {TSESTreeNode} */ (variable.defs[0].node)
        if (defNode.type === 'TSInterfaceDeclaration') {
          yield defNode.body
          return
        } else if (defNode.type === 'TSTypeAliasDeclaration') {
          const typeAnnotation = defNode.typeAnnotation
          let traversedData = traversed.get(typeAnnotation)
          if (traversedData) {
            const copy = [...traversedData.nodes]
            yield* copy
            if (!traversedData.finished) {
              // Include the node because it will probably be referenced recursively.
              yield typeAnnotation
            }
            return
          }
          traversedData = { nodes: new Set(), finished: false }
          traversed.set(typeAnnotation, traversedData)
          for (const e of flattenImpl(typeAnnotation)) {
            traversedData.nodes.add(e)
          }
          traversedData.finished = true
          yield* traversedData.nodes
          return
        }
      }
    }
    yield node
  }
}

/**
 * @param {RuleContext} context The ESLint rule context object.
 * @param {TSESTreeTypeNode} node
 * @param {Set<TSESTreeTypeNode>} [checked]
 * @returns {string[]}
 */
function inferRuntimeType(context, node, checked = new Set()) {
  switch (node.type) {
    case 'TSStringKeyword':
    case 'TSTemplateLiteralType': {
      return ['String']
    }
    case 'TSNumberKeyword': {
      return ['Number']
    }
    case 'TSBooleanKeyword': {
      return ['Boolean']
    }
    case 'TSObjectKeyword': {
      return ['Object']
    }
    case 'TSTypeLiteral': {
      return inferTypeLiteralType(node)
    }
    case 'TSFunctionType': {
      return ['Function']
    }
    case 'TSArrayType':
    case 'TSTupleType': {
      return ['Array']
    }
    case 'TSSymbolKeyword': {
      return ['Symbol']
    }

    case 'TSLiteralType': {
      if (node.literal.type === 'Literal') {
        switch (typeof node.literal.value) {
          case 'boolean': {
            return ['Boolean']
          }
          case 'string': {
            return ['String']
          }
          case 'number':
          case 'bigint': {
            return ['Number']
          }
        }
        if (node.literal.value instanceof RegExp) {
          return ['RegExp']
        }
      }
      return inferRuntimeTypeFromTypeNode(
        context,
        /** @type {TypeNode} */ (node)
      )
    }
    case 'TSTypeReference': {
      if (node.typeName.type === 'Identifier') {
        const variable = findVariable(
          getScope(context, /** @type {any} */ (node)),
          node.typeName.name
        )
        if (variable && variable.defs.length === 1) {
          const defNode = /** @type {TSESTreeNode} */ (variable.defs[0].node)
          if (defNode.type === 'TSInterfaceDeclaration') {
            return [`Object`]
          }
          if (defNode.type === 'TSTypeAliasDeclaration') {
            const typeAnnotation = defNode.typeAnnotation
            if (!checked.has(typeAnnotation)) {
              checked.add(typeAnnotation)
              return inferRuntimeType(context, typeAnnotation, checked)
            }
          }
          if (defNode.type === 'TSEnumDeclaration') {
            return inferEnumType(context, defNode)
          }
        }
        for (const name of [
          node.typeName.name,
          ...(node.typeName.name.startsWith('Readonly')
            ? [node.typeName.name.slice(8)]
            : [])
        ]) {
          switch (name) {
            case 'Array':
            case 'Function':
            case 'Object':
            case 'Set':
            case 'Map':
            case 'WeakSet':
            case 'WeakMap':
            case 'Date': {
              return [name]
            }
          }
        }

        switch (node.typeName.name) {
          case 'Record':
          case 'Partial':
          case 'Readonly':
          case 'Pick':
          case 'Omit':
          case 'Required':
          case 'InstanceType': {
            return ['Object']
          }
          case 'Uppercase':
          case 'Lowercase':
          case 'Capitalize':
          case 'Uncapitalize': {
            return ['String']
          }
          case 'Parameters':
          case 'ConstructorParameters': {
            return ['Array']
          }
          case 'NonNullable': {
            const typeArguments =
              'typeArguments' in node
                ? node.typeArguments
                : /** @type {any} typescript-eslint v5 */ (node).typeParameters
            if (typeArguments && typeArguments.params[0]) {
              return inferRuntimeType(
                context,
                typeArguments.params[0],
                checked
              ).filter((t) => t !== 'null')
            }
            break
          }
          case 'Extract': {
            const typeArguments =
              'typeArguments' in node
                ? node.typeArguments
                : /** @type {any} typescript-eslint v5 */ (node).typeParameters
            if (typeArguments && typeArguments.params[1]) {
              return inferRuntimeType(context, typeArguments.params[1], checked)
            }
            break
          }
          case 'Exclude':
          case 'OmitThisParameter': {
            const typeArguments =
              'typeArguments' in node
                ? node.typeArguments
                : /** @type {any} typescript-eslint v5 */ (node).typeParameters
            if (typeArguments && typeArguments.params[0]) {
              return inferRuntimeType(context, typeArguments.params[0], checked)
            }
            break
          }
        }
      }
      return inferRuntimeTypeFromTypeNode(
        context,
        /** @type {TypeNode} */ (node)
      )
    }

    case 'TSUnionType':
    case 'TSIntersectionType': {
      return inferUnionType(node)
    }

    default: {
      return inferRuntimeTypeFromTypeNode(
        context,
        /** @type {TypeNode} */ (node)
      )
    }
  }

  /**
   * @param {import('@typescript-eslint/types').TSESTree.TSUnionType|import('@typescript-eslint/types').TSESTree.TSIntersectionType} node
   * @returns {string[]}
   */
  function inferUnionType(node) {
    const types = new Set()
    for (const t of node.types) {
      for (const tt of inferRuntimeType(context, t, checked)) {
        types.add(tt)
      }
    }
    return [...types]
  }
}

/**
 * @param {import('@typescript-eslint/types').TSESTree.TSTypeLiteral} node
 * @returns {string[]}
 */
function inferTypeLiteralType(node) {
  const types = new Set()
  for (const m of node.members) {
    switch (m.type) {
      case 'TSCallSignatureDeclaration':
      case 'TSConstructSignatureDeclaration': {
        types.add('Function')
        break
      }
      default: {
        types.add('Object')
      }
    }
  }
  return types.size > 0 ? [...types] : ['Object']
}
/**
 * @param {RuleContext} context The ESLint rule context object.
 * @param {import('@typescript-eslint/types').TSESTree.TSEnumDeclaration} node
 * @returns {string[]}
 */
function inferEnumType(context, node) {
  const types = new Set()
  for (const m of node.members) {
    if (m.initializer) {
      if (m.initializer.type === 'Literal') {
        switch (typeof m.initializer.value) {
          case 'string': {
            types.add('String')
            break
          }
          case 'number':
          case 'bigint': {
            // Now it's a syntax error.
            types.add('Number')
            break
          }
          case 'boolean': {
            // Now it's a syntax error.
            types.add('Boolean')
            break
          }
        }
      } else {
        for (const type of inferRuntimeTypeFromTypeNode(
          context,
          /** @type {Expression} */ (m.initializer)
        )) {
          types.add(type)
        }
      }
    }
  }
  return types.size > 0 ? [...types] : ['Number']
}
