/**
 * 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 _objectSpread(target) {
  for (var i = 1; i < arguments.length; i++) {
    var source = arguments[i] != null ? arguments[i] : {};
    var ownKeys = Object.keys(source);
    if (typeof Object.getOwnPropertySymbols === "function") {
      ownKeys = ownKeys.concat(
        Object.getOwnPropertySymbols(source).filter(function(sym) {
          return Object.getOwnPropertyDescriptor(source, sym).enumerable;
        })
      );
    }
    ownKeys.forEach(function(key) {
      _defineProperty(target, key, source[key]);
    });
  }
  return target;
}

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 Bundler = require("./Bundler");

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

const ResourceNotFoundError = require("./IncrementalBundler/ResourceNotFoundError");

const crypto = require("crypto");

const fs = require("fs");

const getGraphId = require("./lib/getGraphId");

const getPrependedScripts = require("./lib/getPrependedScripts");

const path = require("path");

const transformHelpers = require("./lib/transformHelpers");

function createRevisionId() {
  return crypto.randomBytes(8).toString("hex");
}

function revisionIdFromString(str) {
  return str;
}

class IncrementalBundler {
  constructor(config) {
    _defineProperty(this, "_revisionsById", new Map());

    _defineProperty(this, "_revisionsByGraphId", new Map());

    this._config = config;
    this._bundler = new Bundler(config);
    this._deltaBundler = new DeltaBundler(this._bundler);
  }

  end() {
    this._deltaBundler.end();

    this._bundler.end();
  }

  getBundler() {
    return this._bundler;
  }

  getDeltaBundler() {
    return this._deltaBundler;
  }

  getRevision(revisionId) {
    return this._revisionsById.get(revisionId);
  }

  getRevisionByGraphId(graphId) {
    return this._revisionsByGraphId.get(graphId);
  }

  buildGraphForEntries(entryFiles, transformOptions) {
    var _this = this;

    let otherOptions =
      arguments.length > 2 && arguments[2] !== undefined
        ? arguments[2]
        : {
            onProgress: null
          };
    return _asyncToGenerator(function*() {
      const absoluteEntryFiles = entryFiles.map(entryFile =>
        path.resolve(_this._config.projectRoot, entryFile)
      );
      yield Promise.all(
        absoluteEntryFiles.map(
          entryFile =>
            new Promise((resolve, reject) => {
              // This should throw an error if the file doesn't exist.
              // Using this instead of fs.exists to account for SimLinks.
              fs.realpath(entryFile, err => {
                if (err) {
                  reject(new ResourceNotFoundError(entryFile));
                } else {
                  resolve();
                }
              });
            })
        )
      );
      const graph = yield _this._deltaBundler.buildGraph(absoluteEntryFiles, {
        resolve: yield transformHelpers.getResolveDependencyFn(
          _this._bundler,
          transformOptions.platform
        ),
        transform: yield transformHelpers.getTransformFn(
          absoluteEntryFiles,
          _this._bundler,
          _this._deltaBundler,
          _this._config,
          transformOptions
        ),
        onProgress: otherOptions.onProgress
      });

      _this._config.serializer.experimentalSerializerHook(graph, {
        added: graph.dependencies,
        modified: new Map(),
        deleted: new Set(),
        reset: true
      });

      return graph;
    })();
  }

  buildGraph(entryFile, transformOptions) {
    var _this2 = this;

    let otherOptions =
      arguments.length > 2 && arguments[2] !== undefined
        ? arguments[2]
        : {
            onProgress: null
          };
    return _asyncToGenerator(function*() {
      const graph = yield _this2.buildGraphForEntries(
        [entryFile],
        transformOptions,
        otherOptions
      );
      const transformOptionsWithoutType = {
        customTransformOptions: transformOptions.customTransformOptions,
        dev: transformOptions.dev,
        experimentalImportSupport: transformOptions.experimentalImportSupport,
        hot: transformOptions.hot,
        minify: transformOptions.minify,
        platform: transformOptions.platform
      };
      const prepend = yield getPrependedScripts(
        _this2._config,
        transformOptionsWithoutType,
        _this2._bundler,
        _this2._deltaBundler
      );
      return {
        prepend,
        graph
      };
    })();
  } // TODO T34760750 (alexkirsz) Eventually, I'd like to get to a point where
  // this class exposes only initializeGraph and updateGraph.

  initializeGraph(entryFile, transformOptions) {
    var _this3 = this;

    let otherOptions =
      arguments.length > 2 && arguments[2] !== undefined
        ? arguments[2]
        : {
            onProgress: null
          };
    return _asyncToGenerator(function*() {
      const graphId = getGraphId(entryFile, transformOptions);
      const revisionId = createRevisionId();

      const revisionPromise = _asyncToGenerator(function*() {
        const _ref2 = yield _this3.buildGraph(
            entryFile,
            transformOptions,
            otherOptions
          ),
          graph = _ref2.graph,
          prepend = _ref2.prepend;

        return {
          id: revisionId,
          date: new Date(),
          graphId,
          graph,
          prepend
        };
      })();

      _this3._revisionsById.set(revisionId, revisionPromise);

      _this3._revisionsByGraphId.set(graphId, revisionPromise);

      const revision = yield revisionPromise;
      const delta = {
        added: revision.graph.dependencies,
        modified: new Map(),
        deleted: new Set(),
        reset: true
      };
      return {
        revision,
        delta
      };
    })();
  }

  updateGraph(revision, reset) {
    var _this4 = this;

    return _asyncToGenerator(function*() {
      const delta = yield _this4._deltaBundler.getDelta(revision.graph, {
        reset
      });

      _this4._config.serializer.experimentalSerializerHook(
        revision.graph,
        delta
      );

      if (
        delta.added.size > 0 ||
        delta.modified.size > 0 ||
        delta.deleted.size > 0
      ) {
        _this4._revisionsById.delete(revision.id);

        revision = _objectSpread({}, revision, {
          // Generate a new revision id, to be used to verify the next incremental
          // request.
          id: crypto.randomBytes(8).toString("hex"),
          date: new Date()
        });
        const revisionPromise = Promise.resolve(revision);

        _this4._revisionsById.set(revision.id, revisionPromise);

        _this4._revisionsByGraphId.set(revision.graphId, revisionPromise);
      }

      return {
        revision,
        delta
      };
    })();
  }
}

_defineProperty(
  IncrementalBundler,
  "revisionIdFromString",
  revisionIdFromString
);

module.exports = IncrementalBundler;