import { encode, decode } from '@msgpack/msgpack';
import createDebug from 'debug';
import EventEmitter from 'eventemitter3';
import WebSocketIpml from 'isomorphic-ws';
import sha256 from 'crypto-js/sha256.js';
import Base64 from 'crypto-js/enc-base64.js';

function _extends() {
  _extends = Object.assign ? Object.assign.bind() : function (target) {
    for (var i = 1; i < arguments.length; i++) {
      var source = arguments[i];

      for (var key in source) {
        if (Object.prototype.hasOwnProperty.call(source, key)) {
          target[key] = source[key];
        }
      }
    }

    return target;
  };
  return _extends.apply(this, arguments);
}

function _objectWithoutPropertiesLoose(source, excluded) {
  if (source == null) return {};
  var target = {};
  var sourceKeys = Object.keys(source);
  var key, i;

  for (i = 0; i < sourceKeys.length; i++) {
    key = sourceKeys[i];
    if (excluded.indexOf(key) >= 0) continue;
    target[key] = source[key];
  }

  return target;
}

var WebSocketOpCode;

(function (WebSocketOpCode) {
  /**
   * The initial message sent by obs-websocket to newly connected clients.
   *
   * Initial OBS Version: 5.0.0
   */
  WebSocketOpCode[WebSocketOpCode["Hello"] = 0] = "Hello";
  /**
   * The message sent by a newly connected client to obs-websocket in response to a `Hello`.
   *
   * Initial OBS Version: 5.0.0
   */

  WebSocketOpCode[WebSocketOpCode["Identify"] = 1] = "Identify";
  /**
   * The response sent by obs-websocket to a client after it has successfully identified with obs-websocket.
   *
   * Initial OBS Version: 5.0.0
   */

  WebSocketOpCode[WebSocketOpCode["Identified"] = 2] = "Identified";
  /**
   * The message sent by an already-identified client to update identification parameters.
   *
   * Initial OBS Version: 5.0.0
   */

  WebSocketOpCode[WebSocketOpCode["Reidentify"] = 3] = "Reidentify";
  /**
   * The message sent by obs-websocket containing an event payload.
   *
   * Initial OBS Version: 5.0.0
   */

  WebSocketOpCode[WebSocketOpCode["Event"] = 5] = "Event";
  /**
   * The message sent by a client to obs-websocket to perform a request.
   *
   * Initial OBS Version: 5.0.0
   */

  WebSocketOpCode[WebSocketOpCode["Request"] = 6] = "Request";
  /**
   * The message sent by obs-websocket in response to a particular request from a client.
   *
   * Initial OBS Version: 5.0.0
   */

  WebSocketOpCode[WebSocketOpCode["RequestResponse"] = 7] = "RequestResponse";
  /**
   * The message sent by a client to obs-websocket to perform a batch of requests.
   *
   * Initial OBS Version: 5.0.0
   */

  WebSocketOpCode[WebSocketOpCode["RequestBatch"] = 8] = "RequestBatch";
  /**
   * The message sent by obs-websocket in response to a particular batch of requests from a client.
   *
   * Initial OBS Version: 5.0.0
   */

  WebSocketOpCode[WebSocketOpCode["RequestBatchResponse"] = 9] = "RequestBatchResponse";
})(WebSocketOpCode || (WebSocketOpCode = {}));
/* eslint-disable no-bitwise, @typescript-eslint/prefer-literal-enum-member */


var EventSubscription;

(function (EventSubscription) {
  /**
   * Subcription value used to disable all events.
   *
   * Initial OBS Version: 5.0.0
   */
  EventSubscription[EventSubscription["None"] = 0] = "None";
  /**
   * Subscription value to receive events in the `General` category.
   *
   * Initial OBS Version: 5.0.0
   */

  EventSubscription[EventSubscription["General"] = 1] = "General";
  /**
   * Subscription value to receive events in the `Config` category.
   *
   * Initial OBS Version: 5.0.0
   */

  EventSubscription[EventSubscription["Config"] = 2] = "Config";
  /**
   * Subscription value to receive events in the `Scenes` category.
   *
   * Initial OBS Version: 5.0.0
   */

  EventSubscription[EventSubscription["Scenes"] = 4] = "Scenes";
  /**
   * Subscription value to receive events in the `Inputs` category.
   *
   * Initial OBS Version: 5.0.0
   */

  EventSubscription[EventSubscription["Inputs"] = 8] = "Inputs";
  /**
   * Subscription value to receive events in the `Transitions` category.
   *
   * Initial OBS Version: 5.0.0
   */

  EventSubscription[EventSubscription["Transitions"] = 16] = "Transitions";
  /**
   * Subscription value to receive events in the `Filters` category.
   *
   * Initial OBS Version: 5.0.0
   */

  EventSubscription[EventSubscription["Filters"] = 32] = "Filters";
  /**
   * Subscription value to receive events in the `Outputs` category.
   *
   * Initial OBS Version: 5.0.0
   */

  EventSubscription[EventSubscription["Outputs"] = 64] = "Outputs";
  /**
   * Subscription value to receive events in the `SceneItems` category.
   *
   * Initial OBS Version: 5.0.0
   */

  EventSubscription[EventSubscription["SceneItems"] = 128] = "SceneItems";
  /**
   * Subscription value to receive events in the `MediaInputs` category.
   *
   * Initial OBS Version: 5.0.0
   */

  EventSubscription[EventSubscription["MediaInputs"] = 256] = "MediaInputs";
  /**
   * Subscription value to receive the `VendorEvent` event.
   *
   * Initial OBS Version: 5.0.0
   */

  EventSubscription[EventSubscription["Vendors"] = 512] = "Vendors";
  /**
   * Subscription value to receive events in the `Ui` category.
   *
   * Initial OBS Version: 5.0.0
   */

  EventSubscription[EventSubscription["Ui"] = 1024] = "Ui";
  /**
   * Helper to receive all non-high-volume events.
   *
   * Initial OBS Version: 5.0.0
   */

  EventSubscription[EventSubscription["All"] = 1023] = "All";
  /**
   * Subscription value to receive the `InputVolumeMeters` high-volume event.
   *
   * Initial OBS Version: 5.0.0
   */

  EventSubscription[EventSubscription["InputVolumeMeters"] = 65536] = "InputVolumeMeters";
  /**
   * Subscription value to receive the `InputActiveStateChanged` high-volume event.
   *
   * Initial OBS Version: 5.0.0
   */

  EventSubscription[EventSubscription["InputActiveStateChanged"] = 131072] = "InputActiveStateChanged";
  /**
   * Subscription value to receive the `InputShowStateChanged` high-volume event.
   *
   * Initial OBS Version: 5.0.0
   */

  EventSubscription[EventSubscription["InputShowStateChanged"] = 262144] = "InputShowStateChanged";
  /**
   * Subscription value to receive the `SceneItemTransformChanged` high-volume event.
   *
   * Initial OBS Version: 5.0.0
   */

  EventSubscription[EventSubscription["SceneItemTransformChanged"] = 524288] = "SceneItemTransformChanged";
})(EventSubscription || (EventSubscription = {}));
/* eslint-enable no-bitwise, @typescript-eslint/prefer-literal-enum-member */


var RequestBatchExecutionType;

(function (RequestBatchExecutionType) {
  /**
   * Not a request batch.
   *
   * Initial OBS Version: 5.0.0
   */
  RequestBatchExecutionType[RequestBatchExecutionType["None"] = -1] = "None";
  /**
   * A request batch which processes all requests serially, as fast as possible.
   *
   * Note: To introduce artificial delay, use the `Sleep` request and the `sleepMillis` request field.
   *
   * Initial OBS Version: 5.0.0
   */

  RequestBatchExecutionType[RequestBatchExecutionType["SerialRealtime"] = 0] = "SerialRealtime";
  /**
   * A request batch type which processes all requests serially, in sync with the graphics thread. Designed to provide high accuracy for animations.
   *
   * Note: To introduce artificial delay, use the `Sleep` request and the `sleepFrames` request field.
   *
   * Initial OBS Version: 5.0.0
   */

  RequestBatchExecutionType[RequestBatchExecutionType["SerialFrame"] = 1] = "SerialFrame";
  /**
   * A request batch type which processes all requests using all available threads in the thread pool.
   *
   * Note: This is mainly experimental, and only really shows its colors during requests which require lots of
   * active processing, like `GetSourceScreenshot`.
   *
   * Initial OBS Version: 5.0.0
   */

  RequestBatchExecutionType[RequestBatchExecutionType["Parallel"] = 2] = "Parallel";
})(RequestBatchExecutionType || (RequestBatchExecutionType = {}));

/**
 * SHA256 Hashing.
 *
 * @param  {string} [salt=''] salt.
 * @param  {string} [challenge=''] challenge.
 * @param  {string} msg Message to encode.
 * @returns {string} sha256 encoded string.
 */

function authenticationHashing (salt, challenge, msg) {
  const hash = Base64.stringify(sha256(msg + salt));
  return Base64.stringify(sha256(hash + challenge));
}

const _excluded = ["authentication", "rpcVersion"];
const debug = createDebug('obs-websocket-js');
class OBSWebSocketError extends Error {
  constructor(code, message) {
    super(message);
    this.code = void 0;
    this.code = code;
  }

}
class BaseOBSWebSocket extends EventEmitter {
  constructor(...args) {
    super(...args);
    this._identified = false;
    this.internalListeners = new EventEmitter();
    this.socket = void 0;
  }

  static generateMessageId() {
    return String(BaseOBSWebSocket.requestCounter++);
  }

  get identified() {
    return this._identified;
  }
  /**
   * Connect to an obs-websocket server
   *
   * @param url Websocket server to connect to (including ws:// or wss:// protocol)
   * @param password Password
   * @param identificationParams Data for Identify event
   * @returns Hello & Identified messages data (combined)
   */


  async connect(url = 'ws://127.0.0.1:4455', password, identificationParams = {}) {
    var _this = this;

    if (this.socket) {
      await this.disconnect();
    }

    try {
      const connectionClosedPromise = this.internalEventPromise('ConnectionClosed');
      const connectionErrorPromise = this.internalEventPromise('ConnectionError');
      return await Promise.race([async function () {
        const hello = await _this.createConnection(url);

        _this.emit('Hello', hello);

        return _this.identify(hello, password, identificationParams);
      }(), // Choose the best promise for connection error/close
      // In browser connection close has close code + reason,
      // while in node error event has these
      new Promise((resolve, reject) => {
        void connectionErrorPromise.then(e => {
          if (e.message) {
            reject(e);
          }
        });
        void connectionClosedPromise.then(e => {
          reject(e);
        });
      })]);
    } catch (error) {
      await this.disconnect();
      throw error;
    }
  }
  /**
   * Disconnect from obs-websocket server
   */


  async disconnect() {
    if (!this.socket || this.socket.readyState === WebSocketIpml.CLOSED) {
      return;
    }

    const connectionClosedPromise = this.internalEventPromise('ConnectionClosed');
    this.socket.close();
    await connectionClosedPromise;
  }
  /**
   * Update session parameters
   *
   * @param data Reidentify data
   * @returns Identified message data
   */


  async reidentify(data) {
    const identifiedPromise = this.internalEventPromise(`op:${WebSocketOpCode.Identified}`);
    await this.message(WebSocketOpCode.Reidentify, data);
    return identifiedPromise;
  }
  /**
   * Send a request to obs-websocket
   *
   * @param requestType Request name
   * @param requestData Request data
   * @returns Request response
   */


  async call(requestType, requestData) {
    const requestId = BaseOBSWebSocket.generateMessageId();
    const responsePromise = this.internalEventPromise(`res:${requestId}`);
    await this.message(WebSocketOpCode.Request, {
      requestId,
      requestType,
      requestData
    });
    const {
      requestStatus,
      responseData
    } = await responsePromise;

    if (!requestStatus.result) {
      throw new OBSWebSocketError(requestStatus.code, requestStatus.comment);
    }

    return responseData;
  }
  /**
   * Send a batch request to obs-websocket
   *
   * @param requests Array of Request objects (type and data)
   * @param options A set of options for how the batch will be executed
   * @param options.executionType The mode of execution obs-websocket will run the batch in
   * @param options.haltOnFailure Whether obs-websocket should stop executing the batch if one request fails
   * @returns RequestBatch response
   */


  async callBatch(requests, options = {}) {
    const requestId = BaseOBSWebSocket.generateMessageId();
    const responsePromise = this.internalEventPromise(`res:${requestId}`);
    await this.message(WebSocketOpCode.RequestBatch, _extends({
      requestId,
      requests
    }, options));
    const {
      results
    } = await responsePromise;
    return results;
  }
  /**
   * Cleanup from socket disconnection
   */


  cleanup() {
    if (!this.socket) {
      return;
    }

    this.socket.onopen = null;
    this.socket.onmessage = null;
    this.socket.onerror = null;
    this.socket.onclose = null;
    this.socket = undefined;
    this._identified = false; // Cleanup leftovers

    this.internalListeners.removeAllListeners();
  }
  /**
   * Create connection to specified obs-websocket server
   *
   * @private
   * @param url Websocket address
   * @returns Promise for hello data
   */


  async createConnection(url) {
    var _this$socket;

    const connectionOpenedPromise = this.internalEventPromise('ConnectionOpened');
    const helloPromise = this.internalEventPromise(`op:${WebSocketOpCode.Hello}`);
    this.socket = new WebSocketIpml(url, this.protocol);
    this.socket.onopen = this.onOpen.bind(this);
    this.socket.onmessage = this.onMessage.bind(this);
    this.socket.onerror = this.onError.bind(this);
    this.socket.onclose = this.onClose.bind(this);
    await connectionOpenedPromise;
    const protocol = (_this$socket = this.socket) == null ? void 0 : _this$socket.protocol; // Browsers don't autoclose on missing/wrong protocol

    if (!protocol) {
      throw new OBSWebSocketError(-1, 'Server sent no subprotocol');
    }

    if (protocol !== this.protocol) {
      throw new OBSWebSocketError(-1, 'Server sent an invalid subprotocol');
    }

    return helloPromise;
  }
  /**
   * Send identify message
   *
   * @private
   * @param hello Hello message data
   * @param password Password
   * @param identificationParams Identification params
   * @returns Hello & Identified messages data (combined)
   */


  async identify(_ref, password, identificationParams = {}) {
    let {
      authentication,
      rpcVersion
    } = _ref,
        helloRest = _objectWithoutPropertiesLoose(_ref, _excluded);

    // Set rpcVersion if unset
    const data = _extends({
      rpcVersion
    }, identificationParams);

    if (authentication && password) {
      data.authentication = authenticationHashing(authentication.salt, authentication.challenge, password);
    }

    const identifiedPromise = this.internalEventPromise(`op:${WebSocketOpCode.Identified}`);
    await this.message(WebSocketOpCode.Identify, data);
    const identified = await identifiedPromise;
    this._identified = true;
    this.emit('Identified', identified);
    return _extends({
      rpcVersion
    }, helloRest, identified);
  }
  /**
   * Send message to obs-websocket
   *
   * @private
   * @param op WebSocketOpCode
   * @param d Message data
   */


  async message(op, d) {
    if (!this.socket) {
      throw new Error('Not connected');
    }

    if (!this.identified && op !== 1) {
      throw new Error('Socket not identified');
    }

    const encoded = await this.encodeMessage({
      op,
      d
    });
    this.socket.send(encoded);
  }
  /**
   * Create a promise to listen for an event on internal listener
   * (will be cleaned up on disconnect)
   *
   * @private
   * @param event Event to listen to
   * @returns Event data
   */


  async internalEventPromise(event) {
    return new Promise(resolve => {
      this.internalListeners.once(event, resolve);
    });
  }
  /**
   * Websocket open event listener
   *
   * @private
   * @param e Event
   */


  onOpen(e) {
    debug('socket.open');
    this.emit('ConnectionOpened');
    this.internalListeners.emit('ConnectionOpened', e);
  }
  /**
   * Websocket message event listener
   *
   * @private
   * @param e Event
   */


  async onMessage(e) {
    try {
      const {
        op,
        d
      } = await this.decodeMessage(e.data);
      debug('socket.message: %d %j', op, d);

      if (op === undefined || d === undefined) {
        return;
      }

      switch (op) {
        case WebSocketOpCode.Event:
          {
            const {
              eventType,
              eventData
            } = d; // @ts-expect-error Typescript just doesn't understand it

            this.emit(eventType, eventData);
            return;
          }

        case WebSocketOpCode.RequestResponse:
        case WebSocketOpCode.RequestBatchResponse:
          {
            const {
              requestId
            } = d;
            this.internalListeners.emit(`res:${requestId}`, d);
            return;
          }

        default:
          this.internalListeners.emit(`op:${op}`, d);
      }
    } catch (error) {
      debug('error handling message: %o', error);
    }
  }
  /**
   * Websocket error event listener
   *
   * @private
   * @param e ErrorEvent
   */


  onError(e) {
    debug('socket.error: %o', e);
    const error = new OBSWebSocketError(-1, e.message);
    this.emit('ConnectionError', error);
    this.internalListeners.emit('ConnectionError', error);
  }
  /**
   * Websocket close event listener
   *
   * @private
   * @param e Event
   */


  onClose(e) {
    debug('socket.close: %s (%d)', e.reason, e.code);
    const error = new OBSWebSocketError(e.code, e.reason);
    this.emit('ConnectionClosed', error);
    this.internalListeners.emit('ConnectionClosed', error);
    this.cleanup();
  }

} // https://github.com/developit/microbundle/issues/531#issuecomment-575473024
// Not using ESM export due to it also being detected and breaking rollup based bundlers (vite)

BaseOBSWebSocket.requestCounter = 1;

if (typeof exports !== 'undefined') {
  Object.defineProperty(exports, '__esModule', {
    value: true
  });
}

class OBSWebSocket extends BaseOBSWebSocket {
  constructor(...args) {
    super(...args);
    this.protocol = 'obswebsocket.msgpack';
  }

  async encodeMessage(data) {
    return encode(data);
  }

  async decodeMessage(data) {
    // Browsers provide Blob while node gives straight ArrayBuffer
    if (typeof Blob !== 'undefined' && data instanceof Blob) {
      data = await data.arrayBuffer();
    }

    return decode(data);
  }

}

export { EventSubscription, OBSWebSocketError, RequestBatchExecutionType, WebSocketOpCode, OBSWebSocket as default };
//# sourceMappingURL=msgpack.modern.js.map
