"use strict";

const hmrJSBundle = require("./DeltaBundler/Serializers/hmrJSBundle");
const GraphNotFoundError = require("./IncrementalBundler/GraphNotFoundError");
const RevisionNotFoundError = require("./IncrementalBundler/RevisionNotFoundError");
const debounceAsyncQueue = require("./lib/debounceAsyncQueue");
const formatBundlingError = require("./lib/formatBundlingError");
const getGraphId = require("./lib/getGraphId");
const parseOptionsFromUrl = require("./lib/parseOptionsFromUrl");
const splitBundleOptions = require("./lib/splitBundleOptions");
const transformHelpers = require("./lib/transformHelpers");
const {
  Logger: { createActionStartEntry, createActionEndEntry, log },
} = require("metro-core");
const nullthrows = require("nullthrows");
const url = require("url");
function send(sendFns, message) {
  const strMessage = JSON.stringify(message);
  sendFns.forEach((sendFn) => sendFn(strMessage));
}
class HmrServer {
  constructor(bundler, createModuleId, config) {
    this._config = config;
    this._bundler = bundler;
    this._createModuleId = createModuleId;
    this._clientGroups = new Map();
  }
  onClientConnect = async (requestUrl, sendFn) => {
    return {
      sendFn,
      revisionIds: [],
      optedIntoHMR: false,
    };
  };
  async _registerEntryPoint(client, requestUrl, sendFn) {
    requestUrl = this._config.server.rewriteRequestUrl(requestUrl);
    const clientUrl = nullthrows(url.parse(requestUrl, true));
    const options = parseOptionsFromUrl(
      requestUrl,
      new Set(this._config.resolver.platforms)
    );
    const { entryFile, resolverOptions, transformOptions, graphOptions } =
      splitBundleOptions(options);
    const resolutionFn = await transformHelpers.getResolveDependencyFn(
      this._bundler.getBundler(),
      transformOptions.platform,
      resolverOptions
    );
    const resolvedEntryFilePath = resolutionFn(
      (this._config.server.unstable_serverRoot ?? this._config.projectRoot) +
        "/.",
      {
        name: entryFile,
        data: {
          key: entryFile,
          asyncType: null,
          locs: [],
        },
      }
    ).filePath;
    const graphId = getGraphId(resolvedEntryFilePath, transformOptions, {
      resolverOptions,
      shallow: graphOptions.shallow,
      lazy: graphOptions.lazy,
      unstable_allowRequireContext:
        this._config.transformer.unstable_allowRequireContext,
    });
    const revPromise = this._bundler.getRevisionByGraphId(graphId);
    if (!revPromise) {
      send([sendFn], {
        type: "error",
        body: formatBundlingError(new GraphNotFoundError(graphId)),
      });
      return;
    }
    const { graph, id } = await revPromise;
    client.revisionIds.push(id);
    let clientGroup = this._clientGroups.get(id);
    if (clientGroup != null) {
      clientGroup.clients.add(client);
    } else {
      clientUrl.protocol = "http";
      const {
        dev,
        minify,
        runModule,
        bundleEntry: _bundleEntry,
        ...query
      } = clientUrl.query || {};
      clientUrl.query = {
        ...query,
        dev: dev || "true",
        minify: minify || "false",
        modulesOnly: "true",
        runModule: runModule || "false",
        shallow: "true",
      };
      clientUrl.search = "";
      clientGroup = {
        clients: new Set([client]),
        clientUrl,
        revisionId: id,
        graphOptions,
        unlisten: () => unlisten(),
      };
      this._clientGroups.set(id, clientGroup);
      let latestEventArgs = [];
      const debounceCallHandleFileChange = debounceAsyncQueue(async () => {
        await this._handleFileChange(
          nullthrows(clientGroup),
          {
            isInitialUpdate: false,
          },
          ...latestEventArgs
        );
      }, 50);
      const unlisten = this._bundler
        .getDeltaBundler()
        .listen(graph, async (...args) => {
          latestEventArgs = args;
          await debounceCallHandleFileChange();
        });
    }
    await this._handleFileChange(clientGroup, {
      isInitialUpdate: true,
    });
    send([sendFn], {
      type: "bundle-registered",
    });
  }
  onClientMessage = async (client, message, sendFn) => {
    let data;
    try {
      data = JSON.parse(String(message));
    } catch (error) {
      send([sendFn], {
        type: "error",
        body: formatBundlingError(error),
      });
      return Promise.resolve();
    }
    if (data && data.type) {
      switch (data.type) {
        case "register-entrypoints":
          return Promise.all(
            data.entryPoints.map((entryPoint) =>
              this._registerEntryPoint(client, entryPoint, sendFn)
            )
          );
        case "log":
          if (this._config.server.forwardClientLogs) {
            this._config.reporter.update({
              type: "client_log",
              level: data.level,
              data: data.data,
              mode: data.mode,
            });
          }
          break;
        case "log-opt-in":
          client.optedIntoHMR = true;
          break;
        default:
          break;
      }
    }
    return Promise.resolve();
  };
  onClientError = (client, e) => {
    this._config.reporter.update({
      type: "hmr_client_error",
      error: e.error,
    });
    this.onClientDisconnect(client);
  };
  onClientDisconnect = (client) => {
    client.revisionIds.forEach((revisionId) => {
      const group = this._clientGroups.get(revisionId);
      if (group != null) {
        if (group.clients.size === 1) {
          this._clientGroups.delete(revisionId);
          group.unlisten();
        } else {
          group.clients.delete(client);
        }
      }
    });
  };
  async _handleFileChange(group, options, changeEvent) {
    const logger = !options.isInitialUpdate ? changeEvent?.logger : null;
    if (logger) {
      logger.point("fileChange_end");
      logger.point("hmrPrepareAndSendMessage_start");
    }
    const optedIntoHMR = [...group.clients].some(
      (client) => client.optedIntoHMR
    );
    const processingHmrChange = log(
      createActionStartEntry({
        action_name: optedIntoHMR
          ? "Processing HMR change"
          : "Processing HMR change (no client opt-in)",
      })
    );
    const sendFns = [...group.clients].map((client) => client.sendFn);
    send(sendFns, {
      type: "update-start",
      body: options,
    });
    const message = await this._prepareMessage(group, options, changeEvent);
    send(sendFns, message);
    send(sendFns, {
      type: "update-done",
    });
    log({
      ...createActionEndEntry(processingHmrChange),
      outdated_modules:
        message.type === "update"
          ? message.body.added.length + message.body.modified.length
          : undefined,
    });
    if (logger) {
      logger.point("hmrPrepareAndSendMessage_end");
      logger.end("SUCCESS");
    }
  }
  async _prepareMessage(group, options, changeEvent) {
    const logger = !options.isInitialUpdate ? changeEvent?.logger : null;
    try {
      const revPromise = this._bundler.getRevision(group.revisionId);
      if (!revPromise) {
        return {
          type: "error",
          body: formatBundlingError(
            new RevisionNotFoundError(group.revisionId)
          ),
        };
      }
      logger?.point("updateGraph_start");
      const { revision, delta } = await this._bundler.updateGraph(
        await revPromise,
        false
      );
      logger?.point("updateGraph_end");
      this._clientGroups.delete(group.revisionId);
      group.revisionId = revision.id;
      for (const client of group.clients) {
        client.revisionIds = client.revisionIds.filter(
          (revisionId) => revisionId !== group.revisionId
        );
        client.revisionIds.push(revision.id);
      }
      this._clientGroups.set(group.revisionId, group);
      logger?.point("serialize_start");
      const hmrUpdate = hmrJSBundle(delta, revision.graph, {
        clientUrl: group.clientUrl,
        createModuleId: this._createModuleId,
        includeAsyncPaths: group.graphOptions.lazy,
        projectRoot: this._config.projectRoot,
        serverRoot:
          this._config.server.unstable_serverRoot ?? this._config.projectRoot,
      });
      logger?.point("serialize_end");
      return {
        type: "update",
        body: {
          revisionId: revision.id,
          isInitialUpdate: options.isInitialUpdate,
          ...hmrUpdate,
        },
      };
    } catch (error) {
      const formattedError = formatBundlingError(error);
      this._config.reporter.update({
        type: "bundling_error",
        error,
      });
      return {
        type: "error",
        body: formattedError,
      };
    }
  }
}
module.exports = HmrServer;
