/**
 * 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.
 *
 * @flow strict-local
 * @format
 * @oncall react_native
 */

'use strict';

import type Bundler from '../Bundler';
import type DeltaBundler, {TransformFn} from '../DeltaBundler';
import type {
  BundlerResolution,
  TransformInputOptions,
  TransformResultDependency,
} from '../DeltaBundler/types.flow';
import type {TransformOptions} from '../DeltaBundler/Worker';
import type {ResolverInputOptions} from '../shared/types.flow';
import type {RequireContext} from './contextModule';
import type {ConfigT} from 'metro-config/src/configTypes.flow';
import type {Type} from 'metro-transform-worker';

import {getContextModuleTemplate} from './contextModuleTemplates';
import isAssetFile from 'metro-resolver/src/utils/isAssetFile';

type InlineRequiresRaw = {+blockList: {[string]: true, ...}, ...} | boolean;

type TransformOptionsWithRawInlines = {
  ...TransformOptions,
  +inlineRequires: InlineRequiresRaw,
};

const baseIgnoredInlineRequires = [
  'React',
  'react',
  'react/jsx-dev-runtime',
  'react/jsx-runtime',
  'react-native',
];

async function calcTransformerOptions(
  entryFiles: $ReadOnlyArray<string>,
  bundler: Bundler,
  deltaBundler: DeltaBundler<>,
  config: ConfigT,
  options: TransformInputOptions,
  resolverOptions: ResolverInputOptions,
): Promise<TransformOptionsWithRawInlines> {
  const baseOptions = {
    customTransformOptions: options.customTransformOptions,
    dev: options.dev,
    hot: options.hot,
    inlineRequires: false,
    inlinePlatform: true,
    minify: options.minify,
    platform: options.platform,
    unstable_transformProfile: options.unstable_transformProfile,
  };

  // When we're processing scripts, we don't need to calculate any
  // inlineRequires information, since scripts by definition don't have
  // requires().
  if (options.type === 'script') {
    return {
      ...baseOptions,
      type: 'script',
    };
  }

  const getDependencies = async (path: string) => {
    const dependencies = await deltaBundler.getDependencies([path], {
      resolve: await getResolveDependencyFn(
        bundler,
        options.platform,
        resolverOptions,
      ),
      transform: await getTransformFn(
        [path],
        bundler,
        deltaBundler,
        config,
        {
          ...options,
          minify: false,
        },
        resolverOptions,
      ),
      transformOptions: options,
      onProgress: null,
      lazy: false,
      unstable_allowRequireContext:
        config.transformer.unstable_allowRequireContext,
      unstable_enablePackageExports:
        config.resolver.unstable_enablePackageExports,
      shallow: false,
    });

    return Array.from(dependencies.keys());
  };

  const {transform} = await config.transformer.getTransformOptions(
    entryFiles,
    {dev: options.dev, hot: options.hot, platform: options.platform},
    getDependencies,
  );

  return {
    ...baseOptions,
    inlineRequires: transform?.inlineRequires || false,
    experimentalImportSupport: transform?.experimentalImportSupport || false,
    unstable_disableES6Transforms:
      transform?.unstable_disableES6Transforms || false,
    nonInlinedRequires:
      transform?.nonInlinedRequires || baseIgnoredInlineRequires,
    type: 'module',
  };
}

function removeInlineRequiresBlockListFromOptions(
  path: string,
  inlineRequires: InlineRequiresRaw,
): boolean {
  if (typeof inlineRequires === 'object') {
    return !(path in inlineRequires.blockList);
  }

  return inlineRequires;
}

async function getTransformFn(
  entryFiles: $ReadOnlyArray<string>,
  bundler: Bundler,
  deltaBundler: DeltaBundler<>,
  config: ConfigT,
  options: TransformInputOptions,
  resolverOptions: ResolverInputOptions,
): Promise<TransformFn<>> {
  const {inlineRequires, ...transformOptions} = await calcTransformerOptions(
    entryFiles,
    bundler,
    deltaBundler,
    config,
    options,
    resolverOptions,
  );
  const assetExts = new Set(config.resolver.assetExts);

  return async (modulePath: string, requireContext: ?RequireContext) => {
    let templateBuffer: Buffer;

    if (requireContext) {
      const graph = await bundler.getDependencyGraph();

      // TODO: Check delta changes to avoid having to look over all files each time
      // this is a massive performance boost.

      // Search against all files in a subtree.
      const files = Array.from(
        graph.matchFilesWithContext(requireContext.from, {
          filter: requireContext.filter,
          recursive: requireContext.recursive,
        }),
      );

      const template = getContextModuleTemplate(
        requireContext.mode,
        requireContext.from,
        files,
      );

      templateBuffer = Buffer.from(template);
    }

    return await bundler.transformFile(
      modulePath,
      {
        ...transformOptions,
        type: getType(transformOptions.type, modulePath, assetExts),
        inlineRequires: removeInlineRequiresBlockListFromOptions(
          modulePath,
          inlineRequires,
        ),
      },
      templateBuffer,
    );
  };
}

function getType(
  type: string,
  filePath: string,
  assetExts: $ReadOnlySet<string>,
): Type {
  if (type === 'script') {
    return type;
  }

  if (isAssetFile(filePath, assetExts)) {
    return 'asset';
  }

  return 'module';
}

async function getResolveDependencyFn(
  bundler: Bundler,
  platform: ?string,
  resolverOptions: ResolverInputOptions,
): Promise<
  (from: string, dependency: TransformResultDependency) => BundlerResolution,
> {
  const dependencyGraph = await await bundler.getDependencyGraph();

  return (from: string, dependency: TransformResultDependency) =>
    dependencyGraph.resolveDependency(
      from,
      dependency,
      platform ?? null,
      resolverOptions,
    );
}

module.exports = {
  getTransformFn,
  getResolveDependencyFn,
};