/**
 * 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.
 *
 *  strict-local
 * @format
 */

/* eslint-env worker, serviceworker */
"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);
    });
  };
}

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

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

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

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

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

const _require = require("./bundleDB"),
  openDB = _require.openDB,
  getBundleMetadataFromDB = _require.getBundleMetadata,
  setBundleMetadata = _require.setBundleMetadata,
  removeBundleMetadata = _require.removeBundleMetadata;

const _require2 = require("./metadata"),
  fetchBundleMetadata = _require2.fetchBundleMetadata;

const _require3 = require("./response"),
  createResponse = _require3.createResponse,
  getRevisionId = _require3.getRevisionId;

class UpdateError extends Error {
  constructor(bundleUrl, originalError) {
    super(
      `Error retrieving an initial update for the bundle \`${bundleUrl}\`.`
    );
    this.stack = "Caused by: " + originalError.stack;
  }
}

function defaultGetHmrServerUrl(bundleUrl, revisionId) {
  const url = new URL(bundleUrl);
  return `${url.protocol === "https:" ? "wss" : "ws"}://${
    url.host
  }/hot?revisionId=${revisionId}`;
}

function defaultOnUpdate(clientId, update) {
  clients.get(clientId).then(client => {
    if (client != null) {
      client.postMessage({
        type: "METRO_UPDATE",
        update
      });
    }
  });
}

function defaultOnUpdateStart(clientId) {
  clients.get(clientId).then(client => {
    if (client != null) {
      client.postMessage({
        type: "METRO_UPDATE_START"
      });
    }
  });
}

function defaultOnUpdateError(clientId, error) {
  clients.get(clientId).then(client => {
    if (client != null) {
      client.postMessage({
        type: "METRO_UPDATE_ERROR",
        error
      });
    }
  });
}

const DEFAULT_DB_NAME = "__metroBundleDB";
const CACHE_VERSION = 1;
const DEFAULT_CACHE_NAME = "__metroBundleCacheV" + CACHE_VERSION;

function create() {
  let _ref =
      arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
    _ref$getHmrServerUrl = _ref.getHmrServerUrl,
    getHmrServerUrl =
      _ref$getHmrServerUrl === void 0
        ? defaultGetHmrServerUrl
        : _ref$getHmrServerUrl,
    _ref$getBundleMetadat = _ref.getBundleMetadata,
    getBundleMetadata =
      _ref$getBundleMetadat === void 0
        ? fetchBundleMetadata
        : _ref$getBundleMetadat,
    _ref$onUpdateStart = _ref.onUpdateStart,
    onUpdateStart =
      _ref$onUpdateStart === void 0 ? defaultOnUpdateStart : _ref$onUpdateStart,
    _ref$onUpdate = _ref.onUpdate,
    onUpdate = _ref$onUpdate === void 0 ? defaultOnUpdate : _ref$onUpdate,
    _ref$onUpdateError = _ref.onUpdateError,
    onUpdateError =
      _ref$onUpdateError === void 0 ? defaultOnUpdateError : _ref$onUpdateError,
    _ref$bundleCacheName = _ref.bundleCacheName,
    bundleCacheName =
      _ref$bundleCacheName === void 0
        ? DEFAULT_CACHE_NAME
        : _ref$bundleCacheName,
    _ref$bundleDBName = _ref.bundleDBName,
    bundleDBName =
      _ref$bundleDBName === void 0 ? DEFAULT_DB_NAME : _ref$bundleDBName;

  const cachePromise = caches.open(bundleCacheName);
  const dbPromise = openDB(DEFAULT_DB_NAME);
  const clients = new Map();

  const setupUpdates =
    /*#__PURE__*/
    (function() {
      var _ref2 = _asyncToGenerator(function*(
        bundleKey,
        clientId,
        prevRevisionId,
        prevBundleRes,
        prevBundleMetadataPromise
      ) {
        const cache = yield cachePromise;
        const db = yield dbPromise;
        let bundleRes = prevBundleRes;
        let revisionId = prevRevisionId;

        let bundlePromise = _asyncToGenerator(function*() {
          const stringBundle = yield prevBundleRes.clone().text();
          const prevBundleMetadata = yield prevBundleMetadataPromise;
          return stringToBundle(stringBundle, prevBundleMetadata);
        })();

        let resolveBundleRes;
        let rejectBundleRes;
        const client = {
          ids: new Set([clientId]),
          bundleResPromise: new Promise((resolve, reject) => {
            resolveBundleRes = resolve;
            rejectBundleRes = reject;
          })
        };
        clients.set(bundleKey, client);
        let resolved = false;
        const wsClient = new WebSocketHMRClient(
          getHmrServerUrl(bundleKey, prevRevisionId)
        );
        wsClient.on("connection-error", error => {
          rejectBundleRes(error);
        });
        wsClient.on("close", () => {
          clients.delete(bundleKey);
        });
        wsClient.on("error", error => {
          if (!resolved) {
            rejectBundleRes(error);
            return;
          }

          client.ids.forEach(clientId => onUpdateError(clientId, error));
        });
        wsClient.on("update-start", () => {
          client.ids.forEach(clientId => onUpdateStart(clientId));
        });
        wsClient.on(
          "update",
          /*#__PURE__*/
          (function() {
            var _ref4 = _asyncToGenerator(function*(update) {
              if (resolved) {
                // Only notify clients for later updates.
                client.ids.forEach(clientId => onUpdate(clientId, update));
              }

              let nextBundleRes;

              if (revisionId === update.revisionId) {
                nextBundleRes = bundleRes;
              } else {
                let bundle;

                try {
                  bundle = yield bundlePromise;
                } catch (error) {
                  // This error should only happen when either the initial bundle or the
                  // initial bundle metadata are invalid or cannot be retrieved.
                  rejectBundleRes(error);
                  return;
                }

                const nextBundle = patchBundle(bundle, {
                  added: update.added,
                  modified: update.modified,
                  deleted: update.deleted
                });
                bundlePromise = Promise.resolve(nextBundle);

                const _bundleToString = bundleToString(nextBundle),
                  stringBundle = _bundleToString.code,
                  metadata = _bundleToString.metadata;

                nextBundleRes = createResponse(
                  stringBundle,
                  update.revisionId,
                  new Headers({
                    // In development, we expect the bundle URL to be static. As such,
                    // the browser should always request the Service Worker for the
                    // latest version.
                    "Cache-Control": "no-cache"
                  })
                );
                cache.put(bundleKey, nextBundleRes.clone());
                setBundleMetadata(db, update.revisionId, metadata);
                removeBundleMetadata(db, revisionId);
                revisionId = update.revisionId;
              } // We need to clone the response before it can be consumed anywhere else.

              bundleRes = nextBundleRes.clone();

              if (!resolved) {
                resolved = true;
                resolveBundleRes(nextBundleRes);
              } else {
                client.bundleResPromise = Promise.resolve(nextBundleRes);
              }
            });

            return function(_x6) {
              return _ref4.apply(this, arguments);
            };
          })()
        );
        wsClient.enable();
        return client;
      });

      return function setupUpdates(_x, _x2, _x3, _x4, _x5) {
        return _ref2.apply(this, arguments);
      };
    })();

  function getOrFetchBundleMetadata(_x7, _x8) {
    return _getOrFetchBundleMetadata.apply(this, arguments);
  }

  function _getOrFetchBundleMetadata() {
    _getOrFetchBundleMetadata = _asyncToGenerator(function*(
      bundleKey,
      revisionId
    ) {
      const metadata = yield getBundleMetadataFromDB(
        yield dbPromise,
        revisionId
      );

      if (metadata != null) {
        return metadata;
      }

      return yield getBundleMetadata(bundleKey, revisionId);
    });
    return _getOrFetchBundleMetadata.apply(this, arguments);
  }

  const getBundle =
    /*#__PURE__*/
    (function() {
      var _ref5 = _asyncToGenerator(function*(bundleKey, clientId) {
        let client = clients.get(bundleKey);

        if (client != null) {
          // There's already an update client running for this bundle URL.
          client.ids.add(clientId);
        } else {
          const cache = yield cachePromise;
          const prevBundleRes = yield cache.match(bundleKey);

          if (prevBundleRes == null) {
            throw new BundleNotFoundError(bundleKey);
          }

          const prevRevisionId = getRevisionId(prevBundleRes); // We could expect metadata to always be defined. However, the cache and the
          // database can be cleared independently, which means that there is a
          // possibility that the bundle cache was cleared and the database was not
          // and vice versa.

          const prevBundleMetadataPromise = getOrFetchBundleMetadata(
            bundleKey,
            prevRevisionId
          );
          client = yield setupUpdates(
            bundleKey,
            clientId,
            prevRevisionId,
            prevBundleRes,
            prevBundleMetadataPromise
          );
        }

        let bundleRes;

        try {
          // Whenever we consume a response, we need to clone it so that we can
          // still use its body for the next request.
          bundleRes = yield client.bundleResPromise;
          client.bundleResPromise = Promise.resolve(bundleRes.clone());
        } catch (error) {
          throw new UpdateError(bundleKey, error);
        }

        return bundleRes;
      });

      return function getBundle(_x9, _x10) {
        return _ref5.apply(this, arguments);
      };
    })();

  const registerBundle =
    /*#__PURE__*/
    (function() {
      var _ref6 = _asyncToGenerator(function*(bundleKey, bundleRes, clientId) {
        const cache = yield cachePromise; // Since the user might not be aware of Response semantics, we should not
        // consume the provided response's body, but instead make clones of it.

        const initialRevisionId = getRevisionId(bundleRes);
        const putPromise = cache.put(bundleKey, bundleRes.clone()); // See the comment regarding getOrFetchBundleMetadata in getBundle.

        const metadataPromise = getOrFetchBundleMetadata(
          bundleKey,
          initialRevisionId
        );
        yield Promise.all([
          putPromise,
          _asyncToGenerator(function*() {
            const metadata = yield metadataPromise;
            yield setBundleMetadata(
              yield dbPromise,
              initialRevisionId,
              metadata
            );
          })(),
          setupUpdates(
            bundleKey,
            clientId,
            initialRevisionId,
            bundleRes.clone(),
            metadataPromise
          )
        ]);
      });

      return function registerBundle(_x11, _x12, _x13) {
        return _ref6.apply(this, arguments);
      };
    })();

  return {
    getBundle,
    registerBundle
  };
}

module.exports = {
  create,
  BundleNotFoundError,
  UpdateError,
  CACHE_VERSION
};