/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 *
 * @format
 */

'use strict';

const {
  throwIfConfigNotfound,
  throwIfEventEmitterEventTypeIsUnsupported,
  throwIfEventEmitterTypeIsUnsupported,
  throwIfIncorrectModuleRegistryCallArgument,
  throwIfIncorrectModuleRegistryCallTypeParameterParserError,
  throwIfModuleInterfaceIsMisnamed,
  throwIfModuleInterfaceNotFound,
  throwIfModuleTypeIsUnsupported,
  throwIfMoreThanOneCodegenNativecommands,
  throwIfMoreThanOneConfig,
  throwIfMoreThanOneModuleInterfaceParserError,
  throwIfMoreThanOneModuleRegistryCalls,
  throwIfPropertyValueTypeIsUnsupported,
  throwIfTypeAliasIsNotInterface,
  throwIfUnsupportedFunctionParamTypeAnnotationParserError,
  throwIfUnsupportedFunctionReturnTypeAnnotationParserError,
  throwIfUntypedModule,
  throwIfUnusedModuleInterfaceParserError,
  throwIfWrongNumberOfCallExpressionArgs,
} = require('./error-utils');
const {
  MissingTypeParameterGenericParserError,
  MoreThanOneTypeParameterGenericParserError,
  UnnamedFunctionParamParserError,
  UnsupportedObjectDirectRecursivePropertyParserError,
} = require('./errors');
const {
  createParserErrorCapturer,
  extractNativeModuleName,
  getConfigType,
  getSortedObject,
  isModuleRegistryCall,
  verifyPlatforms,
  visit,
} = require('./utils');
const invariant = require('invariant');

// $FlowFixMe[unclear-type] TODO(T108222691): Use flow-types for @babel/parser

function wrapModuleSchema(nativeModuleSchema, hasteModuleName) {
  return {
    modules: {
      [hasteModuleName]: nativeModuleSchema,
    },
  };
}

// $FlowFixMe[unsupported-variance-annotation]
function unwrapNullable(x) {
  if (x.type === 'NullableTypeAnnotation') {
    return [x.typeAnnotation, true];
  }
  return [x, false];
}

// $FlowFixMe[unsupported-variance-annotation]
function wrapNullable(nullable, typeAnnotation) {
  if (!nullable) {
    return typeAnnotation;
  }
  return {
    type: 'NullableTypeAnnotation',
    typeAnnotation,
  };
}
function assertGenericTypeAnnotationHasExactlyOneTypeParameter(
  moduleName,
  /**
   * TODO(T108222691): Use flow-types for @babel/parser
   */
  typeAnnotation,
  parser,
) {
  if (typeAnnotation.typeParameters == null) {
    throw new MissingTypeParameterGenericParserError(
      moduleName,
      typeAnnotation,
      parser,
    );
  }
  const typeAnnotationType = parser.typeParameterInstantiation;
  invariant(
    typeAnnotation.typeParameters.type === typeAnnotationType,
    `assertGenericTypeAnnotationHasExactlyOneTypeParameter: Type parameters must be an AST node of type '${typeAnnotationType}'`,
  );
  if (typeAnnotation.typeParameters.params.length !== 1) {
    throw new MoreThanOneTypeParameterGenericParserError(
      moduleName,
      typeAnnotation,
      parser,
    );
  }
}
function isObjectProperty(property, language) {
  switch (language) {
    case 'Flow':
      return property.type === 'ObjectTypeProperty';
    case 'TypeScript':
      return property.type === 'TSPropertySignature';
    default:
      return false;
  }
}
function getObjectTypeAnnotations(
  hasteModuleName,
  types,
  tryParse,
  translateTypeAnnotation,
  parser,
) {
  const aliasMap = {};
  Object.entries(types).forEach(([key, value]) => {
    const isTypeAlias =
      value.type === 'TypeAlias' || value.type === 'TSTypeAliasDeclaration';
    if (!isTypeAlias) {
      return;
    }
    const parent = parser.nextNodeForTypeAlias(value);
    if (
      parent.type !== 'ObjectTypeAnnotation' &&
      parent.type !== 'TSTypeLiteral'
    ) {
      return;
    }
    const typeProperties = parser
      .getAnnotatedElementProperties(value)
      .map(prop =>
        parseObjectProperty(
          parent,
          prop,
          hasteModuleName,
          types,
          aliasMap,
          {},
          // enumMap
          tryParse,
          true,
          // cxxOnly
          (prop === null || prop === void 0 ? void 0 : prop.optional) || false,
          translateTypeAnnotation,
          parser,
        ),
      );
    aliasMap[key] = {
      type: 'ObjectTypeAnnotation',
      properties: typeProperties,
    };
  });
  return aliasMap;
}
function parseObjectProperty(
  parentObject,
  property,
  hasteModuleName,
  types,
  aliasMap,
  enumMap,
  tryParse,
  cxxOnly,
  nullable,
  translateTypeAnnotation,
  parser,
) {
  const language = parser.language();
  const name = parser.getKeyName(property, hasteModuleName);
  const {optional = false} = property;
  const languageTypeAnnotation =
    language === 'TypeScript'
      ? property.typeAnnotation.typeAnnotation
      : property.value;

  // Handle recursive types
  if (parentObject) {
    var _languageTypeAnnotati, _languageTypeAnnotati2;
    const propertyType = parser.getResolveTypeAnnotationFN()(
      languageTypeAnnotation,
      types,
      parser,
    );
    if (
      propertyType.typeResolutionStatus.successful === true &&
      propertyType.typeResolutionStatus.type === 'alias' &&
      (language === 'TypeScript'
        ? parentObject.typeName &&
          parentObject.typeName.name ===
            ((_languageTypeAnnotati = languageTypeAnnotation.typeName) ===
              null || _languageTypeAnnotati === void 0
              ? void 0
              : _languageTypeAnnotati.name)
        : parentObject.id &&
          parentObject.id.name ===
            ((_languageTypeAnnotati2 = languageTypeAnnotation.id) === null ||
            _languageTypeAnnotati2 === void 0
              ? void 0
              : _languageTypeAnnotati2.name))
    ) {
      if (!optional) {
        throw new UnsupportedObjectDirectRecursivePropertyParserError(
          name,
          languageTypeAnnotation,
          hasteModuleName,
        );
      }
      return {
        name,
        optional,
        typeAnnotation: {
          type: 'TypeAliasTypeAnnotation',
          name: propertyType.typeResolutionStatus.name,
        },
      };
    }
  }

  // Handle non-recursive types
  const [propertyTypeAnnotation, isPropertyNullable] = unwrapNullable(
    translateTypeAnnotation(
      hasteModuleName,
      languageTypeAnnotation,
      types,
      aliasMap,
      enumMap,
      tryParse,
      cxxOnly,
      parser,
    ),
  );
  if (
    (propertyTypeAnnotation.type === 'FunctionTypeAnnotation' && !cxxOnly) ||
    propertyTypeAnnotation.type === 'PromiseTypeAnnotation' ||
    propertyTypeAnnotation.type === 'VoidTypeAnnotation'
  ) {
    throwIfPropertyValueTypeIsUnsupported(
      hasteModuleName,
      languageTypeAnnotation,
      property.key,
      propertyTypeAnnotation.type,
    );
  }
  return {
    name,
    optional,
    typeAnnotation: wrapNullable(isPropertyNullable, propertyTypeAnnotation),
  };
}
function translateFunctionTypeAnnotation(
  hasteModuleName,
  // TODO(T108222691): Use flow-types for @babel/parser
  // TODO(T71778680): This is a FunctionTypeAnnotation. Type this.
  functionTypeAnnotation,
  types,
  aliasMap,
  enumMap,
  tryParse,
  cxxOnly,
  translateTypeAnnotation,
  parser,
) {
  const params = [];
  for (const param of parser.getFunctionTypeAnnotationParameters(
    functionTypeAnnotation,
  )) {
    const parsedParam = tryParse(() => {
      if (parser.getFunctionNameFromParameter(param) == null) {
        throw new UnnamedFunctionParamParserError(param, hasteModuleName);
      }
      const paramName = parser.getParameterName(param);
      const [paramTypeAnnotation, isParamTypeAnnotationNullable] =
        unwrapNullable(
          translateTypeAnnotation(
            hasteModuleName,
            parser.getParameterTypeAnnotation(param),
            types,
            aliasMap,
            enumMap,
            tryParse,
            cxxOnly,
            parser,
          ),
        );
      if (
        paramTypeAnnotation.type === 'VoidTypeAnnotation' ||
        paramTypeAnnotation.type === 'PromiseTypeAnnotation'
      ) {
        return throwIfUnsupportedFunctionParamTypeAnnotationParserError(
          hasteModuleName,
          param.typeAnnotation,
          paramName,
          paramTypeAnnotation.type,
        );
      }
      return {
        name: paramName,
        optional: Boolean(param.optional),
        typeAnnotation: wrapNullable(
          isParamTypeAnnotationNullable,
          paramTypeAnnotation,
        ),
      };
    });
    if (parsedParam != null) {
      params.push(parsedParam);
    }
  }
  const [returnTypeAnnotation, isReturnTypeAnnotationNullable] = unwrapNullable(
    translateTypeAnnotation(
      hasteModuleName,
      parser.getFunctionTypeAnnotationReturnType(functionTypeAnnotation),
      types,
      aliasMap,
      enumMap,
      tryParse,
      cxxOnly,
      parser,
    ),
  );
  throwIfUnsupportedFunctionReturnTypeAnnotationParserError(
    hasteModuleName,
    functionTypeAnnotation,
    'FunctionTypeAnnotation',
    cxxOnly,
    returnTypeAnnotation.type,
  );
  return {
    type: 'FunctionTypeAnnotation',
    returnTypeAnnotation: wrapNullable(
      isReturnTypeAnnotationNullable,
      returnTypeAnnotation,
    ),
    params,
  };
}
function buildPropertySchema(
  hasteModuleName,
  // TODO(T108222691): [TS] Use flow-types for @babel/parser
  // TODO(T71778680): [Flow] This is an ObjectTypeProperty containing either:
  // - a FunctionTypeAnnotation or GenericTypeAnnotation
  // - a NullableTypeAnnoation containing a FunctionTypeAnnotation or GenericTypeAnnotation
  // Flow type this node
  property,
  types,
  aliasMap,
  enumMap,
  tryParse,
  cxxOnly,
  translateTypeAnnotation,
  parser,
) {
  let nullable = false;
  let {key, value} = property;
  const methodName = key.name;
  if (parser.language() === 'TypeScript') {
    value =
      property.type === 'TSMethodSignature'
        ? property
        : property.typeAnnotation;
  }
  const resolveTypeAnnotationFN = parser.getResolveTypeAnnotationFN();
  ({nullable, typeAnnotation: value} = resolveTypeAnnotationFN(
    value,
    types,
    parser,
  ));
  throwIfModuleTypeIsUnsupported(
    hasteModuleName,
    property.value,
    key.name,
    value.type,
    parser,
  );
  return {
    name: methodName,
    optional: Boolean(property.optional),
    typeAnnotation: wrapNullable(
      nullable,
      translateFunctionTypeAnnotation(
        hasteModuleName,
        value,
        types,
        aliasMap,
        enumMap,
        tryParse,
        cxxOnly,
        translateTypeAnnotation,
        parser,
      ),
    ),
  };
}
function buildEventEmitterSchema(
  hasteModuleName,
  // TODO(T108222691): [TS] Use flow-types for @babel/parser
  // TODO(T71778680): [Flow] This is an ObjectTypeProperty containing either:
  // - a FunctionTypeAnnotation or GenericTypeAnnotation
  // - a NullableTypeAnnoation containing a FunctionTypeAnnotation or GenericTypeAnnotation
  // Flow type this node
  property,
  types,
  aliasMap,
  enumMap,
  tryParse,
  cxxOnly,
  translateTypeAnnotation,
  parser,
) {
  const {key} = property;
  const value =
    parser.language() === 'TypeScript'
      ? property.typeAnnotation.typeAnnotation
      : property.value;
  const eventemitterName = key.name;
  const resolveTypeAnnotationFN = parser.getResolveTypeAnnotationFN();
  const [typeAnnotation, typeAnnotationNullable] = unwrapNullable(value);
  const typeAnnotationUntyped =
    value.typeParameters.params.length === 1 &&
    parser.language() === 'TypeScript'
      ? value.typeParameters.params[0].type === 'TSTypeLiteral' &&
        value.typeParameters.params[0].members.length === 0
      : value.typeParameters.params[0].type === 'ObjectTypeAnnotation' &&
        value.typeParameters.params[0].properties.length === 0;
  throwIfEventEmitterTypeIsUnsupported(
    hasteModuleName,
    key.name,
    typeAnnotation.type,
    parser,
    typeAnnotationNullable,
    typeAnnotationUntyped,
  );
  const eventTypeResolutionStatus = resolveTypeAnnotationFN(
    typeAnnotation.typeParameters.params[0],
    types,
    parser,
  );
  throwIfEventEmitterEventTypeIsUnsupported(
    hasteModuleName,
    key.name,
    eventTypeResolutionStatus.typeAnnotation,
    parser,
    eventTypeResolutionStatus.nullable,
  );
  const eventTypeAnnotation = translateTypeAnnotation(
    hasteModuleName,
    typeAnnotation.typeParameters.params[0],
    types,
    aliasMap,
    enumMap,
    tryParse,
    cxxOnly,
    parser,
  );
  return {
    name: eventemitterName,
    optional: false,
    typeAnnotation: {
      type: 'EventEmitterTypeAnnotation',
      typeAnnotation: eventTypeAnnotation,
    },
  };
}
function buildSchemaFromConfigType(
  configType,
  filename,
  ast,
  wrapComponentSchema,
  buildComponentSchema,
  buildModuleSchema,
  parser,
  translateTypeAnnotation,
) {
  switch (configType) {
    case 'component': {
      return wrapComponentSchema(buildComponentSchema(ast, parser));
    }
    case 'module': {
      if (filename === undefined || filename === null) {
        throw new Error('Filepath expected while parasing a module');
      }
      const nativeModuleName = extractNativeModuleName(filename);
      const [parsingErrors, tryParse] = createParserErrorCapturer();
      const schema = tryParse(() =>
        buildModuleSchema(
          nativeModuleName,
          ast,
          tryParse,
          parser,
          translateTypeAnnotation,
        ),
      );
      if (parsingErrors.length > 0) {
        /**
         * TODO(T77968131): We have two options:
         *  - Throw the first error, but indicate there are more then one errors.
         *  - Display all errors, nicely formatted.
         *
         * For the time being, we're just throw the first error.
         **/

        throw parsingErrors[0];
      }
      invariant(
        schema != null,
        'When there are no parsing errors, the schema should not be null',
      );
      return wrapModuleSchema(schema, nativeModuleName);
    }
    default:
      return {
        modules: {},
      };
  }
}
function buildSchema(
  contents,
  filename,
  wrapComponentSchema,
  buildComponentSchema,
  buildModuleSchema,
  Visitor,
  parser,
  translateTypeAnnotation,
) {
  // Early return for non-Spec JavaScript files
  if (
    !contents.includes('codegenNativeComponent') &&
    !contents.includes('TurboModule')
  ) {
    return {
      modules: {},
    };
  }
  const ast = parser.getAst(contents, filename);
  const configType = getConfigType(ast, Visitor);
  return buildSchemaFromConfigType(
    configType,
    filename,
    ast,
    wrapComponentSchema,
    buildComponentSchema,
    buildModuleSchema,
    parser,
    translateTypeAnnotation,
  );
}
function createComponentConfig(foundConfig, commandsTypeNames) {
  return {
    ...foundConfig,
    commandTypeName:
      commandsTypeNames[0] == null
        ? null
        : commandsTypeNames[0].commandTypeName,
    commandOptionsExpression:
      commandsTypeNames[0] == null
        ? null
        : commandsTypeNames[0].commandOptionsExpression,
  };
}
const parseModuleName = (hasteModuleName, moduleSpec, ast, parser) => {
  const callExpressions = [];
  visit(ast, {
    CallExpression(node) {
      if (isModuleRegistryCall(node)) {
        callExpressions.push(node);
      }
    },
  });
  throwIfUnusedModuleInterfaceParserError(
    hasteModuleName,
    moduleSpec,
    callExpressions,
  );
  throwIfMoreThanOneModuleRegistryCalls(
    hasteModuleName,
    callExpressions,
    callExpressions.length,
  );
  const [callExpression] = callExpressions;
  const typeParameters = parser.callExpressionTypeParameters(callExpression);
  const methodName = callExpression.callee.property.name;
  throwIfWrongNumberOfCallExpressionArgs(
    hasteModuleName,
    callExpression,
    methodName,
    callExpression.arguments.length,
  );
  throwIfIncorrectModuleRegistryCallArgument(
    hasteModuleName,
    callExpression.arguments[0],
    methodName,
  );
  const $moduleName = callExpression.arguments[0].value;
  throwIfUntypedModule(
    typeParameters,
    hasteModuleName,
    callExpression,
    methodName,
    $moduleName,
  );
  throwIfIncorrectModuleRegistryCallTypeParameterParserError(
    hasteModuleName,
    typeParameters,
    methodName,
    $moduleName,
    parser,
  );
  return $moduleName;
};
const buildModuleSchema = (
  hasteModuleName,
  ast,
  tryParse,
  parser,
  translateTypeAnnotation,
) => {
  const language = parser.language();
  const types = parser.getTypes(ast);
  const moduleSpecs = Object.values(types).filter(t =>
    parser.isModuleInterface(t),
  );
  throwIfModuleInterfaceNotFound(
    moduleSpecs.length,
    hasteModuleName,
    ast,
    language,
  );
  throwIfMoreThanOneModuleInterfaceParserError(
    hasteModuleName,
    moduleSpecs,
    language,
  );
  const [moduleSpec] = moduleSpecs;
  throwIfModuleInterfaceIsMisnamed(hasteModuleName, moduleSpec.id, language);

  // Parse Module Name
  const moduleName = parseModuleName(hasteModuleName, moduleSpec, ast, parser);

  // Some module names use platform suffix to indicate platform-exclusive modules.
  // Eventually this should be made explicit in the Flow type itself.
  // Also check the hasteModuleName for platform suffix.
  // Note: this shape is consistent with ComponentSchema.
  const {cxxOnly, excludedPlatforms} = verifyPlatforms(
    hasteModuleName,
    moduleName,
  );
  const aliasMap = cxxOnly
    ? getObjectTypeAnnotations(
        hasteModuleName,
        types,
        tryParse,
        translateTypeAnnotation,
        parser,
      )
    : {};
  const properties =
    language === 'Flow' ? moduleSpec.body.properties : moduleSpec.body.body;
  // $FlowFixMe[missing-type-arg]
  const nativeModuleSchema = properties
    .filter(
      property =>
        property.type === 'ObjectTypeProperty' ||
        property.type === 'TSPropertySignature' ||
        property.type === 'TSMethodSignature',
    )
    .map(property => {
      var _property$typeAnnotat, _property$value;
      const enumMap = {};
      const isEventEmitter =
        language === 'TypeScript'
          ? (property === null || property === void 0
              ? void 0
              : property.type) === 'TSPropertySignature' &&
            parser.getTypeAnnotationName(
              property === null ||
                property === void 0 ||
                (_property$typeAnnotat = property.typeAnnotation) === null ||
                _property$typeAnnotat === void 0
                ? void 0
                : _property$typeAnnotat.typeAnnotation,
            ) === 'EventEmitter'
          : (property === null ||
            property === void 0 ||
            (_property$value = property.value) === null ||
            _property$value === void 0
              ? void 0
              : _property$value.type) === 'GenericTypeAnnotation' &&
            parser.getTypeAnnotationName(
              property === null || property === void 0
                ? void 0
                : property.value,
            ) === 'EventEmitter';
      return tryParse(() => ({
        aliasMap,
        enumMap,
        propertyShape: isEventEmitter
          ? {
              type: 'eventEmitter',
              value: buildEventEmitterSchema(
                hasteModuleName,
                property,
                types,
                aliasMap,
                enumMap,
                tryParse,
                cxxOnly,
                translateTypeAnnotation,
                parser,
              ),
            }
          : {
              type: 'method',
              value: buildPropertySchema(
                hasteModuleName,
                property,
                types,
                aliasMap,
                enumMap,
                tryParse,
                cxxOnly,
                translateTypeAnnotation,
                parser,
              ),
            },
      }));
    })
    .filter(Boolean)
    .reduce(
      (moduleSchema, {enumMap, propertyShape}) => ({
        type: 'NativeModule',
        aliasMap: {
          ...moduleSchema.aliasMap,
          ...aliasMap,
        },
        enumMap: {
          ...moduleSchema.enumMap,
          ...enumMap,
        },
        spec: {
          eventEmitters: [...moduleSchema.spec.eventEmitters].concat(
            propertyShape.type === 'eventEmitter' ? [propertyShape.value] : [],
          ),
          methods: [...moduleSchema.spec.methods].concat(
            propertyShape.type === 'method' ? [propertyShape.value] : [],
          ),
        },
        moduleName: moduleSchema.moduleName,
        excludedPlatforms: moduleSchema.excludedPlatforms,
      }),
      {
        type: 'NativeModule',
        aliasMap: {},
        enumMap: {},
        spec: {
          eventEmitters: [],
          methods: [],
        },
        moduleName,
        excludedPlatforms:
          excludedPlatforms.length !== 0 ? [...excludedPlatforms] : undefined,
      },
    );
  return {
    type: 'NativeModule',
    aliasMap: getSortedObject(nativeModuleSchema.aliasMap),
    enumMap: getSortedObject(nativeModuleSchema.enumMap),
    spec: {
      eventEmitters: nativeModuleSchema.spec.eventEmitters.sort(),
      methods: nativeModuleSchema.spec.methods.sort(),
    },
    moduleName,
    excludedPlatforms: nativeModuleSchema.excludedPlatforms,
  };
};

/**
 * This function is used to find the type of a native component
 * provided the default exports statement from generated AST.
 * @param statement The statement to be parsed.
 * @param foundConfigs The 'mutable' array of configs that have been found.
 * @param parser The language parser to be used.
 * @returns void
 */
function findNativeComponentType(statement, foundConfigs, parser) {
  let declaration = statement.declaration;

  // codegenNativeComponent can be nested inside a cast
  // expression so we need to go one level deeper
  if (
    declaration.type === 'TSAsExpression' ||
    declaration.type === 'AsExpression' ||
    declaration.type === 'TypeCastExpression'
  ) {
    declaration = declaration.expression;
  }
  try {
    if (declaration.callee.name === 'codegenNativeComponent') {
      const typeArgumentParams =
        parser.getTypeArgumentParamsFromDeclaration(declaration);
      const funcArgumentParams = declaration.arguments;
      const nativeComponentType = parser.getNativeComponentType(
        typeArgumentParams,
        funcArgumentParams,
      );
      if (funcArgumentParams.length > 1) {
        nativeComponentType.optionsExpression = funcArgumentParams[1];
      }
      foundConfigs.push(nativeComponentType);
    }
  } catch (e) {
    // ignore
  }
}
function getCommandOptions(commandOptionsExpression) {
  if (commandOptionsExpression == null) {
    return null;
  }
  let foundOptions;
  try {
    foundOptions = commandOptionsExpression.properties.reduce(
      (options, prop) => {
        options[prop.key.name] = (
          (prop && prop.value && prop.value.elements) ||
          []
        ).map(element => element && element.value);
        return options;
      },
      {},
    );
  } catch (e) {
    throw new Error(
      'Failed to parse command options, please check that they are defined correctly',
    );
  }
  return foundOptions;
}
function getOptions(optionsExpression) {
  if (!optionsExpression) {
    return null;
  }
  let foundOptions;
  try {
    foundOptions = optionsExpression.properties.reduce((options, prop) => {
      if (prop.value.type === 'ArrayExpression') {
        options[prop.key.name] = prop.value.elements.map(
          element => element.value,
        );
      } else {
        options[prop.key.name] = prop.value.value;
      }
      return options;
    }, {});
  } catch (e) {
    throw new Error(
      'Failed to parse codegen options, please check that they are defined correctly',
    );
  }
  if (
    foundOptions.paperComponentName &&
    foundOptions.paperComponentNameDeprecated
  ) {
    throw new Error(
      'Failed to parse codegen options, cannot use both paperComponentName and paperComponentNameDeprecated',
    );
  }
  return foundOptions;
}
function getCommandTypeNameAndOptionsExpression(namedExport, parser) {
  let callExpression;
  let calleeName;
  try {
    callExpression = namedExport.declaration.declarations[0].init;
    calleeName = callExpression.callee.name;
  } catch (e) {
    return;
  }
  if (calleeName !== 'codegenNativeCommands') {
    return;
  }
  if (callExpression.arguments.length !== 1) {
    throw new Error(
      'codegenNativeCommands must be passed options including the supported commands',
    );
  }
  const typeArgumentParam =
    parser.getTypeArgumentParamsFromDeclaration(callExpression)[0];
  if (!parser.isGenericTypeAnnotation(typeArgumentParam.type)) {
    throw new Error(
      "codegenNativeCommands doesn't support inline definitions. Specify a file local type alias",
    );
  }
  return {
    commandTypeName: parser.getTypeAnnotationName(typeArgumentParam),
    commandOptionsExpression: callExpression.arguments[0],
  };
}
function propertyNames(properties) {
  return properties
    .map(property => property && property.key && property.key.name)
    .filter(Boolean);
}
function extendsForProp(prop, types, parser) {
  const argument = parser.argumentForProp(prop);
  if (!argument) {
    console.log('null', prop);
  }
  const name = parser.nameForArgument(prop);
  if (types[name] != null) {
    // This type is locally defined in the file
    return null;
  }
  switch (name) {
    case 'ViewProps':
      return {
        type: 'ReactNativeBuiltInType',
        knownTypeName: 'ReactNativeCoreViewProps',
      };
    default: {
      throw new Error(`Unable to handle prop spread: ${name}`);
    }
  }
}
function buildPropSchema(property, types, parser) {
  const getSchemaInfoFN = parser.getGetSchemaInfoFN();
  const info = getSchemaInfoFN(property, types, parser);
  if (info == null) {
    return null;
  }
  const {name, optional, typeAnnotation, defaultValue, withNullDefault} = info;
  const getTypeAnnotationFN = parser.getGetTypeAnnotationFN();
  return {
    name,
    optional,
    typeAnnotation: getTypeAnnotationFN(
      name,
      typeAnnotation,
      defaultValue,
      withNullDefault,
      types,
      parser,
      buildPropSchema,
    ),
  };
}

/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
 * LTI update could not be added via codemod */
function getEventArgument(argumentProps, parser, getPropertyType) {
  return {
    type: 'ObjectTypeAnnotation',
    properties: argumentProps.map(member =>
      buildPropertiesForEvent(member, parser, getPropertyType),
    ),
  };
}

/* $FlowFixMe[signature-verification-failure] there's no flowtype for AST.
 * TODO(T108222691): Use flow-types for @babel/parser */
function findComponentConfig(ast, parser) {
  const foundConfigs = [];
  const defaultExports = ast.body.filter(
    node => node.type === 'ExportDefaultDeclaration',
  );
  defaultExports.forEach(statement => {
    findNativeComponentType(statement, foundConfigs, parser);
  });
  throwIfConfigNotfound(foundConfigs);
  throwIfMoreThanOneConfig(foundConfigs);
  const foundConfig = foundConfigs[0];
  const namedExports = ast.body.filter(
    node => node.type === 'ExportNamedDeclaration',
  );
  const commandsTypeNames = namedExports
    .map(statement => getCommandTypeNameAndOptionsExpression(statement, parser))
    .filter(Boolean);
  throwIfMoreThanOneCodegenNativecommands(commandsTypeNames);
  return createComponentConfig(foundConfig, commandsTypeNames);
}

// $FlowFixMe[signature-verification-failure] there's no flowtype for AST
function getCommandProperties(ast, parser) {
  const {commandTypeName, commandOptionsExpression} = findComponentConfig(
    ast,
    parser,
  );
  if (commandTypeName == null) {
    return [];
  }
  const types = parser.getTypes(ast);
  const typeAlias = types[commandTypeName];
  throwIfTypeAliasIsNotInterface(typeAlias, parser);
  const properties = parser.bodyProperties(typeAlias);
  if (!properties) {
    throw new Error(
      `Failed to find type definition for "${commandTypeName}", please check that you have a valid codegen file`,
    );
  }
  const commandPropertyNames = propertyNames(properties);
  const commandOptions = getCommandOptions(commandOptionsExpression);
  if (commandOptions == null || commandOptions.supportedCommands == null) {
    throw new Error(
      'codegenNativeCommands must be given an options object with supportedCommands array',
    );
  }
  if (
    commandOptions.supportedCommands.length !== commandPropertyNames.length ||
    !commandOptions.supportedCommands.every(supportedCommand =>
      commandPropertyNames.includes(supportedCommand),
    )
  ) {
    throw new Error(
      `codegenNativeCommands expected the same supportedCommands specified in the ${commandTypeName} interface: ${commandPropertyNames.join(
        ', ',
      )}`,
    );
  }
  return properties;
}
function getTypeResolutionStatus(type, typeAnnotation, parser) {
  return {
    successful: true,
    type,
    name: parser.getTypeAnnotationName(typeAnnotation),
  };
}
function handleGenericTypeAnnotation(
  typeAnnotation,
  resolvedTypeAnnotation,
  parser,
) {
  let typeResolutionStatus;
  let node;
  switch (resolvedTypeAnnotation.type) {
    case parser.typeAlias:
    case 'OpaqueType': {
      typeResolutionStatus = getTypeResolutionStatus(
        'alias',
        typeAnnotation,
        parser,
      );
      node = parser.nextNodeForTypeAlias(resolvedTypeAnnotation);
      break;
    }
    case parser.enumDeclaration: {
      typeResolutionStatus = getTypeResolutionStatus(
        'enum',
        typeAnnotation,
        parser,
      );
      node = parser.nextNodeForEnum(resolvedTypeAnnotation);
      break;
    }
    // parser.interfaceDeclaration is not used here because for flow it should fall through to default case and throw an error
    case 'TSInterfaceDeclaration': {
      typeResolutionStatus = getTypeResolutionStatus(
        'alias',
        typeAnnotation,
        parser,
      );
      node = resolvedTypeAnnotation;
      break;
    }
    default: {
      throw new TypeError(
        parser.genericTypeAnnotationErrorMessage(resolvedTypeAnnotation),
      );
    }
  }
  return {
    typeAnnotation: node,
    typeResolutionStatus,
  };
}
function buildPropertiesForEvent(property, parser, getPropertyType) {
  const name = property.key.name;
  const optional = parser.isOptionalProperty(property);
  const typeAnnotation = parser.getTypeAnnotationFromProperty(property);
  return getPropertyType(name, optional, typeAnnotation, parser);
}
function verifyPropNotAlreadyDefined(props, needleProp) {
  const propName = needleProp.key.name;
  const foundProp = props.some(prop => prop.key.name === propName);
  if (foundProp) {
    throw new Error(`A prop was already defined with the name ${propName}`);
  }
}
function handleEventHandler(
  name,
  typeAnnotation,
  parser,
  types,
  findEventArgumentsAndType,
) {
  const eventType = name === 'BubblingEventHandler' ? 'bubble' : 'direct';
  const paperTopLevelNameDeprecated =
    parser.getPaperTopLevelNameDeprecated(typeAnnotation);
  switch (typeAnnotation.typeParameters.params[0].type) {
    case parser.nullLiteralTypeAnnotation:
    case parser.undefinedLiteralTypeAnnotation:
      return {
        argumentProps: [],
        bubblingType: eventType,
        paperTopLevelNameDeprecated,
      };
    default:
      return findEventArgumentsAndType(
        parser,
        typeAnnotation.typeParameters.params[0],
        types,
        eventType,
        paperTopLevelNameDeprecated,
      );
  }
}
function emitBuildEventSchema(
  paperTopLevelNameDeprecated,
  name,
  optional,
  nonNullableBubblingType,
  argument,
) {
  if (paperTopLevelNameDeprecated != null) {
    return {
      name,
      optional,
      bubblingType: nonNullableBubblingType,
      paperTopLevelNameDeprecated,
      typeAnnotation: {
        type: 'EventTypeAnnotation',
        argument: argument,
      },
    };
  }
  return {
    name,
    optional,
    bubblingType: nonNullableBubblingType,
    typeAnnotation: {
      type: 'EventTypeAnnotation',
      argument: argument,
    },
  };
}
module.exports = {
  wrapModuleSchema,
  unwrapNullable,
  wrapNullable,
  assertGenericTypeAnnotationHasExactlyOneTypeParameter,
  isObjectProperty,
  parseObjectProperty,
  translateFunctionTypeAnnotation,
  buildPropertySchema,
  buildSchemaFromConfigType,
  buildSchema,
  createComponentConfig,
  parseModuleName,
  buildModuleSchema,
  findNativeComponentType,
  propertyNames,
  getCommandOptions,
  getOptions,
  getCommandTypeNameAndOptionsExpression,
  extendsForProp,
  buildPropSchema,
  getEventArgument,
  findComponentConfig,
  getCommandProperties,
  handleGenericTypeAnnotation,
  getTypeResolutionStatus,
  buildPropertiesForEvent,
  verifyPropNotAlreadyDefined,
  handleEventHandler,
  emitBuildEventSchema,
};
