/**
 * @fileoverview Common propTypes detection functionality.
 */

'use strict';

const flatMap = require('array.prototype.flatmap');

const annotations = require('./annotations');
const propsUtil = require('./props');
const variableUtil = require('./variable');
const testFlowVersion = require('./version').testFlowVersion;
const propWrapperUtil = require('./propWrapper');
const astUtil = require('./ast');
const isFirstLetterCapitalized = require('./isFirstLetterCapitalized');

/**
 * Check if node is function type.
 * @param {ASTNode} node
 * @returns {Boolean}
 */
function isFunctionType(node) {
  if (!node) return false;
  const nodeType = node.type;
  return nodeType === 'FunctionDeclaration'
    || nodeType === 'FunctionExpression'
    || nodeType === 'ArrowFunctionExpression';
}

/**
 * Checks if we are declaring a props as a generic type in a flow-annotated class.
 *
 * @param {ASTNode} node  the AST node being checked.
 * @returns {Boolean} True if the node is a class with generic prop types, false if not.
 */
function isSuperTypeParameterPropsDeclaration(node) {
  if (node && (node.type === 'ClassDeclaration' || node.type === 'ClassExpression')) {
    if (node.superTypeParameters && node.superTypeParameters.params.length > 0) {
      return true;
    }
  }
  return false;
}

/**
 * Iterates through a properties node, like a customized forEach.
 * @param {Object} context Array of properties to iterate.
 * @param {Object[]} properties Array of properties to iterate.
 * @param {Function} fn Function to call on each property, receives property key
    and property value. (key, value) => void
  * @param {Function} [handleSpreadFn] Function to call on each ObjectTypeSpreadProperty, receives the
    argument
 */
function iterateProperties(context, properties, fn, handleSpreadFn) {
  if (properties && properties.length && typeof fn === 'function') {
    for (let i = 0, j = properties.length; i < j; i++) {
      const node = properties[i];
      const key = astUtil.getKeyValue(context, node);

      if (node.type === 'ObjectTypeSpreadProperty' && typeof handleSpreadFn === 'function') {
        handleSpreadFn(node.argument);
      }

      const value = node.value;
      fn(key, value, node);
    }
  }
}

/**
 * Checks if a node is inside a class body.
 *
 * @param {ASTNode} node  the AST node being checked.
 * @returns {Boolean} True if the node has a ClassBody ancestor, false if not.
 */
function isInsideClassBody(node) {
  let parent = node.parent;
  while (parent) {
    if (parent.type === 'ClassBody') {
      return true;
    }
    parent = parent.parent;
  }
  return false;
}

function startWithCapitalizedLetter(node) {
  return (
    node.parent.type === 'VariableDeclarator'
    && !isFirstLetterCapitalized(node.parent.id.name)
  );
}

module.exports = function propTypesInstructions(context, components, utils) {
  // Used to track the type annotations in scope.
  // Necessary because babel's scopes do not track type annotations.
  let stack = null;

  const classExpressions = [];
  const defaults = { customValidators: [] };
  const configuration = Object.assign({}, defaults, context.options[0] || {});
  const customValidators = configuration.customValidators;
  const allowedGenericTypes = new Set(['forwardRef', 'ForwardRefRenderFunction', 'VFC', 'VoidFunctionComponent', 'PropsWithChildren', 'SFC', 'StatelessComponent', 'FunctionComponent', 'FC']);
  const genericTypeParamIndexWherePropsArePresent = {
    ForwardRefRenderFunction: 1,
    forwardRef: 1,
    VoidFunctionComponent: 0,
    VFC: 0,
    PropsWithChildren: 0,
    SFC: 0,
    StatelessComponent: 0,
    FunctionComponent: 0,
    FC: 0,
  };
  const genericReactTypesImport = new Set();
  // import { FC as X } from 'react' -> localToImportedMap = { x: FC }
  const localToImportedMap = {};

  /**
   * Returns the full scope.
   * @returns {Object} The whole scope.
   */
  function typeScope() {
    return stack[stack.length - 1];
  }

  /**
   * Gets a node from the scope.
   * @param {string} key The name of the identifier to access.
   * @returns {ASTNode} The ASTNode associated with the given identifier.
   */
  function getInTypeScope(key) {
    return stack[stack.length - 1][key];
  }

  /**
   * Sets the new value in the scope.
   * @param {string} key The name of the identifier to access
   * @param {ASTNode} value The new value for the identifier.
   * @returns {ASTNode} The ASTNode associated with the given identifier.
   */
  function setInTypeScope(key, value) {
    stack[stack.length - 1][key] = value;
    return value;
  }

  /**
   * Checks if prop should be validated by plugin-react-proptypes
   * @param {String} validator Name of validator to check.
   * @returns {Boolean} True if validator should be checked by custom validator.
   */
  function hasCustomValidator(validator) {
    return customValidators.indexOf(validator) !== -1;
  }

  /* eslint-disable no-use-before-define */
  /** @type {TypeDeclarationBuilders} */
  const typeDeclarationBuilders = {
    GenericTypeAnnotation(annotation, parentName, seen) {
      if (getInTypeScope(annotation.id.name)) {
        return buildTypeAnnotationDeclarationTypes(getInTypeScope(annotation.id.name), parentName, seen);
      }
      return {};
    },

    ObjectTypeAnnotation(annotation, parentName, seen) {
      let containsUnresolvedObjectTypeSpread = false;
      let containsSpread = false;
      const containsIndexers = Boolean(annotation.indexers && annotation.indexers.length);
      const shapeTypeDefinition = {
        type: 'shape',
        children: {},
      };
      iterateProperties(
        context,
        annotation.properties,
        (childKey, childValue, propNode) => {
          const fullName = [parentName, childKey].join('.');
          if (childKey || childValue) {
            const types = buildTypeAnnotationDeclarationTypes(childValue, fullName, seen);
            types.fullName = fullName;
            types.name = childKey;
            types.node = propNode;
            types.isRequired = !childValue.optional;
            shapeTypeDefinition.children[childKey] = types;
          }
        },
        (spreadNode) => {
          const key = astUtil.getKeyValue(context, spreadNode);
          const types = buildTypeAnnotationDeclarationTypes(spreadNode, key, seen);
          if (!types.children) {
            containsUnresolvedObjectTypeSpread = true;
          } else {
            Object.assign(shapeTypeDefinition, types.children);
          }
          containsSpread = true;
        }
      );

      // Mark if this shape has spread or an indexer. We will know to consider all props from this shape as having propTypes,
      // but still have the ability to detect unused children of this shape.
      shapeTypeDefinition.containsUnresolvedSpread = containsUnresolvedObjectTypeSpread;
      shapeTypeDefinition.containsIndexers = containsIndexers;
      // Deprecated: containsSpread is not used anymore in the codebase, ensure to keep API backward compatibility
      shapeTypeDefinition.containsSpread = containsSpread;

      return shapeTypeDefinition;
    },

    UnionTypeAnnotation(annotation, parentName, seen) {
      /** @type {UnionTypeDefinition} */
      const unionTypeDefinition = {
        type: 'union',
        children: annotation.types.map((type) => buildTypeAnnotationDeclarationTypes(type, parentName, seen)),
      };
      if (unionTypeDefinition.children.length === 0) {
        // no complex type found, simply accept everything
        return {};
      }
      return unionTypeDefinition;
    },

    ArrayTypeAnnotation(annotation, parentName, seen) {
      const fullName = [parentName, '*'].join('.');
      const child = buildTypeAnnotationDeclarationTypes(annotation.elementType, fullName, seen);
      child.fullName = fullName;
      child.name = '__ANY_KEY__';
      child.node = annotation;
      return {
        type: 'object',
        children: {
          __ANY_KEY__: child,
        },
      };
    },
  };
  /* eslint-enable no-use-before-define */

  /**
   * Resolve the type annotation for a given node.
   * Flow annotations are sometimes wrapped in outer `TypeAnnotation`
   * and `NullableTypeAnnotation` nodes which obscure the annotation we're
   * interested in.
   * This method also resolves type aliases where possible.
   *
   * @param {ASTNode} node The annotation or a node containing the type annotation.
   * @returns {ASTNode} The resolved type annotation for the node.
   */
  function resolveTypeAnnotation(node) {
    let annotation = (node.left && node.left.typeAnnotation) || node.typeAnnotation || node;
    while (annotation && (annotation.type === 'TypeAnnotation' || annotation.type === 'NullableTypeAnnotation')) {
      annotation = annotation.typeAnnotation;
    }
    if (annotation.type === 'GenericTypeAnnotation' && getInTypeScope(annotation.id.name)) {
      return getInTypeScope(annotation.id.name);
    }
    return annotation;
  }

  /**
   * Creates the representation of the React props type annotation for the component.
   * The representation is used to verify nested used properties.
   * @param {ASTNode} annotation Type annotation for the props class property.
   * @param {String} parentName
   * @param {Set<ASTNode>} [seen]
   * @return {Object} The representation of the declaration, empty object means
   *    the property is declared without the need for further analysis.
   */
  function buildTypeAnnotationDeclarationTypes(annotation, parentName, seen) {
    if (typeof seen === 'undefined') {
      // Keeps track of annotations we've already seen to
      // prevent problems with recursive types.
      seen = new Set();
    }
    if (seen.has(annotation)) {
      // This must be a recursive type annotation, so just accept anything.
      return {};
    }
    seen.add(annotation);

    if (annotation.type in typeDeclarationBuilders) {
      return typeDeclarationBuilders[annotation.type](annotation, parentName, seen);
    }
    return {};
  }

  /**
   * Marks all props found inside ObjectTypeAnnotation as declared.
   *
   * Modifies the declaredProperties object
   * @param {ASTNode} propTypes
   * @param {Object} declaredPropTypes
   * @returns {Boolean} True if propTypes should be ignored (e.g. when a type can't be resolved, when it is imported)
   */
  function declarePropTypesForObjectTypeAnnotation(propTypes, declaredPropTypes) {
    let ignorePropsValidation = false;

    iterateProperties(context, propTypes.properties, (key, value, propNode) => {
      if (!value) {
        ignorePropsValidation = ignorePropsValidation || propNode.type !== 'ObjectTypeSpreadProperty';
        return;
      }

      const types = buildTypeAnnotationDeclarationTypes(value, key);
      types.fullName = key;
      types.name = key;
      types.node = propNode;
      types.isRequired = !propNode.optional;
      declaredPropTypes[key] = types;
    }, (spreadNode) => {
      const key = astUtil.getKeyValue(context, spreadNode);
      const spreadAnnotation = getInTypeScope(key);
      if (!spreadAnnotation) {
        ignorePropsValidation = true;
      } else {
        const spreadIgnoreValidation = declarePropTypesForObjectTypeAnnotation(spreadAnnotation, declaredPropTypes);
        ignorePropsValidation = ignorePropsValidation || spreadIgnoreValidation;
      }
    });

    return ignorePropsValidation;
  }

  /**
   * Marks all props found inside IntersectionTypeAnnotation as declared.
   * Since InterSectionTypeAnnotations can be nested, this handles recursively.
   *
   * Modifies the declaredPropTypes object
   * @param {ASTNode} propTypes
   * @param {Object} declaredPropTypes
   * @returns {Boolean} True if propTypes should be ignored (e.g. when a type can't be resolved, when it is imported)
   */
  function declarePropTypesForIntersectionTypeAnnotation(propTypes, declaredPropTypes) {
    return propTypes.types.some((annotation) => {
      if (annotation.type === 'ObjectTypeAnnotation') {
        return declarePropTypesForObjectTypeAnnotation(annotation, declaredPropTypes);
      }

      if (annotation.type === 'UnionTypeAnnotation') {
        return true;
      }

      // Type can't be resolved
      if (!annotation.id) {
        return true;
      }

      const typeNode = getInTypeScope(annotation.id.name);

      if (!typeNode) {
        return true;
      }
      if (typeNode.type === 'IntersectionTypeAnnotation') {
        return declarePropTypesForIntersectionTypeAnnotation(typeNode, declaredPropTypes);
      }

      return declarePropTypesForObjectTypeAnnotation(typeNode, declaredPropTypes);
    });
  }

  /**
   * Resolve node of type Identifier when building declaration types.
   * @param {ASTNode} node
   * @param {Function} callback called with the resolved value only if resolved.
   */
  function resolveValueForIdentifierNode(node, callback) {
    if (
      node
      && node.type === 'Identifier'
    ) {
      const scope = context.getScope();
      const identVariable = scope.variableScope.variables.find(
        (variable) => variable.name === node.name
      );
      if (identVariable) {
        const definition = identVariable.defs[identVariable.defs.length - 1];
        callback(definition.node.init);
      }
    }
  }

  /**
   * Creates the representation of the React propTypes for the component.
   * The representation is used to verify nested used properties.
   * @param {ASTNode} value Node of the PropTypes for the desired property
   * @param {string} parentName
   * @return {Object} The representation of the declaration, empty object means
   *    the property is declared without the need for further analysis.
   */
  function buildReactDeclarationTypes(value, parentName) {
    if (
      value
      && value.callee
      && value.callee.object
      && hasCustomValidator(value.callee.object.name)
    ) {
      return {};
    }

    let identNodeResolved = false;
    // Resolve identifier node for cases where isRequired is set in
    // the variable declaration or not at all.
    // const variableType = PropTypes.shape({ foo: ... }).isRequired
    // propTypes = {
    //   example: variableType
    // }
    // --------
    // const variableType = PropTypes.shape({ foo: ... })
    // propTypes = {
    //   example: variableType
    // }
    resolveValueForIdentifierNode(value, (newValue) => {
      identNodeResolved = true;
      value = newValue;
    });

    if (
      value
      && value.type === 'MemberExpression'
      && value.property
      && value.property.name
      && value.property.name === 'isRequired'
    ) {
      value = value.object;
    }

    // Resolve identifier node for cases where isRequired is set in
    // the prop types.
    // const variableType = PropTypes.shape({ foo: ... })
    // propTypes = {
    //   example: variableType.isRequired
    // }
    if (!identNodeResolved) {
      resolveValueForIdentifierNode(value, (newValue) => {
        value = newValue;
      });
    }

    // Verify PropTypes that are functions
    if (
      value
      && value.type === 'CallExpression'
      && value.callee
      && value.callee.property
      && value.callee.property.name
      && value.arguments
      && value.arguments.length > 0
    ) {
      const callName = value.callee.property.name;
      const argument = value.arguments[0];
      switch (callName) {
        case 'shape':
        case 'exact': {
          if (argument.type !== 'ObjectExpression') {
            // Invalid proptype or cannot analyse statically
            return {};
          }
          const shapeTypeDefinition = {
            type: callName,
            children: {},
          };
          iterateProperties(context, argument.properties, (childKey, childValue, propNode) => {
            if (childValue) { // skip spread propTypes
              const fullName = [parentName, childKey].join('.');
              const types = buildReactDeclarationTypes(childValue, fullName);
              types.fullName = fullName;
              types.name = childKey;
              types.node = propNode;
              shapeTypeDefinition.children[childKey] = types;
            }
          });
          return shapeTypeDefinition;
        }
        case 'arrayOf':
        case 'objectOf': {
          const fullName = [parentName, '*'].join('.');
          const child = buildReactDeclarationTypes(argument, fullName);
          child.fullName = fullName;
          child.name = '__ANY_KEY__';
          child.node = argument;
          return {
            type: 'object',
            children: {
              __ANY_KEY__: child,
            },
          };
        }
        case 'oneOfType': {
          if (
            !argument.elements
            || !argument.elements.length
          ) {
            // Invalid proptype or cannot analyse statically
            return {};
          }

          /** @type {UnionTypeDefinition} */
          const unionTypeDefinition = {
            type: 'union',
            children: argument.elements.map((element) => buildReactDeclarationTypes(element, parentName)),
          };
          if (unionTypeDefinition.children.length === 0) {
            // no complex type found, simply accept everything
            return {};
          }
          return unionTypeDefinition;
        }
        default:
          return {};
      }
    }
    // Unknown property or accepts everything (any, object, ...)
    return {};
  }

  function isValidReactGenericTypeAnnotation(annotation) {
    if (annotation.typeName) {
      if (annotation.typeName.name) { // if FC<Props>
        const typeName = annotation.typeName.name;
        if (!genericReactTypesImport.has(typeName)) {
          return false;
        }
      } else if (annotation.typeName.right.name) { // if React.FC<Props>
        const right = annotation.typeName.right.name;
        const left = annotation.typeName.left.name;

        if (!genericReactTypesImport.has(left) || !allowedGenericTypes.has(right)) {
          return false;
        }
      }
    }
    return true;
  }

  /**
   * Returns the left most typeName of a node, e.g: FC<Props>, React.FC<Props>
   * The representation is used to verify nested used properties.
   * @param {ASTNode} node
   * @return {string | undefined}
   */
  function getLeftMostTypeName(node) {
    if (node.name) return node.name;
    if (node.left) return getLeftMostTypeName(node.left);
  }

  function getRightMostTypeName(node) {
    if (node.name) return node.name;
    if (node.right) return getRightMostTypeName(node.right);
  }

  /**
   * Returns true if the node is either a interface or type alias declaration
   * @param {ASTNode} node
   * @return {boolean}
   */
  function filterInterfaceOrTypeAlias(node) {
    return (
      astUtil.isTSInterfaceDeclaration(node) || astUtil.isTSTypeAliasDeclaration(node)
    );
  }

  /**
   * Returns true if the interface or type alias declaration node name matches the type-name str
   * @param {ASTNode} node
   * @param {string} typeName
   * @return {boolean}
   */
  function filterInterfaceOrAliasByName(node, typeName) {
    return (
      (node.id && node.id.name === typeName)
      || (node.declaration && node.declaration.id && node.declaration.id.name === typeName)
    );
  }

  class DeclarePropTypesForTSTypeAnnotation {
    constructor(propTypes, declaredPropTypes) {
      this.propTypes = propTypes;
      this.declaredPropTypes = declaredPropTypes;
      this.foundDeclaredPropertiesList = [];
      this.referenceNameMap = new Set();
      this.sourceCode = context.getSourceCode();
      this.shouldIgnorePropTypes = false;
      this.visitTSNode(this.propTypes);
      this.endAndStructDeclaredPropTypes();
    }

    /**
     * The node will be distribute to different function.
     * @param {ASTNode} node
     */
    visitTSNode(node) {
      if (!node) return;
      if (astUtil.isTSTypeAnnotation(node)) {
        const typeAnnotation = node.typeAnnotation;
        this.visitTSNode(typeAnnotation);
      } else if (astUtil.isTSTypeReference(node)) {
        this.searchDeclarationByName(node);
      } else if (astUtil.isTSInterfaceHeritage(node)) {
        this.searchDeclarationByName(node);
      } else if (astUtil.isTSTypeLiteral(node)) {
        // Check node is an object literal
        if (Array.isArray(node.members)) {
          this.foundDeclaredPropertiesList = this.foundDeclaredPropertiesList.concat(node.members);
        }
      } else if (astUtil.isTSIntersectionType(node)) {
        this.convertIntersectionTypeToPropTypes(node);
      } else if (astUtil.isTSParenthesizedType(node)) {
        const typeAnnotation = node.typeAnnotation;
        this.visitTSNode(typeAnnotation);
      } else if (astUtil.isTSTypeParameterInstantiation(node)) {
        if (Array.isArray(node.params)) {
          node.params.forEach(this.visitTSNode, this);
        }
      } else {
        this.shouldIgnorePropTypes = true;
      }
    }

    /**
     * Search TSInterfaceDeclaration or TSTypeAliasDeclaration,
     * by using TSTypeReference and TSInterfaceHeritage name.
     * @param {ASTNode} node
     */
    searchDeclarationByName(node) {
      let typeName;
      if (astUtil.isTSTypeReference(node)) {
        typeName = node.typeName.name;
        const leftMostName = getLeftMostTypeName(node.typeName);
        const shouldTraverseTypeParams = genericReactTypesImport.has(leftMostName);
        if (shouldTraverseTypeParams && node.typeParameters && node.typeParameters.length !== 0) {
          // All react Generic types are derived from:
          // type PropsWithChildren<P> = P & { children?: ReactNode | undefined }
          // So we should construct an optional children prop
          this.shouldSpecifyOptionalChildrenProps = true;

          const rightMostName = getRightMostTypeName(node.typeName);
          const importedName = localToImportedMap[rightMostName];
          const idx = genericTypeParamIndexWherePropsArePresent[
            leftMostName !== rightMostName ? rightMostName : importedName
          ];
          const nextNode = node.typeParameters.params[idx];
          this.visitTSNode(nextNode);
          return;
        }
      } else if (astUtil.isTSInterfaceHeritage(node)) {
        if (!node.expression && node.id) {
          typeName = node.id.name;
        } else {
          typeName = node.expression.name;
        }
      }
      if (!typeName) {
        this.shouldIgnorePropTypes = true;
        return;
      }
      if (typeName === 'ReturnType') {
        this.convertReturnTypeToPropTypes(node);
        return;
      }
      // Prevent recursive inheritance will cause maximum callstack.
      if (this.referenceNameMap.has(typeName)) {
        this.shouldIgnorePropTypes = true;
        return;
      }
      // Add typeName to Set and consider it as traversed.
      this.referenceNameMap.add(typeName);

      /**
       * From line 577 to line 581, and line 588 to line 590 are trying to handle typescript-eslint-parser
       * Need to be deprecated after remove typescript-eslint-parser support.
       */
      const candidateTypes = this.sourceCode.ast.body.filter((item) => astUtil.isTSTypeDeclaration(item));

      const declarations = flatMap(
        candidateTypes,
        (type) => type.declarations || (type.declaration && type.declaration.declarations) || type.declaration);

      // we tried to find either an interface or a type with the TypeReference name
      const typeDeclaration = declarations.filter((dec) => dec.id.name === typeName);

      const interfaceDeclarations = this.sourceCode.ast.body
        .filter(filterInterfaceOrTypeAlias)
        .filter((item) => filterInterfaceOrAliasByName(item, typeName))
        .map((item) => (item.declaration || item));

      if (typeDeclaration.length !== 0) {
        typeDeclaration.map((t) => t.init || t.typeAnnotation).forEach(this.visitTSNode, this);
      } else if (interfaceDeclarations.length !== 0) {
        interfaceDeclarations.forEach(this.traverseDeclaredInterfaceOrTypeAlias, this);
      } else {
        this.shouldIgnorePropTypes = true;
      }
    }

    /**
     * Traverse TSInterfaceDeclaration and TSTypeAliasDeclaration
     * which retrieve from function searchDeclarationByName;
     * @param {ASTNode} node
     */
    traverseDeclaredInterfaceOrTypeAlias(node) {
      if (astUtil.isTSInterfaceDeclaration(node)) {
        // Handle TSInterfaceDeclaration interface Props { name: string, id: number}, should put in properties list directly;
        this.foundDeclaredPropertiesList = this.foundDeclaredPropertiesList.concat(node.body.body);
      }
      // Handle TSTypeAliasDeclaration type Props = {name:string}
      if (astUtil.isTSTypeAliasDeclaration(node)) {
        const typeAnnotation = node.typeAnnotation;
        this.visitTSNode(typeAnnotation);
      }
      if (Array.isArray(node.extends)) {
        node.extends.forEach(this.visitTSNode, this);
        // This line is trying to handle typescript-eslint-parser
        // typescript-eslint-parser extension is name as heritage
      } else if (Array.isArray(node.heritage)) {
        node.heritage.forEach(this.visitTSNode, this);
      }
    }

    convertIntersectionTypeToPropTypes(node) {
      if (!node) return;
      if (Array.isArray(node.types)) {
        node.types.forEach(this.visitTSNode, this);
      } else {
        this.shouldIgnorePropTypes = true;
      }
    }

    convertReturnTypeToPropTypes(node) {
      // ReturnType<T> should always have one parameter
      if (node.typeParameters) {
        if (node.typeParameters.params.length === 1) {
          let returnType = node.typeParameters.params[0];
          // This line is trying to handle typescript-eslint-parser
          // typescript-eslint-parser TSTypeQuery is wrapped by TSTypeReference
          if (astUtil.isTSTypeReference(returnType)) {
            returnType = returnType.typeName;
          }
          // Handle ReturnType<typeof mapStateToProps>
          if (astUtil.isTSTypeQuery(returnType)) {
            const returnTypeFunction = flatMap(this.sourceCode.ast.body
              .filter((item) => item.type === 'VariableDeclaration'
                && item.declarations.find((dec) => dec.id.name === returnType.exprName.name)
              ), (type) => type.declarations).map((dec) => dec.init);

            if (Array.isArray(returnTypeFunction)) {
              if (returnTypeFunction.length === 0) {
                // Cannot find identifier in current scope. It might be an exported type.
                this.shouldIgnorePropTypes = true;
                return;
              }
              returnTypeFunction.forEach((func) => {
                if (isFunctionType(func)) {
                  let res = func.body;
                  if (res.type === 'BlockStatement') {
                    res = astUtil.findReturnStatement(func);
                    if (res) {
                      res = res.argument;
                    }
                  }
                  switch (res.type) {
                    case 'ObjectExpression':
                      iterateProperties(context, res.properties, (key, value, propNode) => {
                        if (propNode && propNode.argument && propNode.argument.type === 'CallExpression') {
                          if (propNode.argument.typeParameters) {
                            this.visitTSNode(propNode.argument.typeParameters);
                          } else {
                            // Ignore this CallExpression return value since it doesn't have any typeParameters to let us know it's types.
                            this.shouldIgnorePropTypes = true;
                            return;
                          }
                        }
                        if (!value) {
                          this.shouldIgnorePropTypes = true;
                          return;
                        }
                        const types = buildReactDeclarationTypes(value, key);
                        types.fullName = key;
                        types.name = key;
                        types.node = propNode;
                        types.isRequired = propsUtil.isRequiredPropType(value);
                        this.declaredPropTypes[key] = types;
                      });
                      break;
                    case 'CallExpression':
                      if (res.typeParameters) {
                        this.visitTSNode(res.typeParameters);
                      } else {
                        // Ignore this CallExpression return value since it doesn't have any typeParameters to let us know it's types.
                        this.shouldIgnorePropTypes = true;
                      }
                      break;
                    default:
                  }
                }
              });
              return;
            }
          }
          // Handle ReturnType<()=>returnType>
          if (astUtil.isTSFunctionType(returnType)) {
            if (astUtil.isTSTypeAnnotation(returnType.returnType)) {
              this.visitTSNode(returnType.returnType);
              return;
            }
            // This line is trying to handle typescript-eslint-parser
            // typescript-eslint-parser TSFunction name returnType as typeAnnotation
            if (astUtil.isTSTypeAnnotation(returnType.typeAnnotation)) {
              this.visitTSNode(returnType.typeAnnotation);
              return;
            }
          }
        }
      }
      this.shouldIgnorePropTypes = true;
    }

    endAndStructDeclaredPropTypes() {
      if (this.shouldSpecifyOptionalChildrenProps) {
        this.declaredPropTypes.children = {
          fullName: 'children',
          name: 'children',
          isRequired: false,
        };
      }
      this.foundDeclaredPropertiesList.forEach((tsInterfaceBody) => {
        if (tsInterfaceBody && (tsInterfaceBody.type === 'TSPropertySignature' || tsInterfaceBody.type === 'TSMethodSignature')) {
          let accessor = 'name';
          if (tsInterfaceBody.key.type === 'Literal') {
            if (typeof tsInterfaceBody.key.value === 'number') {
              accessor = 'raw';
            } else {
              accessor = 'value';
            }
          }
          this.declaredPropTypes[tsInterfaceBody.key[accessor]] = {
            fullName: tsInterfaceBody.key[accessor],
            name: tsInterfaceBody.key[accessor],
            node: tsInterfaceBody,
            isRequired: !tsInterfaceBody.optional,
          };
        }
      });
    }
  }

  /**
   * Mark a prop type as declared
   * @param {ASTNode} node The AST node being checked.
   * @param {ASTNode} propTypes The AST node containing the proptypes
   */
  function markPropTypesAsDeclared(node, propTypes) {
    let componentNode = node;
    while (componentNode && !components.get(componentNode)) {
      componentNode = componentNode.parent;
    }
    const component = components.get(componentNode);
    let declaredPropTypes = (component && component.declaredPropTypes) || {};
    let ignorePropsValidation = (component && component.ignorePropsValidation) || false;
    switch (propTypes && propTypes.type) {
      case 'ObjectTypeAnnotation':
        ignorePropsValidation = declarePropTypesForObjectTypeAnnotation(propTypes, declaredPropTypes);
        break;
      case 'ObjectExpression':
        iterateProperties(context, propTypes.properties, (key, value, propNode) => {
          if (!value) {
            ignorePropsValidation = true;
            return;
          }
          const types = buildReactDeclarationTypes(value, key);
          types.fullName = key;
          types.name = key;
          types.node = propNode;
          types.isRequired = propsUtil.isRequiredPropType(value);
          declaredPropTypes[key] = types;
        });
        break;
      case 'MemberExpression': {
        let curDeclaredPropTypes = declaredPropTypes;
        // Walk the list of properties, until we reach the assignment
        // ie: ClassX.propTypes.a.b.c = ...
        while (
          propTypes
          && propTypes.parent
          && propTypes.parent.type !== 'AssignmentExpression'
          && propTypes.property
          && curDeclaredPropTypes
        ) {
          const propName = propTypes.property.name;
          if (propName in curDeclaredPropTypes) {
            curDeclaredPropTypes = curDeclaredPropTypes[propName].children;
            propTypes = propTypes.parent;
          } else {
            // This will crash at runtime because we haven't seen this key before
            // stop this and do not declare it
            propTypes = null;
          }
        }
        if (propTypes && propTypes.parent && propTypes.property) {
          if (!(propTypes === propTypes.parent.left && propTypes.parent.left.object)) {
            ignorePropsValidation = true;
            break;
          }
          const parentProp = context.getSource(propTypes.parent.left.object).replace(/^.*\.propTypes\./, '');
          const types = buildReactDeclarationTypes(
            propTypes.parent.right,
            parentProp
          );

          types.name = propTypes.property.name;
          types.fullName = [parentProp, propTypes.property.name].join('.');
          types.node = propTypes.parent;
          types.isRequired = propsUtil.isRequiredPropType(propTypes.parent.right);
          curDeclaredPropTypes[propTypes.property.name] = types;
        } else {
          let isUsedInPropTypes = false;
          let n = propTypes;
          while (n) {
            if (((n.type === 'AssignmentExpression') && propsUtil.isPropTypesDeclaration(n.left))
              || ((n.type === 'ClassProperty' || n.type === 'PropertyDefinition' || n.type === 'Property') && propsUtil.isPropTypesDeclaration(n))) {
              // Found a propType used inside of another propType. This is not considered usage, we'll still validate
              // this component.
              isUsedInPropTypes = true;
              break;
            }
            n = n.parent;
          }
          if (!isUsedInPropTypes) {
            ignorePropsValidation = true;
          }
        }
        break;
      }
      case 'Identifier': {
        const variablesInScope = variableUtil.variablesInScope(context);
        const firstMatchingVariable = variablesInScope
          .find((variableInScope) => variableInScope.name === propTypes.name);
        if (firstMatchingVariable) {
          const defInScope = firstMatchingVariable.defs[firstMatchingVariable.defs.length - 1];
          markPropTypesAsDeclared(node, defInScope.node && defInScope.node.init);
          return;
        }
        ignorePropsValidation = true;
        break;
      }
      case 'CallExpression': {
        if (
          propWrapperUtil.isPropWrapperFunction(
            context,
            context.getSourceCode().getText(propTypes.callee)
          )
          && propTypes.arguments && propTypes.arguments[0]
        ) {
          markPropTypesAsDeclared(node, propTypes.arguments[0]);
          return;
        }
        break;
      }
      case 'IntersectionTypeAnnotation':
        ignorePropsValidation = declarePropTypesForIntersectionTypeAnnotation(propTypes, declaredPropTypes);
        break;
      case 'GenericTypeAnnotation':
        if (propTypes.id.name === '$ReadOnly') {
          ignorePropsValidation = declarePropTypesForObjectTypeAnnotation(
            propTypes.typeParameters.params[0],
            declaredPropTypes
          );
        } else {
          ignorePropsValidation = true;
        }
        break;
      case 'TSTypeReference':
      case 'TSTypeAnnotation': {
        const tsTypeAnnotation = new DeclarePropTypesForTSTypeAnnotation(propTypes, declaredPropTypes);
        ignorePropsValidation = tsTypeAnnotation.shouldIgnorePropTypes;
        declaredPropTypes = tsTypeAnnotation.declaredPropTypes;
      }
        break;
      case null:
        break;
      default:
        ignorePropsValidation = true;
        break;
    }

    components.set(node, {
      declaredPropTypes,
      ignorePropsValidation,
    });
  }

  /**
   * @param {ASTNode} node We expect either an ArrowFunctionExpression,
   *   FunctionDeclaration, or FunctionExpression
   */
  function markAnnotatedFunctionArgumentsAsDeclared(node) {
    if (!node.params || !node.params.length) {
      return;
    }

    if (
      node.parent
      && node.parent.callee
      && node.parent.typeParameters
      && node.parent.typeParameters.params
      && (
        node.parent.callee.name === 'forwardRef' || (
          node.parent.callee.object
          && node.parent.callee.property
          && node.parent.callee.object.name === 'React'
          && node.parent.callee.property.name === 'forwardRef'
        )
      )
    ) {
      const propTypes = node.parent.typeParameters.params[1];
      const declaredPropTypes = {};
      const obj = new DeclarePropTypesForTSTypeAnnotation(propTypes, declaredPropTypes);
      components.set(node, {
        declaredPropTypes: obj.declaredPropTypes,
        ignorePropsValidation: obj.shouldIgnorePropTypes,
      });
      return;
    }

    const siblingIdentifier = node.parent && node.parent.id;
    const siblingHasTypeAnnotation = siblingIdentifier && siblingIdentifier.typeAnnotation;
    const isNodeAnnotated = annotations.isAnnotatedFunctionPropsDeclaration(node, context);

    if (!isNodeAnnotated && !siblingHasTypeAnnotation) {
      return;
    }

    // https://github.com/jsx-eslint/eslint-plugin-react/issues/2784
    if (isInsideClassBody(node) && !astUtil.isFunction(node)) {
      return;
    }

    // Should ignore function that not return JSXElement
    if (!utils.isReturningJSXOrNull(node) || startWithCapitalizedLetter(node)) {
      return;
    }

    if (isNodeAnnotated) {
      const param = node.params[0];
      if (param.typeAnnotation && param.typeAnnotation.typeAnnotation && param.typeAnnotation.typeAnnotation.type === 'UnionTypeAnnotation') {
        param.typeAnnotation.typeAnnotation.types.forEach((annotation) => {
          if (annotation.type === 'GenericTypeAnnotation') {
            markPropTypesAsDeclared(node, resolveTypeAnnotation(annotation));
          } else {
            markPropTypesAsDeclared(node, annotation);
          }
        });
      } else {
        markPropTypesAsDeclared(node, resolveTypeAnnotation(param));
      }
    } else {
      // implements what's discussed here: https://github.com/jsx-eslint/eslint-plugin-react/issues/2777#issuecomment-683944481
      const annotation = siblingIdentifier.typeAnnotation.typeAnnotation;

      if (
        annotation
        && annotation.type !== 'TSTypeReference'
        && annotation.typeParameters == null
      ) {
        return;
      }

      if (!isValidReactGenericTypeAnnotation(annotation)) return;

      markPropTypesAsDeclared(node, resolveTypeAnnotation(siblingIdentifier));
    }
  }

  /**
   * Resolve the type annotation for a given class declaration node with superTypeParameters.
   *
   * @param {ASTNode} node The annotation or a node containing the type annotation.
   * @returns {ASTNode} The resolved type annotation for the node.
   */
  function resolveSuperParameterPropsType(node) {
    let propsParameterPosition;
    try {
      // Flow <=0.52 had 3 required TypedParameters of which the second one is the Props.
      // Flow >=0.53 has 2 optional TypedParameters of which the first one is the Props.
      propsParameterPosition = testFlowVersion(context, '>= 0.53.0') ? 0 : 1;
    } catch (e) {
      // In case there is no flow version defined, we can safely assume that when there are 3 Props we are dealing with version <= 0.52
      propsParameterPosition = node.superTypeParameters.params.length <= 2 ? 0 : 1;
    }

    let annotation = node.superTypeParameters.params[propsParameterPosition];
    while (annotation && (annotation.type === 'TypeAnnotation' || annotation.type === 'NullableTypeAnnotation')) {
      annotation = annotation.typeAnnotation;
    }

    if (annotation && annotation.type === 'GenericTypeAnnotation' && getInTypeScope(annotation.id.name)) {
      return getInTypeScope(annotation.id.name);
    }
    return annotation;
  }

  /**
   * Checks if we are declaring a `props` class property with a flow type annotation.
   * @param {ASTNode} node The AST node being checked.
   * @returns {Boolean} True if the node is a type annotated props declaration, false if not.
   */
  function isAnnotatedClassPropsDeclaration(node) {
    if (node && (node.type === 'ClassProperty' || node.type === 'PropertyDefinition')) {
      const tokens = context.getFirstTokens(node, 2);
      if (
        node.typeAnnotation && (
          tokens[0].value === 'props'
          || (tokens[1] && tokens[1].value === 'props')
        )
      ) {
        return true;
      }
    }
    return false;
  }

  return {
    ClassExpression(node) {
      // TypeParameterDeclaration need to be added to typeScope in order to handle ClassExpressions.
      // This visitor is executed before TypeParameterDeclaration are scoped, therefore we postpone
      // processing class expressions until when the program exists.
      classExpressions.push(node);
    },

    ClassDeclaration(node) {
      if (isSuperTypeParameterPropsDeclaration(node)) {
        markPropTypesAsDeclared(node, resolveSuperParameterPropsType(node));
      }
    },

    'ClassProperty, PropertyDefinition'(node) {
      if (isAnnotatedClassPropsDeclaration(node)) {
        markPropTypesAsDeclared(node, resolveTypeAnnotation(node));
      } else if (propsUtil.isPropTypesDeclaration(node)) {
        markPropTypesAsDeclared(node, node.value);
      }
    },

    ObjectExpression(node) {
      // Search for the proptypes declaration
      node.properties.forEach((property) => {
        if (!propsUtil.isPropTypesDeclaration(property)) {
          return;
        }
        markPropTypesAsDeclared(node, property.value);
      });
    },

    FunctionExpression(node) {
      if (node.parent.type !== 'MethodDefinition') {
        markAnnotatedFunctionArgumentsAsDeclared(node);
      }
    },

    ImportDeclaration(node) {
      // parse `import ... from 'react`
      if (node.source.value === 'react') {
        node.specifiers.forEach((specifier) => {
          if (
            // handles import * as X from 'react'
            specifier.type === 'ImportNamespaceSpecifier'
            // handles import React from 'react'
            || specifier.type === 'ImportDefaultSpecifier'
          ) {
            genericReactTypesImport.add(specifier.local.name);
          }

          // handles import { FC } from 'react' or import { FC as X } from 'react'
          if (specifier.type === 'ImportSpecifier' && allowedGenericTypes.has(specifier.imported.name)) {
            genericReactTypesImport.add(specifier.local.name);
            localToImportedMap[specifier.local.name] = specifier.imported.name;
          }
        });
      }
    },

    FunctionDeclaration: markAnnotatedFunctionArgumentsAsDeclared,

    ArrowFunctionExpression: markAnnotatedFunctionArgumentsAsDeclared,

    MemberExpression(node) {
      if (propsUtil.isPropTypesDeclaration(node)) {
        const component = utils.getRelatedComponent(node);
        if (!component) {
          return;
        }
        try {
          markPropTypesAsDeclared(component.node, node.parent.right || node.parent);
        } catch (e) {
          if (e.constructor !== RangeError) { throw e; }
        }
      }
    },

    MethodDefinition(node) {
      if (!node.static || node.kind !== 'get' || !propsUtil.isPropTypesDeclaration(node)) {
        return;
      }

      let i = node.value.body.body.length - 1;
      for (; i >= 0; i--) {
        if (node.value.body.body[i].type === 'ReturnStatement') {
          break;
        }
      }

      if (i >= 0) {
        markPropTypesAsDeclared(node, node.value.body.body[i].argument);
      }
    },

    TypeAlias(node) {
      setInTypeScope(node.id.name, node.right);
    },

    TypeParameterDeclaration(node) {
      const identifier = node.params[0];

      if (identifier.typeAnnotation) {
        setInTypeScope(identifier.name, identifier.typeAnnotation.typeAnnotation);
      }
    },

    Program() {
      stack = [{}];
    },

    BlockStatement() {
      stack.push(Object.create(typeScope()));
    },

    'BlockStatement:exit'() {
      stack.pop();
    },

    'Program:exit'() {
      classExpressions.forEach((node) => {
        if (isSuperTypeParameterPropsDeclaration(node)) {
          markPropTypesAsDeclared(node, resolveSuperParameterPropsType(node));
        }
      });
    },
  };
};
