 /**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */
'use strict';

const AsyncTaskGroup = require('../lib/AsyncTaskGroup');
const MapWithDefaults = require('../lib/MapWithDefaults');
const debug = require('debug')('ReactNativePackager:DependencyGraph');
const util = require('util');
const path = require('../fastpath');
const realPath = require('path');
const isAbsolutePath = require('absolute-path');
const getAssetDataFromName = require('../lib/getAssetDataFromName');

class ResolutionRequest {
  constructor({
    platform,
    platforms,
    preferNativePlatform,
    entryPath,
    hasteMap,
    deprecatedAssetMap,
    helpers,
    moduleCache,
    fastfs,
    shouldThrowOnUnresolvedErrors,
    extraNodeModules,
  }) {
    this._platform = platform;
    this._platforms = platforms;
    this._preferNativePlatform = preferNativePlatform;
    this._entryPath = entryPath;
    this._hasteMap = hasteMap;
    this._deprecatedAssetMap = deprecatedAssetMap;
    this._helpers = helpers;
    this._moduleCache = moduleCache;
    this._fastfs = fastfs;
    this._shouldThrowOnUnresolvedErrors = shouldThrowOnUnresolvedErrors;
    this._extraNodeModules = extraNodeModules;
    this._resetResolutionCache();
  }

  _tryResolve(action, secondaryAction) {
    return action().catch((error) => {
      if (error.type !== 'UnableToResolveError') {
        throw error;
      }
      return secondaryAction();
    });
  }

  resolveDependency(fromModule, toModuleName) {
    const resHash = resolutionHash(fromModule.path, toModuleName);

    if (this._immediateResolutionCache[resHash]) {
      return Promise.resolve(this._immediateResolutionCache[resHash]);
    }

    const asset_DEPRECATED = this._deprecatedAssetMap.resolve(
      fromModule,
      toModuleName
    );
    if (asset_DEPRECATED) {
      return Promise.resolve(asset_DEPRECATED);
    }

    const cacheResult = (result) => {
      this._immediateResolutionCache[resHash] = result;
      return result;
    };

    const forgive = (error) => {
      if (
        error.type !== 'UnableToResolveError' ||
        this._shouldThrowOnUnresolvedErrors(this._entryPath, this._platform)
      ) {
        throw error;
      }

      debug(
        'Unable to resolve module %s from %s',
        toModuleName,
        fromModule.path
      );
      return null;
    };

    if (!this._helpers.isNodeModulesDir(fromModule.path)
        && !(isRelativeImport(toModuleName) || isAbsolutePath(toModuleName))) {
      return this._tryResolve(
        () => this._resolveHasteDependency(fromModule, toModuleName),
        () => this._resolveNodeDependency(fromModule, toModuleName)
      ).then(
        cacheResult,
        forgive,
      );
    }

    return this._resolveNodeDependency(fromModule, toModuleName)
      .then(
        cacheResult,
        forgive,
      );
  }

  getOrderedDependencies({
    response,
    mocksPattern,
    transformOptions,
    onProgress,
    recursive = true,
  }) {
    return this._getAllMocks(mocksPattern).then(allMocks => {
      const entry = this._moduleCache.getModule(this._entryPath);
      const mocks = Object.create(null);

      response.pushDependency(entry);
      let totalModules = 1;
      let finishedModules = 0;

      const resolveDependencies = module =>
        module.getDependencies(transformOptions)
          .then(dependencyNames =>
            Promise.all(
              dependencyNames.map(name => this.resolveDependency(module, name))
            ).then(dependencies => [dependencyNames, dependencies])
          );

      const addMockDependencies = !allMocks
        ? (module, result) => result
        : (module, [dependencyNames, dependencies]) => {
          const list = [module.getName()];
          const pkg = module.getPackage();
          if (pkg) {
            list.push(pkg.getName());
          }
          return Promise.all(list).then(names => {
            names.forEach(name => {
              if (allMocks[name] && !mocks[name]) {
                const mockModule = this._moduleCache.getModule(allMocks[name]);
                dependencyNames.push(name);
                dependencies.push(mockModule);
                mocks[name] = allMocks[name];
              }
            });
            return [dependencyNames, dependencies];
          });
        };

      const collectedDependencies = new MapWithDefaults(module => collect(module));
      const crawlDependencies = (mod, [depNames, dependencies]) => {
        const filteredPairs = [];

        dependencies.forEach((modDep, i) => {
          const name = depNames[i];
          if (modDep == null) {
            // It is possible to require mocks that don't have a real
            // module backing them. If a dependency cannot be found but there
            // exists a mock with the desired ID, resolve it and add it as
            // a dependency.
            if (allMocks && allMocks[name] && !mocks[name]) {
              const mockModule = this._moduleCache.getModule(allMocks[name]);
              mocks[name] = allMocks[name];
              return filteredPairs.push([name, mockModule]);
            }

            debug(
              'WARNING: Cannot find required module `%s` from module `%s`',
              name,
              mod.path
            );
            return false;
          }
          return filteredPairs.push([name, modDep]);
        });

        response.setResolvedDependencyPairs(mod, filteredPairs);

        const dependencyModules = filteredPairs.map(([, m]) => m);
        const newDependencies =
          dependencyModules.filter(m => !collectedDependencies.has(m));

        if (onProgress) {
          finishedModules += 1;
          totalModules += newDependencies.length;
          onProgress(finishedModules, totalModules);
        }

        if (recursive) {
          // doesn't block the return of this function invocation, but defers
          // the resulution of collectionsInProgress.done.then(…)
          dependencyModules
            .forEach(dependency => collectedDependencies.get(dependency));
        }
        return dependencyModules;
      };

      const collectionsInProgress = new AsyncTaskGroup();
      function collect(module) {
        collectionsInProgress.start(module);
        const result = resolveDependencies(module)
          .then(result => addMockDependencies(module, result))
          .then(result => crawlDependencies(module, result));
        const end = () => collectionsInProgress.end(module);
        result.then(end, end);
        return result;
      }

      return Promise.all([
        // kicks off recursive dependency discovery, but doesn't block until it's done
        collectedDependencies.get(entry),

        // resolves when there are no more modules resolving dependencies
        collectionsInProgress.done,
      ]).then(([rootDependencies]) => {
        return Promise.all(
          Array.from(collectedDependencies, resolveKeyWithPromise)
        ).then(moduleToDependenciesPairs =>
          [rootDependencies, new MapWithDefaults(() => [], moduleToDependenciesPairs)]
        );
      }).then(([rootDependencies, moduleDependencies]) => {
        // serialize dependencies, and make sure that every single one is only
        // included once
        const seen = new Set([entry]);
        function traverse(dependencies) {
          dependencies.forEach(dependency => {
            if (seen.has(dependency)) { return; }

            seen.add(dependency);
            response.pushDependency(dependency);
            traverse(moduleDependencies.get(dependency));
          });
        }

        traverse(rootDependencies);
        response.setMocks(mocks);
      });
    });
  }

  _getAllMocks(pattern) {
    // Take all mocks in all the roots into account. This is necessary
    // because currently mocks are global: any module can be mocked by
    // any mock in the system.
    let mocks = null;
    if (pattern) {
      mocks = Object.create(null);
      this._fastfs.matchFilesByPattern(pattern).forEach(file =>
        mocks[path.basename(file, path.extname(file))] = file
      );
    }
    return Promise.resolve(mocks);
  }

  _resolveHasteDependency(fromModule, toModuleName) {
    toModuleName = normalizePath(toModuleName);

    let p = fromModule.getPackage();
    if (p) {
      p = p.redirectRequire(toModuleName);
    } else {
      p = Promise.resolve(toModuleName);
    }

    return p.then((realModuleName) => {
      let dep = this._hasteMap.getModule(realModuleName, this._platform);
      if (dep && dep.type === 'Module') {
        return dep;
      }

      let packageName = realModuleName;
      while (packageName && packageName !== '.') {
        dep = this._hasteMap.getModule(packageName, this._platform);
        if (dep && dep.type === 'Package') {
          break;
        }
        packageName = path.dirname(packageName);
      }

      if (dep && dep.type === 'Package') {
        const potentialModulePath = path.join(
          dep.root,
          path.relative(packageName, realModuleName)
        );
        return this._tryResolve(
          () => this._loadAsFile(
            potentialModulePath,
            fromModule,
            toModuleName,
          ),
          () => this._loadAsDir(potentialModulePath, fromModule, toModuleName),
        );
      }

      throw new UnableToResolveError(
        fromModule,
        toModuleName,
        'Unable to resolve dependency',
      );
    });
  }

  _redirectRequire(fromModule, modulePath) {
    return Promise.resolve(fromModule.getPackage()).then(p => {
      if (p) {
        return p.redirectRequire(modulePath);
      }
      return modulePath;
    });
  }

  _resolveFileOrDir(fromModule, toModuleName) {
    const potentialModulePath = isAbsolutePath(toModuleName) ?
        toModuleName :
        path.join(path.dirname(fromModule.path), toModuleName);

    return this._redirectRequire(fromModule, potentialModulePath).then(
      realModuleName => {
        if (realModuleName === false) {
          return null;
        }

        return this._tryResolve(
          () => this._loadAsFile(realModuleName, fromModule, toModuleName),
          () => this._loadAsDir(realModuleName, fromModule, toModuleName)
        );
      }
    );
  }

  _resolveNodeDependency(fromModule, toModuleName) {
    if (isRelativeImport(toModuleName) || isAbsolutePath(toModuleName)) {
      return this._resolveFileOrDir(fromModule, toModuleName);
    } else {
      return this._redirectRequire(fromModule, toModuleName).then(
        realModuleName => {
          // exclude
          if (realModuleName === false) {
            return null;
          }

          if (isRelativeImport(realModuleName) || isAbsolutePath(realModuleName)) {
            // derive absolute path /.../node_modules/fromModuleDir/realModuleName
            const fromModuleParentIdx = fromModule.path.lastIndexOf('node_modules/') + 13;
            const fromModuleDir = fromModule.path.slice(0, fromModule.path.indexOf('/', fromModuleParentIdx));
            const absPath = path.join(fromModuleDir, realModuleName);
            return this._resolveFileOrDir(fromModule, absPath);
          }

          const searchQueue = [];
          for (let currDir = path.dirname(fromModule.path);
               currDir !== realPath.parse(fromModule.path).root;
               currDir = path.dirname(currDir)) {
            searchQueue.push(
              path.join(currDir, 'node_modules', realModuleName)
            );
          }

          if (this._extraNodeModules) {
            const bits = toModuleName.split('/');
            const packageName = bits[0];
            if (this._extraNodeModules[packageName]) {
              bits[0] = this._extraNodeModules[packageName];
              searchQueue.push(path.join.apply(path, bits));
            }
          }

          let p = Promise.reject(new UnableToResolveError(
            fromModule,
            toModuleName,
            'Node module not found',
          ));
          searchQueue.forEach(potentialModulePath => {
            p = this._tryResolve(
              () => this._tryResolve(
                () => p,
                () => this._loadAsFile(potentialModulePath, fromModule, toModuleName),
              ),
              () => this._loadAsDir(potentialModulePath, fromModule, toModuleName)
            );
          });

          return p;
        });
    }
  }

  _loadAsFile(potentialModulePath, fromModule, toModule) {
    return Promise.resolve().then(() => {
      if (this._helpers.isAssetFile(potentialModulePath)) {
        const dirname = path.dirname(potentialModulePath);
        if (!this._fastfs.dirExists(dirname)) {
          throw new UnableToResolveError(
            fromModule,
            toModule,
            `Directory ${dirname} doesn't exist`,
          );
        }

        const {name, type} = getAssetDataFromName(potentialModulePath, this._platforms);

        let pattern = '^' + name + '(@[\\d\\.]+x)?';
        if (this._platform != null) {
          pattern += '(\\.' + this._platform + ')?';
        }
        pattern += '\\.' + type;

        // We arbitrarly grab the first one, because scale selection
        // will happen somewhere
        const [assetFile] = this._fastfs.matches(
          dirname,
          new RegExp(pattern)
        );

        if (assetFile) {
          return this._moduleCache.getAssetModule(assetFile);
        }
      }

      let file;
      if (this._fastfs.fileExists(potentialModulePath)) {
        file = potentialModulePath;
      } else if (this._platform != null &&
                 this._fastfs.fileExists(potentialModulePath + '.' + this._platform + '.js')) {
        file = potentialModulePath + '.' + this._platform + '.js';
      } else if (this._preferNativePlatform &&
                 this._fastfs.fileExists(potentialModulePath + '.native.js')) {
        file = potentialModulePath + '.native.js';
      } else if (this._fastfs.fileExists(potentialModulePath + '.js')) {
        file = potentialModulePath + '.js';
      } else if (this._fastfs.fileExists(potentialModulePath + '.json')) {
        file = potentialModulePath + '.json';
      } else {
        throw new UnableToResolveError(
          fromModule,
          toModule,
          `File ${potentialModulePath} doesnt exist`,
        );
      }

      return this._moduleCache.getModule(file);
    });
  }

  _loadAsDir(potentialDirPath, fromModule, toModule) {
    return Promise.resolve().then(() => {
      if (!this._fastfs.dirExists(potentialDirPath)) {
        throw new UnableToResolveError(
          fromModule,
          toModule,
`Unable to find this module in its module map or any of the node_modules directories under ${potentialDirPath} and its parent directories

This might be related to https://github.com/facebook/react-native/issues/4968
To resolve try the following:
  1. Clear watchman watches: \`watchman watch-del-all\`.
  2. Delete the \`node_modules\` folder: \`rm -rf node_modules && npm install\`.
  3. Reset packager cache: \`rm -fr $TMPDIR/react-*\` or \`npm start -- --reset-cache\`.`,
        );
      }

      const packageJsonPath = path.join(potentialDirPath, 'package.json');
      if (this._fastfs.fileExists(packageJsonPath)) {
        return this._moduleCache.getPackage(packageJsonPath)
          .getMain().then(
            (main) => this._tryResolve(
              () => this._loadAsFile(main, fromModule, toModule),
              () => this._loadAsDir(main, fromModule, toModule)
            )
          );
      }

      return this._loadAsFile(
        path.join(potentialDirPath, 'index'),
        fromModule,
        toModule,
      );
    });
  }

  _resetResolutionCache() {
    this._immediateResolutionCache = Object.create(null);
  }

}


function resolutionHash(modulePath, depName) {
  return `${path.resolve(modulePath)}:${depName}`;
}


function UnableToResolveError(fromModule, toModule, message) {
  Error.call(this);
  Error.captureStackTrace(this, this.constructor);
  this.message = util.format(
    'Unable to resolve module %s from %s: %s',
    toModule,
    fromModule.path,
    message,
  );
  this.type = this.name = 'UnableToResolveError';
}

util.inherits(UnableToResolveError, Error);

function normalizePath(modulePath) {
  if (path.sep === '/') {
    modulePath = path.normalize(modulePath);
  } else if (path.posix) {
    modulePath = path.posix.normalize(modulePath);
  }

  return modulePath.replace(/\/$/, '');
}

function resolveKeyWithPromise([key, promise]) {
  return promise.then(value => [key, value]);
}

function isRelativeImport(path) {
  return /^[.][.]?[/]/.test(path);
}

module.exports = ResolutionRequest;
