"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
Object.defineProperty(exports, "ERROR_LOG_FILE", {
  enumerable: true,
  get: function () {
    return _config.ERROR_LOG_FILE;
  }
});
Object.defineProperty(exports, "STATES", {
  enumerable: true,
  get: function () {
    return _constants.STATES;
  }
});
Object.defineProperty(exports, "UPLOADS_DIR", {
  enumerable: true,
  get: function () {
    return _config.UPLOADS_DIR;
  }
});
Object.defineProperty(exports, "WORK_DIR", {
  enumerable: true,
  get: function () {
    return _config.WORK_DIR;
  }
});
exports.default = void 0;
exports.extractBundledAssets = extractBundledAssets;
exports.getActiveServer = getActiveServer;
exports.getActiveServerId = void 0;
Object.defineProperty(exports, "resolveAssetsPath", {
  enumerable: true,
  get: function () {
    return _utils.resolveAssetsPath;
  }
});
var _reactNative = require("react-native");
var _reactNativeFs = require("@dr.pogodin/react-native-fs");
var _jsUtils = require("@dr.pogodin/js-utils");
var _config = require("./config.js");
var _constants = require("./constants.js");
var _ReactNativeStaticServer = _interopRequireDefault(require("./ReactNativeStaticServer.js"));
var _utils = require("./utils.js");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
// ID-to-StaticServer map for all potentially active server instances,
// used to route native events back to JS server objects.
const servers = {};
const nativeEventEmitter = new _reactNative.NativeEventEmitter(_ReactNativeStaticServer.default);
const LOOPBACK_ADDRESS = '127.0.0.1';
nativeEventEmitter.addListener('RNStaticServer', ({
  serverId,
  event,
  details
}) => {
  const group = servers[serverId];
  if (group) {
    switch (event) {
      case _constants.SIGNALS.CRASHED:
        // TODO: We probably can, and should, capture the native stack trace
        // and pass it along with the error.
        const error = Error(details);
        group.forEach(item => item._setState(_constants.STATES.CRASHED, details, error));

        // TODO: Should we do here the following?
        // delete servers[this._id];
        break;
      default:
        throw Error(`Unexpected signal ${event}`);
    }
  }
});

// TODO: The idea for later implementation is to allow users to provide their
// own lighttpd config files with completely custom configuration. To do so,
// we'll probably split StaticServer class in two: the BaseServer will run
// server with given config path, and it will contain the logic like server
// state watching, stopInBackground, etc. The StaticServer class will extend it,
// allowing the current interface (starting the server with default config,
// and providing the most important config options via arguments), it will
// hold the props for accesing those options, selected hostname and port, etc.
// (as BaseServer will run the server on whatever port is given in provided
// config, but it won't be able to tell user what address / port it was, etc.).
class StaticServer {
  // NOTE: could be a private method, and we tried it, but it turns out that
  // Babel's @babel/plugin-proposal-private-methods causes many troubles in RN.
  // See: https://github.com/birdofpreyru/react-native-static-server/issues/6
  // and: https://github.com/birdofpreyru/react-native-static-server/issues/9

  _hostname = '';

  /* DEPRECATED */

  _origin = '';
  _stateChangeEmitter = new _jsUtils.Emitter();

  // TODO: It will be better to use UUID, but I believe "uuid" library
  // I would use won't work in RN without additional workarounds applied
  // to RN setup to get around some issues with randombytes support in
  // RN JS engine. Anyway, it should be double-checked later, but using
  // timestamps as ID will do for now.

  // It is used to serialize state change requests, thus ensuring that parallel
  // requests to start / stop the server won't result in a corrupt state.
  _sem = new _jsUtils.Semaphore(true);
  get errorLog() {
    return this._errorLog || false;
  }
  get fileDir() {
    return this._fileDir;
  }
  get hostname() {
    return this._hostname;
  }
  get id() {
    return this._id;
  }

  /** @deprecated */
  get nonLocal() {
    return this._nonLocal;
  }
  get origin() {
    return this._origin;
  }
  get stopInBackground() {
    return this._stopInBackground;
  }
  get port() {
    return this._port;
  }
  get state() {
    return this._state;
  }
  _setState(neu, details = '', error) {
    this._state = neu;
    this._stateChangeEmitter.emit(neu, details, error);
  }

  /**
   * Creates a new Server instance.
   */
  constructor({
    errorLog = false,
    extraConfig = '',
    fileDir,
    hostname,
    // NOTE: For some reasons RN-Windows corrupts large numbers sent
    // as event arguments across JS / Native boundary.
    // Everything smaller than 65535 seems to work fine, so let's just
    // truncate these IDs for now.
    // See: https://github.com/microsoft/react-native-windows/issues/11322
    id = Date.now() % 65535,
    /* DEPRECATED */nonLocal = false,
    port = 0,
    state = _constants.STATES.INACTIVE,
    stopInBackground = false,
    /* DEPRECATED */webdav
  }) {
    if (errorLog) this._errorLog = errorLog === true ? {} : errorLog;
    this._extraConfig = extraConfig;
    this._id = id;
    this._nonLocal = nonLocal;
    this._hostname = hostname || (nonLocal ? '' : LOOPBACK_ADDRESS);
    this._port = port;
    this._stopInBackground = stopInBackground;
    this._state = state;

    // NOTE: Normally, a server instance is connected to events from the native
    // side inside its .start() call, and it is disconnected from them inside
    // its .stop() call (or crash clean-up sequence). However, if the server is
    // created with a claim that it is already active, we should connect it to
    // the events rigth away here.
    switch (state) {
      case _constants.STATES.ACTIVE:
      case _constants.STATES.STARTING:
        {
          this._registerSelf();
          break;
        }
      default:
    }
    if (!fileDir) throw Error('`fileDir` MUST BE a non-empty string');
    this._fileDir = (0, _utils.resolveAssetsPath)(fileDir);
    this._webdav = webdav;
  }
  addStateListener(listener) {
    return this._stateChangeEmitter.addListener(listener);
  }
  _configureAppStateHandling() {
    if (this._stopInBackground) {
      if (!this._appStateSub) {
        this._appStateSub = _reactNative.AppState.addEventListener('change', this._handleAppStateChange.bind(this));
      }
    } else if (this._appStateSub) {
      this._appStateSub.remove();
      this._appStateSub = undefined;
    }
  }
  async _removeConfigFile() {
    if (this._configPath) {
      const p = this._configPath;

      // Resetting the field prior to the async unlink attempt is safer,
      // in case the caller does not await for this method to complete.
      this._configPath = undefined;
      try {
        await (0, _reactNativeFs.unlink)(p);
      } catch {
        // IGNORE
      }
    }
  }
  _registerSelf() {
    let group = servers[this._id];
    if (group) group.add(this);else {
      group = new Set([this]);
      servers[this._id] = group;
    }
  }

  /**
   * This method throws if server is not in a "stable" state, i.e. not in one
   * of these: ACTIVE, CRASHED, INACTIVE.
   */
  _stableStateGuard() {
    switch (this._state) {
      case _constants.STATES.ACTIVE:
      case _constants.STATES.CRASHED:
      case _constants.STATES.INACTIVE:
        return;
      default:
        throw Error(`Server is in unstable state ${this._state}`);
    }
  }

  /**
   * Removes all state listeners connected to this server instance.
   */
  removeAllStateListeners() {
    this._stateChangeEmitter.removeAllListeners();
  }

  /**
   * Removes given state listener, if it is connected to this server instance;
   * or does nothing if the listener is not connected to it.
   * @param listener
   */
  removeStateListener(listener) {
    this._stateChangeEmitter.removeListener(listener);
  }

  /**
   * @param {string} [details] Optional. If provided, it will be added
   * to the STARTING message emitted to the server state change listeners.
   * @returns {Promise<string>}
   */
  async start(details) {
    try {
      await this._sem.seize();
      this._stableStateGuard();
      if (this._state === _constants.STATES.ACTIVE) return this._origin;
      this._registerSelf();
      this._setState(_constants.STATES.STARTING, details);
      this._configureAppStateHandling();

      // NOTE: This is done at the first start only, to avoid hostname changes
      // when server is paused for background and automatically reactivated
      // later. Same for automatic port selection.
      if (!this._hostname) {
        this._hostname = await _ReactNativeStaticServer.default.getLocalIpAddress();
      }
      if (!this._port) {
        this._port = await _ReactNativeStaticServer.default.getOpenPort(this._hostname);
      }
      this._origin = `http://${this._hostname}:${this._port}`;
      await this._removeConfigFile();
      this._configPath = await (0, _config.newStandardConfigFile)({
        errorLog: this._errorLog,
        extraConfig: this._extraConfig,
        fileDir: this._fileDir,
        hostname: this._hostname,
        port: this._port,
        webdav: this._webdav
      });

      // Native implementations of .start() method must resolve only once
      // the server has been launched (ready to handle incoming requests).
      await _ReactNativeStaticServer.default.start(this._id, this._configPath, this._errorLog ? _config.ERROR_LOG_FILE : '');
      this._setState(_constants.STATES.ACTIVE);
      this._removeConfigFile();
      return this._origin;
    } catch (e) {
      const error = e instanceof Error ? e : Error(e.message, {
        cause: e
      });
      this._setState(_constants.STATES.CRASHED, error.message, error);
      throw error;
    } finally {
      this._sem.setReady(true);
    }
  }

  /**
   * Soft-stop the server: if automatic pause for background is enabled,
   * it will be automatically reactivated when the app goes into foreground
   * again. End users are expected to use public .stop() method below, which
   * additionally cleans-up that automatic re-activation.
   * @param {string} [details] Optional. If provided, it will be added
   *  to the STOPPING message emitted to the server state change listeners.
   * @returns {Promise<>}
   */
  async _stop(details) {
    try {
      await this._sem.seize();
      this._stableStateGuard();
      if (this._state !== _constants.STATES.ACTIVE) return;
      this._setState(_constants.STATES.STOPPING, details);

      // Native implementations of .stop() method must resolve only once
      // the server has been completely shut down (released the port it listens).
      await _ReactNativeStaticServer.default.stop();
      this._setState(_constants.STATES.INACTIVE);
    } catch (e) {
      const error = e instanceof Error ? e : Error(e.message, {
        cause: e
      });
      this._setState(_constants.STATES.CRASHED, error.message, error);
      throw error;
    } finally {
      const set = servers[this._id];
      set?.delete(this);
      if (!set?.size) delete servers[this._id];
      this._sem.setReady(true);
    }
  }

  /**
   * Stops or pauses the server, if it is running, depending on whether it was
   * started with keepAlive option or not (note, keepAlive means that server is
   * not paused / restarted when the app goes into background). Pausing the
   * server means it will
   * automatically start again the next time the app transitions from background
   * to foreground. To ensure the server is stopped for good, pass in `kill`
   * flag. In that case only explicit call to .start() will start the server
   * again.
   * @param {string} [details] Optional. If provided, it will be added
   *  to the STOPPING message emitted to the server state change listeners.
   * @returns {Promise<>}
   */
  async stop(details) {
    if (this._appStateSub) {
      this._appStateSub.remove();
      this._appStateSub = undefined;
    }
    await this._stop(details);
  }
  async _handleAppStateChange(appState) {
    const starting = appState === 'active' || appState === 'inactive';
    try {
      if (starting) await this.start('App entered foreground');else await this._stop('App entered background');
    } catch (e) {
      // If anything goes wrong within .start() or ._stop() calls, those methods
      // will move the server into the "CRASHED" state, and they'll notify all
      // server state listeners (see .addStateListener()) about the error, with
      // all related details.
      //
      // Thus, if any state listener is connected to the server, we assume it
      // handles possible errors, and we just silently ignore the error here
      // (otherwise some instrumentation, like e.g. Sentry, may catch and report
      // it as unhandled, which is confusing when the error has been handled
      // within the listener).
      //
      // However, if no listeners are connected, we throw an error to allow
      // the instrumentation, if any, to detect and report it as unhandled.
      //
      // In either case this very function is used internally, thus no way for
      // the library consumer to directly handle its possible rejections.
      if (!this._stateChangeEmitter.hasListeners) {
        throw Error(starting ? `Server (#${this._id}) auto-start on the app going into foreground failed` : `Server (#${this._id}) auto-stop on the app going into background failed`);
      }
    }
  }
}
var _default = exports.default = StaticServer;
/**
 * Extracts bundled assets into the specified regular directory,
 * preserving asset folder structure, and overwriting any conflicting files
 * in the destination.
 *
 * This is an Android-specific function; it does nothing if called on iOS.
 *
 * @param {string} [into=''] Optional. The destination folder for extracted
 *  assets. By default assets are extracted into the app's document folder.
 * @param {string} [from=''] Optional. Relative path of the root asset folder,
 *  starting from which all assets contained in that folder and its subfolders
 *  will be extracted into the destination folder, preserving asset folder
 *  structure. By default all bundled assets will be extracted.
 * @return {Promise} Resolves once unpacking is completed.
 */
async function extractBundledAssets(into = _reactNativeFs.DocumentDirectoryPath, from = '') {
  console.warn('extractBundledAssets() is deprecated! See: https://github.com/birdofpreyru/react-native-static-server?tab=readme-ov-file#extractbundledassets');
  if (_reactNative.Platform.OS !== 'android') return;
  await (0, _reactNativeFs.mkdir)(into);
  const assets = await (0, _reactNativeFs.readDirAssets)(from);
  for (let i = 0; i < assets.length; ++i) {
    const asset = assets[i];
    const target = `${into}/${asset.name}`;
    if (asset.isDirectory()) await extractBundledAssets(target, asset.path);else await (0, _reactNativeFs.copyFileAssets)(asset.path, target);
  }
}

/**
 * Returns a server instance currently being in ACTIVE, STARTING,
 * or STOPPING state, if such server instance exists.
 * @return {StaticServer|undefined}
 */
function getActiveServer() {
  return Object.values(servers).find(group => {
    const server = group.values().next().value;
    const state = server?.state;
    return state !== _constants.STATES.INACTIVE && state !== _constants.STATES.CRASHED;
  });
}
const getActiveServerId = exports.getActiveServerId = _ReactNativeStaticServer.default.getActiveServerId;
//# sourceMappingURL=index.js.map