/**
 * Copyright (c) Facebook, Inc. and its 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";

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
  try {
    var info = gen[key](arg);
    var value = info.value;
  } catch (error) {
    reject(error);
    return;
  }
  if (info.done) {
    resolve(value);
  } else {
    Promise.resolve(value).then(_next, _throw);
  }
}

function _asyncToGenerator(fn) {
  return function() {
    var self = this,
      args = arguments;
    return new Promise(function(resolve, reject) {
      var gen = fn.apply(self, args);
      function _next(value) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
      }
      function _throw(err) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
      }
      _next(undefined);
    });
  };
}

function _defineProperty(obj, key, value) {
  if (key in obj) {
    Object.defineProperty(obj, key, {
      value: value,
      enumerable: true,
      configurable: true,
      writable: true
    });
  } else {
    obj[key] = value;
  }
  return obj;
}

const AssetResolutionCache = require("./AssetResolutionCache");

const DependencyGraphHelpers = require("./DependencyGraph/DependencyGraphHelpers");

const JestHasteMap = require("jest-haste-map");

const Module = require("./Module");

const ModuleCache = require("./ModuleCache");

const ResolutionRequest = require("./DependencyGraph/ResolutionRequest");

const fs = require("fs");

const path = require("path");

const _require = require("./DependencyGraph/ModuleResolution"),
  ModuleResolver = _require.ModuleResolver;

const _require2 = require("events"),
  EventEmitter = _require2.EventEmitter;

const _require3 = require("metro-core"),
  _require3$Logger = _require3.Logger,
  createActionStartEntry = _require3$Logger.createActionStartEntry,
  createActionEndEntry = _require3$Logger.createActionEndEntry,
  log = _require3$Logger.log;

const JEST_HASTE_MAP_CACHE_BREAKER = 4;

class DependencyGraph extends EventEmitter {
  constructor(_ref) {
    let config = _ref.config,
      haste = _ref.haste,
      initialHasteFS = _ref.initialHasteFS,
      initialModuleMap = _ref.initialModuleMap;
    super();

    _defineProperty(this, "_doesFileExist", filePath => {
      return this._hasteFS.exists(filePath);
    });

    this._config = config;
    this._assetResolutionCache = new AssetResolutionCache({
      assetExtensions: new Set(config.resolver.assetExts),
      getDirFiles: dirPath => fs.readdirSync(dirPath),
      platforms: new Set(config.resolver.platforms)
    });
    this._haste = haste;
    this._hasteFS = initialHasteFS;
    this._moduleMap = initialModuleMap;
    this._helpers = new DependencyGraphHelpers({
      assetExts: config.resolver.assetExts,
      providesModuleNodeModules: config.resolver.providesModuleNodeModules
    });

    this._haste.on("change", this._onHasteChange.bind(this));

    this._moduleCache = this._createModuleCache();

    this._createModuleResolver();
  }

  static _createHaste(config) {
    return new JestHasteMap({
      computeDependencies: false,
      computeSha1: true,
      extensions: config.resolver.sourceExts.concat(config.resolver.assetExts),
      forceNodeFilesystemAPI: !config.resolver.useWatchman,
      hasteImplModulePath: config.resolver.hasteImplModulePath,
      ignorePattern: config.resolver.blacklistRE || / ^/,
      mapper: config.resolver.virtualMapper,
      maxWorkers: config.maxWorkers,
      mocksPattern: "",
      name: "metro-" + JEST_HASTE_MAP_CACHE_BREAKER,
      platforms: config.resolver.platforms,
      providesModuleNodeModules: config.resolver.providesModuleNodeModules,
      retainAllFiles: true,
      resetCache: config.resetCache,
      rootDir: config.projectRoot,
      roots: config.watchFolders,
      throwOnModuleCollision: true,
      useWatchman: config.resolver.useWatchman,
      watch: true
    });
  }

  static load(config) {
    return _asyncToGenerator(function*() {
      const initializingMetroLogEntry = log(
        createActionStartEntry("Initializing Metro")
      );
      config.reporter.update({
        type: "dep_graph_loading"
      });

      const haste = DependencyGraph._createHaste(config);

      const _ref2 = yield haste.build(),
        hasteFS = _ref2.hasteFS,
        moduleMap = _ref2.moduleMap;

      log(createActionEndEntry(initializingMetroLogEntry));
      config.reporter.update({
        type: "dep_graph_loaded"
      });
      return new DependencyGraph({
        haste,
        initialHasteFS: hasteFS,
        initialModuleMap: moduleMap,
        config
      });
    })();
  }

  _getClosestPackage(filePath) {
    const parsedPath = path.parse(filePath);
    const root = parsedPath.root;
    let dir = parsedPath.dir;

    do {
      const candidate = path.join(dir, "package.json");

      if (this._hasteFS.exists(candidate)) {
        return candidate;
      }

      dir = path.dirname(dir);
    } while (dir !== "." && dir !== root);

    return null;
  }

  _onHasteChange(_ref3) {
    let eventsQueue = _ref3.eventsQueue,
      hasteFS = _ref3.hasteFS,
      moduleMap = _ref3.moduleMap;
    this._hasteFS = hasteFS;

    this._assetResolutionCache.clear();

    this._moduleMap = moduleMap;
    eventsQueue.forEach(_ref4 => {
      let type = _ref4.type,
        filePath = _ref4.filePath;
      return this._moduleCache.processFileChange(type, filePath);
    });

    this._createModuleResolver();

    this.emit("change");
  }

  _createModuleResolver() {
    this._moduleResolver = new ModuleResolver({
      allowPnp: this._config.resolver.allowPnp,
      dirExists: filePath => {
        try {
          return fs.lstatSync(filePath).isDirectory();
        } catch (e) {}

        return false;
      },
      doesFileExist: this._doesFileExist,
      extraNodeModules: this._config.resolver.extraNodeModules,
      isAssetFile: filePath => this._helpers.isAssetFile(filePath),
      mainFields: this._config.resolver.resolverMainFields,
      moduleCache: this._moduleCache,
      moduleMap: this._moduleMap,
      preferNativePlatform: true,
      resolveAsset: (dirPath, assetName, platform) =>
        this._assetResolutionCache.resolve(dirPath, assetName, platform),
      resolveRequest: this._config.resolver.resolveRequest,
      sourceExts: this._config.resolver.sourceExts
    });
  }

  _createModuleCache() {
    return new ModuleCache({
      getClosestPackage: this._getClosestPackage.bind(this)
    });
  }

  getSha1(filename) {
    // TODO If it looks like we're trying to get the sha1 from a file located
    // within a Zip archive, then we instead compute the sha1 for what looks
    // like the Zip archive itself.
    const splitIndex = filename.indexOf(".zip/");
    const containerName =
      splitIndex !== -1 ? filename.slice(0, splitIndex + 4) : filename; // TODO Calling realpath allows us to get a hash for a given path even when
    // it's a symlink to a file, which prevents Metro from crashing in such a
    // case. However, it doesn't allow Metro to track changes to the target file
    // of the symlink. We should fix this by implementing a symlink map into
    // Metro (or maybe by implementing those "extra transformation sources" we've
    // been talking about for stuff like CSS or WASM).

    const resolvedPath = fs.realpathSync(containerName);

    const sha1 = this._hasteFS.getSha1(resolvedPath);

    if (!sha1) {
      throw new ReferenceError(
        `SHA-1 for file ${filename} (${resolvedPath}) is not computed`
      );
    }

    return sha1;
  }

  getWatcher() {
    return this._haste;
  }

  end() {
    this._haste.end();
  }

  resolveDependency(from, to, platform) {
    const req = new ResolutionRequest({
      moduleResolver: this._moduleResolver,
      entryPath: from,
      helpers: this._helpers,
      platform: platform || null,
      moduleCache: this._moduleCache
    });
    return req.resolveDependency(this._moduleCache.getModule(from), to).path;
  }

  getHasteName(filePath) {
    const hasteName = this._hasteFS.getModuleName(filePath);

    if (hasteName) {
      return hasteName;
    }

    return path.relative(this._config.projectRoot, filePath);
  }
}

module.exports = DependencyGraph;