const {
  assertBusNameValid,
  assertInterfaceNameValid,
  assertObjectPathValid,
  assertMemberNameValid
} = require('./validators');

const {
  METHOD_CALL,
  METHOD_RETURN,
  ERROR,
  SIGNAL
} = require('./constants').MessageType;

/**
 * @class
 * A `Message` is a class used for sending and receiving messages through the
 * {@link MessageBus} with the low-level api. `Message`s can be constructed by
 * the user directly for method calls or with the static convenience methods on
 * this class for the other types of messages. `Message`s can be sent through a
 * connected MessageBus with {@link MessageBus#call} for method calls that
 * expect a reply from the server or {@link MessageBus#send} for messages that
 * do not expect a reply. See those methods for an example of how to use this
 * class.
 *
 * @see https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol
 *
 * @param {object} options - Options to construct this `Message` with. See the
 * corresponding member for more information.
 * @param {MessageType} [options.type={@link MessageType.METHOD_CALL}]
 * @param {int} [options.serial]
 * @param {string} [options.destination]
 * @param {string} [options.path]
 * @param {string} [options.interface] - Required for signals.
 * @param {string} [options.member] - Required for method calls and signals.
 * @param {string} [options.signature='']
 * @param {Array} [options.body=[]] -  Must match the signature.
 * @param {string} [options.errorName] - Must be a valid interface name.
 * Required for errors.
 * @param {string} [options.replySerial] - Required for errors and method returns.
 * @param {MessageFlags} [options.flags]
 */
class Message {
  /**
   * Construct a new `Message` to send on the bus.
   */
  constructor (msg) {
    /**
     * @member {MessageType} - The type of message this is.
     */
    this.type = (msg.type ? msg.type : METHOD_CALL);
    this._sent = false;
    this._serial = (isNaN(msg.serial) ? null : msg.serial);
    /**
     * @member {string} - The name of the object path for the message.
     * Required for method calls and signals.
     */
    this.path = msg.path;
    /**
     * @member {string} - The destination interface for this message.
     */
    this.interface = msg.interface;
    /**
     * @member {string} - The destination member on the interface for this message.
     */
    this.member = msg.member;
    /**
     * @member {string} - The name of the error if this is a message of type
     * `ERROR`. Must be a valid interface name.
     */
    this.errorName = msg.errorName;
    /**
     * @member {int} - The serial for the message this is in reply to. Set for
     * types `ERROR` and `METHOD_RETURN`.
     */
    this.replySerial = msg.replySerial;
    /**
     * @member {string} - The address on the bus to send the message
     * to.
     */
    this.destination = msg.destination;
    /**
     * @member {string} - The name of the bus from which this message was sent.
     * Set by the {@link MessageBus}.
     */
    this.sender = msg.sender;
    /**
     * @member {string} - The type signature for the body args.
     */
    this.signature = msg.signature || '';
    /**
     * @member {Array} - The body arguments for this message. Must match the signature.
     */
    this.body = msg.body || [];
    /**
     * @member {MessageFlags} - The flags for this message.
     */
    this.flags = msg.flags || 0;

    if (this.destination) {
      assertBusNameValid(this.destination);
    }

    if (this.interface) {
      assertInterfaceNameValid(this.interface);
    }

    if (this.path) {
      assertObjectPathValid(this.path);
    }

    if (this.member) {
      assertMemberNameValid(this.member);
    }

    if (this.errorName) {
      assertInterfaceNameValid(this.errorName);
    }

    const requireFields = (...fields) => {
      for (const field of fields) {
        if (this[field] === undefined) {
          throw new Error(`Message is missing a required field: ${field}`);
        }
      }
    };

    // validate required fields
    switch (this.type) {
      case METHOD_CALL:
        requireFields('path', 'member');
        break;
      case SIGNAL:
        requireFields('path', 'member', 'interface');
        break;
      case ERROR:
        requireFields('errorName', 'replySerial');
        break;
      case METHOD_RETURN:
        requireFields('replySerial');
        break;
      default:
        throw new Error(`Got unknown message type: ${this.type}`);
    }
  }

  /**
    * @member {int} - The serial of the message to track through the bus.  You
    * must use {@link MessageBus#newSerial} to get this serial. If not set, it
    * will be set automatically when the message is sent.
    */
  get serial () {
    return this._serial;
  }

  set serial (value) {
    this._sent = false;
    this._serial = value;
  }

  /**
   * Construct a new `Message` of type `ERROR` in reply to the given `Message`.
   *
   * @param {Message} msg - The `Message` this error is in reply to.
   * @param {string} errorName - The name of the error. Must be a valid
   * interface name.
   * @param {string} [errorText='An error occurred.'] - An error message for
   * the error.
   */
  static newError (msg, errorName, errorText = 'An error occurred.') {
    assertInterfaceNameValid(errorName);
    return new Message({
      type: ERROR,
      replySerial: msg.serial,
      destination: msg.sender,
      errorName: errorName,
      signature: 's',
      body: [errorText]
    });
  }

  /**
   * Construct a new `Message` of type `METHOD_RETURN` in reply to the given
   * message.
   *
   * @param {Message} msg - The `Message` this `Message` is in reply to.
   * @param {string} signature - The signature for the message body.
   * @param {Array} body - The body of the message as an array of arguments.
   * Must match the signature.
   */
  static newMethodReturn (msg, signature = '', body = []) {
    return new Message({
      type: METHOD_RETURN,
      replySerial: msg.serial,
      destination: msg.sender,
      signature: signature,
      body: body
    });
  }

  /**
   * Construct a new `Message` of type `SIGNAL` to broadcast on the bus.
   *
   * @param {string} path - The object path of this signal.
   * @param {string} iface - The interface of this signal.
   * @param {string} signature - The signature of the message body.
   * @param {Array] body - The body of the message as an array of arguments.
   * Must match the signature.
   */
  static newSignal (path, iface, name, signature = '', body = []) {
    return new Message({
      type: SIGNAL,
      interface: iface,
      path: path,
      member: name,
      signature: signature,
      body: body
    });
  }
}

module.exports = {
  Message: Message
};
