import fs from 'fs';
import path from 'path';
import resolveFrom from 'resolve-from';

import { getBareExtensions } from './extensions';
import { getConfig } from '../Config';
import { ProjectConfig } from '../Config.types';

// https://github.com/facebook/create-react-app/blob/9750738cce89a967cc71f28390daf5d4311b193c/packages/react-scripts/config/paths.js#L22
export function ensureSlash(inputPath: string, needsSlash: boolean): string {
  const hasSlash = inputPath.endsWith('/');
  if (hasSlash && !needsSlash) {
    return inputPath.substr(0, inputPath.length - 1);
  } else if (!hasSlash && needsSlash) {
    return `${inputPath}/`;
  } else {
    return inputPath;
  }
}

export function getPossibleProjectRoot(): string {
  return fs.realpathSync(process.cwd());
}

const nativePlatforms = ['ios', 'android'];

export function resolveEntryPoint(
  projectRoot: string,
  { platform, projectConfig }: { platform: string; projectConfig?: Partial<ProjectConfig> }
) {
  const platforms = nativePlatforms.includes(platform) ? [platform, 'native'] : [platform];
  return getEntryPoint(projectRoot, ['./index'], platforms, projectConfig);
}

export function getEntryPoint(
  projectRoot: string,
  entryFiles: string[],
  platforms: string[],
  projectConfig?: Partial<ProjectConfig>
): string | null {
  const extensions = getBareExtensions(platforms);
  return getEntryPointWithExtensions(projectRoot, entryFiles, extensions, projectConfig);
}

// Used to resolve the main entry file for a project.
export function getEntryPointWithExtensions(
  projectRoot: string,
  entryFiles: string[],
  extensions: string[],
  projectConfig?: Partial<ProjectConfig>
): string {
  if (!projectConfig) {
    // drop all logging abilities
    const original = process.stdout.write;
    process.stdout.write = () => true;
    try {
      projectConfig = getConfig(projectRoot, { skipSDKVersionRequirement: true });
    } finally {
      process.stdout.write = original;
    }
  }

  const { pkg } = projectConfig;

  if (pkg) {
    // If the config doesn't define a custom entry then we want to look at the `package.json`s `main` field, and try again.
    const { main } = pkg;
    if (main && typeof main === 'string') {
      // Testing the main field against all of the provided extensions - for legacy reasons we can't use node module resolution as the package.json allows you to pass in a file without a relative path and expect it as a relative path.
      let entry = getFileWithExtensions(projectRoot, main, extensions);
      if (!entry) {
        // Allow for paths like: `{ "main": "expo/AppEntry" }`
        entry = resolveFromSilentWithExtensions(projectRoot, main, extensions);
        if (!entry)
          throw new Error(
            `Cannot resolve entry file: The \`main\` field defined in your \`package.json\` points to a non-existent path.`
          );
      }
      return entry;
    }
  }

  // Now we will start looking for a default entry point using the provided `entryFiles` argument.
  // This will add support for create-react-app (src/index.js) and react-native-cli (index.js) which don't define a main.
  for (const fileName of entryFiles) {
    const entry = resolveFromSilentWithExtensions(projectRoot, fileName, extensions);
    if (entry) return entry;
  }

  try {
    // If none of the default files exist then we will attempt to use the main Expo entry point.
    // This requires `expo` to be installed in the project to work as it will use `node_module/expo/AppEntry.js`
    // Doing this enables us to create a bare minimum Expo project.

    // TODO(Bacon): We may want to do a check against `./App` and `expo` in the `package.json` `dependencies` as we can more accurately ensure that the project is expo-min without needing the modules installed.
    return resolveFrom(projectRoot, 'expo/AppEntry');
  } catch {
    throw new Error(
      `The project entry file could not be resolved. Please define it in the \`main\` field of the \`package.json\`, create an \`index.js\`, or install the \`expo\` package.`
    );
  }
}

// Resolve from but with the ability to resolve like a bundler
export function resolveFromSilentWithExtensions(
  fromDirectory: string,
  moduleId: string,
  extensions: string[]
): string | null {
  for (const extension of extensions) {
    const modulePath = resolveFrom.silent(fromDirectory, `${moduleId}.${extension}`);
    if (modulePath && modulePath.endsWith(extension)) {
      return modulePath;
    }
  }
  return resolveFrom.silent(fromDirectory, moduleId) || null;
}

// Statically attempt to resolve a module but with the ability to resolve like a bundler.
// This won't use node module resolution.
export function getFileWithExtensions(
  fromDirectory: string,
  moduleId: string,
  extensions: string[]
): string | null {
  const modulePath = path.join(fromDirectory, moduleId);
  if (fs.existsSync(modulePath)) {
    return modulePath;
  }
  for (const extension of extensions) {
    const modulePath = path.join(fromDirectory, `${moduleId}.${extension}`);
    if (fs.existsSync(modulePath)) {
      return modulePath;
    }
  }
  return null;
}
