'use strict';

const resolve = require('path').resolve;
const basename = require('path').basename;
const sync = require('glob').sync;
const bundleTypes = require('./bundles').bundleTypes;
const extractErrorCodes = require('../error-codes/extract-errors');

const exclude = [
  'src/**/__benchmarks__/**/*.js',
  'src/**/__tests__/**/*.js',
  'src/**/__mocks__/**/*.js',
];

const UMD_DEV = bundleTypes.UMD_DEV;
const UMD_PROD = bundleTypes.UMD_PROD;
const NODE_DEV = bundleTypes.NODE_DEV;
const NODE_PROD = bundleTypes.NODE_PROD;
const FB_DEV = bundleTypes.FB_DEV;
const FB_PROD = bundleTypes.FB_PROD;
const RN_DEV = bundleTypes.RN_DEV;
const RN_PROD = bundleTypes.RN_PROD;

const errorCodeOpts = {
  errorMapFilePath: 'scripts/error-codes/codes.json',
};

// these are the FBJS modules that are used throughout our bundles
const fbjsModules = [
  'fbjs/lib/warning',
  'fbjs/lib/invariant',
  'fbjs/lib/emptyFunction',
  'fbjs/lib/emptyObject',
  'fbjs/lib/hyphenateStyleName',
  'fbjs/lib/getUnboundedScrollPosition',
  'fbjs/lib/camelizeStyleName',
  'fbjs/lib/containsNode',
  'fbjs/lib/shallowEqual',
  'fbjs/lib/getActiveElement',
  'fbjs/lib/focusNode',
  'fbjs/lib/EventListener',
  'fbjs/lib/memoizeStringOnly',
  'fbjs/lib/ExecutionEnvironment',
  'fbjs/lib/createNodesFromMarkup',
  'fbjs/lib/performanceNow',
];

const devOnlyFilesToStubOut = [
  "'ReactDebugCurrentFrame'",
  "'ReactComponentTreeHook'",
  "'ReactPerf'",
  "'ReactTestUtils'",
];

const legacyModules = [
  'create-react-class',
  'create-react-class/factory',
  'prop-types',
  'prop-types/checkPropTypes',
];

// this function builds up a very niave Haste-like moduleMap
// that works to create up an alias map for modules to link
// up to their actual disk location so Rollup can properly
// bundle them
function createModuleMap(paths, extractErrors, bundleType) {
  const moduleMap = {};

  paths.forEach(path => {
    const files = sync(path, {ignore: exclude});

    files.forEach(file => {
      if (extractErrors) {
        extractErrors(file);
      }
      const moduleName = basename(file, '.js');

      moduleMap[moduleName] = resolve(file);
    });
  });
  // if this is FB, we want to remove ReactCurrentOwner, so we can
  // handle it with a different case
  if (bundleType === FB_DEV || bundleType === FB_PROD) {
    delete moduleMap.ReactCurrentOwner;
  }
  return moduleMap;
}

function getNodeModules(bundleType) {
  // rather than adding the rollup node resolve plugin,
  // we can instead deal with the only node module that is used
  // for UMD bundles - object-assign
  switch (bundleType) {
    case UMD_DEV:
    case UMD_PROD:
      return {
        'object-assign': resolve('./node_modules/object-assign/index.js'),
        // include the ART package modules directly by aliasing them from node_modules
        'art/modes/current': resolve('./node_modules/art/modes/current.js'),
        'art/modes/fast-noSideEffects': resolve(
          './node_modules/art/modes/fast-noSideEffects.js'
        ),
        'art/core/transform': resolve('./node_modules/art/core/transform.js'),
      };
    case NODE_DEV:
    case NODE_PROD:
    case FB_DEV:
    case FB_PROD:
    case RN_DEV:
    case RN_PROD:
      return {};
  }
}

function ignoreFBModules() {
  return [
    // At FB, we don't know them statically:
    'ReactFeatureFlags',
    'ReactDOMFeatureFlags',
    // In FB bundles, we preserve an inline require to ReactCurrentOwner.
    // See the explanation in FB version of ReactCurrentOwner in www:
    'ReactCurrentOwner',
  ];
}

function ignoreReactNativeModules() {
  return [
    // This imports NativeMethodsMixin, causing
    // a circular dependency.
    'View',
  ];
}

function getExternalModules(externals, bundleType, isRenderer) {
  // external modules tell Rollup that we should not attempt
  // to bundle these modules and instead treat them as
  // external depedencies to the bundle. so for CJS bundles
  // this means having a require("name-of-external-module") at
  // the top of the bundle. for UMD bundles this means having
  // both a require and a global check for them
  let externalModules = externals;

  switch (bundleType) {
    case UMD_DEV:
    case UMD_PROD:
      if (isRenderer) {
        externalModules.push('react');
      }
      break;
    case NODE_DEV:
    case NODE_PROD:
    case RN_DEV:
    case RN_PROD:
      fbjsModules.forEach(module => externalModules.push(module));
      externalModules.push('object-assign');

      if (isRenderer) {
        externalModules.push('react');
      }
      break;
    case FB_DEV:
    case FB_PROD:
      fbjsModules.forEach(module => externalModules.push(module));
      externalModules.push('ReactCurrentOwner');
      if (isRenderer) {
        externalModules.push('React');
        if (externalModules.indexOf('react-dom') > -1) {
          externalModules.push('ReactDOM');
        }
      }
      break;
  }
  return externalModules;
}

function getInternalModules() {
  // we tell Rollup where these files are located internally, otherwise
  // it doesn't pick them up and assumes they're external
  return {
    reactProdInvariant: resolve('./src/shared/utils/reactProdInvariant.js'),
  };
}

function getFbjsModuleAliases(bundleType) {
  switch (bundleType) {
    case UMD_DEV:
    case UMD_PROD:
      // we want to bundle these modules, so we re-alias them to the actual
      // file so Rollup can bundle them up
      const fbjsModulesAlias = {};
      fbjsModules.forEach(fbjsModule => {
        fbjsModulesAlias[fbjsModule] = resolve(`./node_modules/${fbjsModule}`);
      });
      return fbjsModulesAlias;
    case NODE_DEV:
    case NODE_PROD:
    case FB_DEV:
    case FB_PROD:
    case RN_DEV:
    case RN_PROD:
      // for FB we don't want to bundle the above modules, instead keep them
      // as external require() calls in the bundle
      return {};
  }
}

function replaceFbjsModuleAliases(bundleType) {
  switch (bundleType) {
    case FB_DEV:
    case FB_PROD:
      // Haste at FB doesn't currently allow case sensitive names,
      // and product code already uses "React". In the future,
      // we will either allow both variants or migrate to lowercase.
      return {
        "'react'": "'React'",
        "'react-dom'": "'ReactDOM'",
      };
    default:
      return {};
  }
}

const devOnlyModuleStub = `'${resolve('./scripts/rollup/shims/rollup/DevOnlyStubShim.js')}'`;

function replaceDevOnlyStubbedModules(bundleType) {
  switch (bundleType) {
    case UMD_DEV:
    case NODE_DEV:
    case FB_DEV:
    case RN_DEV:
    case RN_PROD:
      return {};
    case FB_PROD:
    case UMD_PROD:
    case NODE_PROD:
      const devOnlyModuleAliases = {};
      devOnlyFilesToStubOut.forEach(devOnlyModule => {
        devOnlyModuleAliases[devOnlyModule] = devOnlyModuleStub;
      });
      return devOnlyModuleAliases;
  }
}

function replaceLegacyModuleAliases(bundleType) {
  switch (bundleType) {
    case UMD_DEV:
    case UMD_PROD:
      const modulesAlias = {};
      legacyModules.forEach(legacyModule => {
        const modulePath = legacyModule.includes('/')
          ? legacyModule
          : `${legacyModule}/index`;
        const resolvedPath = resolve(`./node_modules/${modulePath}`);
        modulesAlias[`'${legacyModule}'`] = `'${resolvedPath}'`;
      });
      return modulesAlias;
    case NODE_DEV:
    case NODE_PROD:
    case FB_DEV:
    case FB_PROD:
    case RN_DEV:
    case RN_PROD:
      return {};
  }
}

function getAliases(paths, bundleType, isRenderer, extractErrors) {
  return Object.assign(
    createModuleMap(
      paths,
      extractErrors && extractErrorCodes(errorCodeOpts),
      bundleType
    ),
    getInternalModules(),
    getNodeModules(bundleType),
    getFbjsModuleAliases(bundleType)
  );
}

function getDefaultReplaceModules(bundleType) {
  return Object.assign(
    {},
    replaceFbjsModuleAliases(bundleType),
    replaceDevOnlyStubbedModules(bundleType),
    replaceLegacyModuleAliases(bundleType)
  );
}

function getExcludedHasteGlobs() {
  return exclude;
}

module.exports = {
  getExcludedHasteGlobs,
  getDefaultReplaceModules,
  getAliases,
  ignoreFBModules,
  ignoreReactNativeModules,
  getExternalModules,
};
