import { Network } from '@tao.js/core';
 
// for backwards compatibility
const MAX_SAFE_INTEGER = Math.pow(2, 53) - 1;
 
let transponderId = 0;
function newTransponderId() {
  return (transponderId = ++transponderId % MAX_SAFE_INTEGER);
}
 
function transponderControl(transponderId, signal) {
  return { transponderId, signal };
}
 
/**
 * Allows use of Promises with a Network by returning a Promise from a signalling method
 * that will only resolve based on one of the handlers attached to the wrapped Network
 * being called.
 *
 * It is not recommended to attach a Transponder to your primary Network or Kernel as this will
 * resolve Promises with the first AppCon handled (unless that is the desired behavior).
 *
 * It is recommended to attach a Transponder to one of the other `utils` classes that filter
 * or have a subset of the handlers of your primary Network like a Channel.
 *
 * @export
 * @class Transponder
 */
export default class Transponder {
  /**
   *Creates an instance of Transponder.
   * @param {Network} network - the `Network` to wrap with a `Transponder`
   * @param {(string|function)} [id] - pass either a desired Channel ID value as a `string` or a `function` that will be used to generate a Channel ID
   *        the `function` will be called with a new Channel ID integer value to help ensure uniqueness
   * @param {number} [timeoutMs=0] - a timeout to be used when awaiting `Promises`
   *        - `0` - no timeout will be used
   *        - `> 0` - timeout will be used to `reject` the `Promise` if it is not signaled in time
   *        - use this to prevent unexpected behaviors
   * @param {Thenable} [promise=Promise] - a `Promise` constructor to be used when creating promises
   *        by a signalling method.
   * @param {boolean} [debug=false] - pass true to console.log internal activity
   * @memberof Transponder
   */
  constructor(network, id, timeoutMs = 0, promise = Promise, debug = false) {
    this._debug = debug;
    this._transponderId =
      typeof id === 'function'
        ? id(newTransponderId())
        : id || newTransponderId();
    const inTO = +timeoutMs || 0;
    this._timeoutMs = inTO > 0 ? inTO : 0;
    this._network =
      typeof network.use === 'function' ? network : network._network;
    this._network.use(this.handleSignalAppCon);
    this._promise = promise;
    this._cloneWithId = typeof id === 'function' ? id : undefined;
  }
 
  clone(cloneId) {
    const clone = new Transponder(
      this._network,
      cloneId || this._cloneWithId,
      this._timeoutMs,
      this._promise,
      this._debug
    );
    return clone;
  }
 
  /**
   * Attaches the `Transponder` to the `Network` it wraps by enabling the signalling to any
   * `Promises` that are created using one of the signalling methods.
   *
   * This is the inverse operation of the {@linkcode detach()} method and used to undo that
   * operation.
   *
   * `attach()` is unnecessary unless {@linkcode detach()} has been called as a newly
   * constructed `Transponder` is always attached to the `Network` it wraps.
   *
   * @see {@link detach}
   *
   * @returns this
   * @memberof Transponder
   */
  attach() {
    this._network.use(this.handleSignalAppCon);
    return this;
  }
 
  /**
   * Detaches the `Transponder` from the `Network` it wraps by disabling the signaling to any
   * `Promises` that are created using one of the signalling methods.
   *
   * Detach is temporary and can be reestablished using the {@link attach()} method.
   *
   * @see {@link attach()}
   *
   * @returns this
   * @memberof Transponder
   */
  detach() {
    this._network.stop(this.handleSignalAppCon);
    return this;
  }
 
  setCtx({ t, term, a, action, o, orient }, data) {
    const transponderId = this._transponderId;
    const timeoutMs = this._timeoutMs;
    const promise = this._promise;
 
    return new promise((resolve, reject) => {
      if (timeoutMs) {
        setTimeout(() => {
          reject(`reached timeout of: ${timeoutMs}ms`);
        }, timeoutMs);
      }
      const control = transponderControl(transponderId, resolve);
      this._network.setCtxControl(
        { t, term, a, action, o, orient },
        data,
        control
      );
    });
  }
 
  setAppCtx(ac) {
    const transponderId = this._transponderId;
    const timeoutMs = this._timeoutMs;
    const promise = this._promise;
 
    return new promise((resolve, reject) => {
      if (timeoutMs) {
        setTimeout(() => {
          reject(`reached timeout of: ${timeoutMs}ms`);
        }, timeoutMs);
      }
      const control = transponderControl(transponderId, resolve);
      this._network.setAppCtxControl(ac, control);
    });
  }
 
  handleSignalAppCon = (handler, ac, forwardAppCtx, control) => {
    this._debug &&
      console.log(
        `transponder{${this._transponderId}}::handleSignalFirstAppCon::ac:`,
        ac.unwrapCtx()
      );
    this._debug &&
      console.log(
        `transponder{${
          this._transponderId
        }}::handleSignalFirstAppCon::control:`,
        control
      );
    // first matching handler will signal the listener
    if (
      control.transponderId === this._transponderId &&
      control.signal &&
      !control.signalled
    ) {
      control.signalled = true;
      control.signal(ac);
    }
    // ALERT: handler will have already handled the AppCon before now
    // return handler.handleAppCon(ac, forwardAppCtx, control);
  };
}
  |