import moment from 'moment';
import { Promise } from 'rsvp';
import { stringify, dataViewToString, log, isDef } from './util.js';
import { parseSingleDataRecord } from './miniseed.js';

/*
 * Philip Crotwell
 * University of South Carolina, 2019
 * http://www.seis.sc.edu
 */
/** const for datalink protocol for web sockets, DataLink1.0 */

const DATALINK_PROTOCOL = "DataLink1.0";
/** const for query mode, QUERY */

const QUERY_MODE = "QUERY";
/** const for streaming mode, STREAM */

const STREAM_MODE = "STREAM";
/** const for maximum process number to create fake number for datalink id, 2^16-2 */

const MAX_PROC_NUM = Math.pow(2, 16) - 2;
/** const for fake user name for datalink id, browser */

const USER_BROWSER = "browser";
/** const for error response, ERROR */

const ERROR = "ERROR";
const PACKET = "PACKET";
const STREAM = "STREAM";
const ENDSTREAM = "ENDSTREAM";
const MSEED_TYPE = "/MSEED";
const IRIS_RINGSERVER_URL = "ws://rtserve.iris.washington.edu/datalink";

let defaultHandleResponse = function defaultHandleResponse(message) {
  log("Unhandled datalink response: " + message);
};
/**
 * A websocket based Datalink connection to a ringserver.
 *  https://raw.githubusercontent.com/iris-edu/libdali/master/doc/DataLink.protocol
 * @param url websocket url to the ringserver
 * @param packetHandler callback for packets as they arrive
 * @param errorHandler callback for errors
 */


class DataLinkConnection {
  /** @private */
  constructor(url, packetHandler, errorHandler) {
    this.url = url;
    this._mode = QUERY_MODE;
    this.packetHandler = packetHandler;
    this.errorHandler = errorHandler;
    this.closeHandler = null;
    this.serverId = null; // meant to be processId, so use 1 <= num <= 2^15 to be safe

    this.clientIdNum = Math.floor(Math.random() * MAX_PROC_NUM) + 1;
    this.username = USER_BROWSER;
    this._responseResolve = null;
    this._responseReject = null;
  }

  setOnClose(closeHandler) {
    this.closeHandler = closeHandler;
  }
  /** creates the websocket connection and sends the client
  *  ID. Returns a Promise that resolves to the server's
  * ID.
  */


  connect() {
    const that = this;
    return new Promise(function (resolve, reject) {
      const webSocket = new WebSocket(that.url, DATALINK_PROTOCOL);
      that.webSocket = webSocket;
      webSocket.binaryType = 'arraybuffer';

      webSocket.onmessage = function (event) {
        that.handle(event);
      };

      webSocket.onerror = function (event) {
        that.handleError(new Error("" + stringify(event)));
        reject(event);
      };

      webSocket.onclose = function (closeEvent) {
        that.webSocket = null; // clean up

        that._mode = QUERY_MODE;

        if (that.closeHandler) {
          that.closeHandler(closeEvent);
        }
      };

      webSocket.onopen = function () {
        resolve(that);
      };
    }).then(datalink => {
      return datalink.sendId();
    }).then(idmsg => {
      that.serverId = idmsg;
      return idmsg;
    });
  }
  /**
   * @returns true if the websocket is connected (non-null)
   */


  isConnected() {
    return this.webSocket !== null;
  }
  /**
   * gets the current mode, QUERY_MODE or STREAM_MODE
   */


  get mode() {
    return this._mode;
  }
  /**
   * Switches to streaming mode to receive data packets from the ringserver.
   */


  stream() {
    if (this._mode === STREAM_MODE) {
      return;
    }

    this._mode = STREAM_MODE;
    this.sendDLCommand(STREAM, "");
  }
  /**
   * Switches back to query mode to enable commands to be sent to the ringserver.
   */


  endStream() {
    if (this.webSocket === null || this._mode === null || this._mode === QUERY_MODE) {
      return;
    }

    this._mode = QUERY_MODE;
    this.sendDLCommand(ENDSTREAM, "");
  }
  /**
   * Closes the connection and the underlying websocket. No communication
   * is possible until connect() is called again.
   */


  close() {
    if (this.webSocket) {
      this.endStream(); // end streaming just in case

      if (this.webSocket) {
        this.webSocket.close();
      }

      this.webSocket = null;
      this._mode = null;
    }
  }
  /**
  * Send a ID Command. Command is a string.
  * @return a Promise that resolves to the response from the ringserver.
  */


  sendId() {
    const that = this;
    return this.id("seisplotjs", this.username, stringify(this.clientIdNum), "javascript").then(this.ensureDataLinkResponse).then(dlResponse => {
      if (dlResponse.type === 'ID') {
        that.serverId = '' + dlResponse.message;
        return that.serverId;
      } else {
        throw new Error("not ID response: " + stringify(dlResponse.type));
      }
    });
  }
  /** encodes as a Datalink packet, header with optional data section as
   * binary Uint8Array. Size of the binary data is appended
   * to the header if present.
   */


  encodeDL(command, data) {
    let cmdLen = command.length;
    let len = 3 + command.length;
    let lenStr = "";

    if (data && data.length > 0) {
      lenStr = String(data.length);
      len += lenStr.length + 1;
      cmdLen += lenStr.length + 1;
      len += data.length;
    }

    let rawPacket = new ArrayBuffer(len);
    const binaryPacket = new Uint8Array(rawPacket);
    let packet = new DataView(rawPacket);
    packet.setUint8(0, 68); // ascii D

    packet.setUint8(1, 76); // ascii L

    packet.setUint8(2, cmdLen);
    let i = 3;

    for (const c of command) {
      packet.setUint8(i, c.charCodeAt(0));
      i++;
    }

    const SPACE = ' ';

    if (data && data.length > 0) {
      packet.setUint8(i, SPACE.charCodeAt(0)); // ascii space

      i++;

      for (const c of lenStr) {
        packet.setUint8(i, c.charCodeAt(0));
        i++;
      }

      binaryPacket.set(data, i);
    }

    return rawPacket;
  }
  /** sends the header with optional binary data
   * as the data section. Size of the data is appended
   * to the header before sending if present.
   * @param header header to send
   * @param data optional data to send
   */


  sendDLBinary(header, data) {
    const rawPacket = this.encodeDL(header, data);

    if (this.webSocket) {
      this.webSocket.send(rawPacket);
    } else {
      throw new Error("WebSocket has been closed.");
    }
  }
  /** sends the command as header with optional dataString
   * as the data section. Size of the dataString is appended
   * to the header before sending.
   */


  sendDLCommand(command, dataString) {
    this.sendDLBinary(command, stringToUint8Array(dataString));
  }
  /**
  * Send a DataLink Command and await the response. Command is a string.
  * @returns a Promise that resolves with the webSocket MessageEvent.
  */


  awaitDLBinary(header, data) {
    let that = this;
    let promise = new Promise(function (resolve, reject) {
      that._responseResolve = resolve;
      that._responseReject = reject;
      that.sendDLBinary(header, data);
    }).then(response => {
      that._responseResolve = null;
      that._responseReject = null;
      return response;
    }).catch(error => {
      that._responseResolve = null;
      that._responseReject = null;
      throw error;
    });
    return promise;
  }
  /**
  * Send a DataLink Command and await the response. Command is a string.
  * Returns a Promise that resolves with the webSocket MessageEvent.
  */


  awaitDLCommand(command, dataString) {
    return this.awaitDLBinary(command, stringToUint8Array(dataString));
  }
  /** Writes data to the ringserver and awaits a acknowledgement.
  *
  */


  writeAck(streamid, hpdatastart, hpdataend, data) {
    let header = "WRITE ".concat(streamid, " ").concat(momentToHPTime(hpdatastart), " ").concat(momentToHPTime(hpdataend), " A");
    return this.awaitDLBinary(header, data);
  }

  ensureDataLinkResponse(dl) {
    if (dl instanceof DataLinkResponse) {
      return dl;
    }

    throw new Error("Expected DataLinkResponse but got ".concat(dl.header));
  }

  ensureDataLinkPacket(dl) {
    if (dl instanceof DataLinkPacket) {
      return dl;
    }

    throw new Error("Expected DataLinkPacket but got ".concat(dl.type));
  }

  id(programname, username, processid, architecture) {
    let command = "ID ".concat(programname, ":").concat(username, ":").concat(processid, ":").concat(architecture);
    return this.awaitDLCommand(command).then(this.ensureDataLinkResponse);
  }

  info(infoType) {
    let command = "INFO ".concat(infoType);
    return this.awaitDLCommand(command).then(this.ensureDataLinkResponse);
  }

  positionAfter(time) {
    return this.positionAfterHPTime(momentToHPTime(time)).then(this.ensureDataLinkResponse);
  }

  positionAfterHPTime(hpTime) {
    let command = "POSITION AFTER ".concat(hpTime);
    return this.awaitDLCommand(command).then(this.ensureDataLinkResponse);
  }

  match(pattern) {
    let command = "MATCH";
    return this.awaitDLCommand(command, pattern).then(this.ensureDataLinkResponse);
  }

  reject(pattern) {
    let command = "REJECT ".concat(pattern);
    return this.awaitDLCommand(command).then(this.ensureDataLinkResponse);
  }

  read(packetId) {
    let command = "READ ".concat(packetId);
    return this.awaitDLBinary(command).then(this.ensureDataLinkPacket);
  }
  /**
   * Handles a web socket message from the data link connection.
   * @private
   */


  handle(wsEvent) {
    const rawData = wsEvent.data;
    let dlPreHeader = new DataView(rawData, 0, 3);

    if ('D' === String.fromCharCode(dlPreHeader.getUint8(0)) && 'L' === String.fromCharCode(dlPreHeader.getUint8(1))) {
      const headerLen = dlPreHeader.getUint8(2);
      const header = dataViewToString(new DataView(rawData, 3, headerLen));

      if (header.startsWith(PACKET)) {
        let packet = new DataLinkPacket(header, new DataView(rawData, 3 + headerLen));

        if (this.packetHandler) {
          try {
            this.packetHandler(packet);
          } catch (e) {
            this.errorHandler(e);
          }
        } else {
          this.errorHandler(new Error("packetHandler not defined"));
        }
      } else {
        let dv;

        if (rawData.byteLength > 3 + headerLen) {
          dv = new DataView(rawData, 3 + headerLen);
        }

        const dlResponse = DataLinkResponse.parse(header, dv);

        if (dlResponse.type === 'ERROR') {
          this.handleError(new Error("value=".concat(dlResponse.value, " ").concat(dlResponse.message)));
        } else {
          if (this._responseResolve) {
            this._responseResolve(dlResponse);
          } else {
            defaultHandleResponse(header);
          }
        }
      }
    } else {
      throw new Error("DataLink Packet did not start with DL");
    }
  }

  handleError(error) {
    if (this._responseReject) {
      this._responseReject(error);
    }

    if (this.errorHandler) {
      this.errorHandler(error);
    } else {
      log("datalink handleError: " + error.message);
    }
  }

}
/**
 * Datalink response, used for ID, INFO, OK and ERROR responses.
 * @type {[type]}
 */

class DataLinkResponse {
  constructor(type, value, message) {
    this.type = type;
    this.value = value;
    this.message = message;
  }

  toString() {
    return "".concat(this.type, " ").concat(this.value, " | ").concat(this.message);
  }

  static parse(header, data) {
    let value = "";
    let s = header.split(' ');
    let type = s[0];
    let message = '';

    if (type === 'ID') {
      message = '' + header.substring(3);
    } else if (type === 'INFO' || type === 'OK' || type === 'ERROR') {
      value = s[1];

      if (data) {
        message = dataViewToString(new DataView(data.buffer, 3 + header.length));
      }

      const dSize = parseInt(s[2]);
    } else {
      log("unknown DataLink response type: ".concat(type, "  ").concat(header));
      message = header.substring(type.length + 1);
    }

    return new DataLinkResponse(type, value, message);
  }

}
/**
 * Represents a Datalink packet from the ringserver.
 * Note this cannot connect directly to a native TCP socket, instead it
 * sends the datalink protocol over a websocket. Currently only the IRIS
 * ringserver supports websockets, but it may be possible to use thrid party
 * tools to proxy the websocket to a TCP datalink socket.
 *
 */

class DataLinkPacket {
  constructor(header, dataview) {
    this._miniseed = null;
    this.header = header;
    this.data = dataview;
    let split = this.header.split(' ');
    this.streamId = split[1];
    this.pktid = split[2];
    this.hppackettime = split[3];
    this.hppacketstart = split[4];
    this.hppacketend = split[5];
    this.dataSize = Number.parseInt(split[6]);

    if (dataview.byteLength < this.dataSize) {
      throw new Error("not enough bytes in dataview for packet: " + this.dataSize);
    }
  }
  /**
   * Packet start time as a moment.
   * @return start time
   */


  get packetStart() {
    return hpTimeToMoment(parseInt(this.hppacketstart));
  }
  /**
   * Packet end time as a moment.
   * @return end time
   */


  get packetEnd() {
    return hpTimeToMoment(parseInt(this.hppacketend));
  }
  /**
   * Packet time as a moment.
   * @return packet time
   */


  get packetTime() {
    return hpTimeToMoment(parseInt(this.hppackettime));
  }
  /**
   * is this packet a miniseed packet
   * @return {Boolean}         true if it is miniseed
   */


  isMiniseed() {
    return isDef(this._miniseed) || this.streamId.endsWith(MSEED_TYPE);
  }
  /**
   * Parsed payload as a miniseed data record, if the streamid
   * ends with '/MSEED', null otherwise.
   */


  get miniseed() {
    if (!isDef(this._miniseed)) {
      if (this.streamId.endsWith(MSEED_TYPE)) {
        this._miniseed = parseSingleDataRecord(this.data);
      } else {
        this._miniseed = null;
      }
    }

    return this._miniseed;
  }

}
/**
 * Convert moment to a HPTime number.
 * @param   m moment to convert
 * @return  microseconds since epoch
 */

function momentToHPTime(m) {
  return m.valueOf() * 1000;
}
/**
 * Convert moment to a HPTime number.
 * @param   m moment to convert
 * @return  microseconds since epoch
 */

function hpTimeToMoment(hptime) {
  return moment.utc(hptime / 1000);
}
/**
 * Encode string into a Uint8Array.
 * @param   dataString String to encode.
 * @return             String as bytes in Uint8Array.
 */

function stringToUint8Array(dataString) {
  let binaryData = undefined;

  if (dataString) {
    binaryData = new Uint8Array(dataString.length);

    for (let i = 0; i < dataString.length; i++) {
      binaryData[i] = dataString.charCodeAt(i);
    }
  }

  return binaryData;
}

export { DATALINK_PROTOCOL, DataLinkConnection, DataLinkPacket, DataLinkResponse, ENDSTREAM, ERROR, IRIS_RINGSERVER_URL, MAX_PROC_NUM, MSEED_TYPE, PACKET, QUERY_MODE, STREAM, STREAM_MODE, USER_BROWSER, hpTimeToMoment, momentToHPTime, stringToUint8Array };
//# sourceMappingURL=datalink.js.map
