<commit_before>/*!
  * Copyright (c) 2015-2019 Cisco Systems, Inc. See LICENSE file.
*/

import '@webex/internal-plugin-mercury';

import {WebexPlugin} from '@webex/webex-core';

import Metrics from '../metrics';
import {trigger, eventType} from '../metrics/config';
import LoggerConfig from '../common/logs/logger-config';
import StaticConfig from '../common/config';
import LoggerProxy from '../common/logs/logger-proxy';
import Trigger from '../common/events/trigger-proxy';
import {
  MEETINGS,
  EVENTS,
  EVENT_TRIGGERS,
  READY,
  LOCUSEVENT,
  LOCUS_URL,
  ROAP,
  _MEETING_,
  _JOIN_,
  _LOCUS_ID_,
  _INCOMING_,
  LOCUS,
  CORRELATION_ID,
  SIP_URI,
  _LEFT_,
  _ID_
} from '../constants';
import MeetingInfo from '../meeting-info';
import Meeting from '../meeting';
import PersonalMeetingRoom from '../personal-meeting-room';
import Reachability from '../reachability';
import Request from '../meetings/request';
import StatsAnalyzer from '../analyzer/analyzer';
import StatsCalculator from '../analyzer/calculator';

import MeetingCollection from './collection';
import MeetingsUtil from './util';


/**
   * Meetings Ready Event
   * Emitted when the meetings instance on webex is ready
   * @event meetings:ready
   * @instance
   * @memberof Meetings
   */

/**
 * Meetings Registered Event
 * Emitted when the meetings instance has been registered and listening
 * @event meetings:registered
 * @instance
 * @memberof Meetings
 */

/**
    * Meeting Removed Event
    * Emitted when a meeting was removed from the cache of meetings
    * @event meeting:removed
    * @instance
    * @type {Object}
    * @property {String} meetingId the removed meeting
    * @property {Object} response the server response
    * @property {String} type what type of meeting it was
    * @memberof Meetings
    */

/**
    * Meeting Added Event
    * Emitted when a meeting was added to the cache of meetings
    * @event meeting:added
    * @instance
    * @type {Object}
    * @property {String} meetingId the added meeting
    * @property {String} type what type of meeting it was
    * @memberof Meetings
    */

/**
   * Maintain a cache of meetings and sync with services.
   * @class
   */
export default class Meetings extends WebexPlugin {
    namespace = MEETINGS;

    /**
     * Initializes the Meetings Plugin
     * @constructor
     * @public
     * @memberof Meetings
     */
    constructor(...args) {
      super(...args);
      /**
       * The MeetingInfo object to interact with server
       * @instance
       * @type {Object}
       * @private
       * @memberof Meetings
       */
      this.meetingInfo = new MeetingInfo({}, {parent: this.webex});
      /**
       * The Meetings request to interact with server
       * @instance
       * @type {Object}
       * @private
       * @memberof Meetings
       */
      this.request = new Request({}, {parent: this.webex});
      this.meetingCollection = new MeetingCollection();
      /**
       * The PersonalMeetingRoom object to interact with server
       * @instance
       * @type {Object}
       * @public
       * @memberof Meetings
       */
      this.personalMeetingRoom = new PersonalMeetingRoom({}, {parent: this.webex});
      /**
       * The Reachability object to interact with server, starts as null until {@link Meeting#setReachability} is called
       * starts as null
       * @instance
       * @type {Object}
       * @private
       * @memberof Meetings
       */
      this.reachability = null;

      /**
       * If the meetings plugin has been registered and listening via {@link Meetings#register}
       * @instance
       * @type {Boolean}
       * @public
       * @memberof Meetings
       */
      this.registered = false;

      this.onReady();
      Metrics.initialSetup(this.meetingCollection, this.webex.version);
    }

    /**
     * handle locus events and takes meeting actions with them as they come in
     * @param {Object} data a locus event
     * @param {String} data.locusUrl
     * @param {Object} data.locus
     * @param {String} data.eventType
     * @returns {undefined}
     * @private
     * @memberof Meetings
     */
    handleLocusEvent(data) {
      let meeting = null;

      // getting meeting by correlationId. This will happen for the new event
      // Either the locus
      // TODO : Add check for the callBack Address
      meeting = this.meetingCollection.getByKey(LOCUS_URL, data.locusUrl) ||
      this.meetingCollection.getByKey(CORRELATION_ID, MeetingsUtil.checkForCorrelationId(this.webex.internal.device.url, data.locus)) ||
      this.meetingCollection.getByKey(SIP_URI, data.locus.self && data.locus.self.callbackInfo && data.locus.self.callbackInfo.callbackAddress);

      if (!meeting) {
        // TODO: create meeting when we get a meeting object
        // const checkForEnded = (locus) => {
        // TODO: you already ended the meeting but you got an event later
        // Mainly for 1:1 Callsor meeting
        // Happens mainly after refresh

        // 1:1 Meeting
        // 1)  You ended a call before but you got a mercury event
        // Make sure end the call and cleanup the meeting only if the mercury
        // event says so
        // 2) Maintain lastSync time in the meetings object which helps to compare
        // If the meeting came befor or after the sync . ANy meeting start time before the sync time is invalid

        // For space Meeting
        // Check the locus object and see who has joined

        // };
        // rather then locus object change to locus url

        if (data.locus && data.locus.fullState && data.locus.fullState.state === LOCUS.STATE.INACTIVE) {
          // just ignore the event as its already ended and not active
          LoggerProxy.logger.warn('Meetings->handleLocusEvent#Locus event received for meeting, after it was ended.');

          return;
        }


        // When its wireless share or guest and user leaves the meeting we dont have to keep the meeting object
        // Any future events will be neglected

        if (data.locus && data.locus.self && (data.locus.self.state === _LEFT_ && data.locus.self.removed === true)) {
          // just ignore the event as its already ended and not active
          LoggerProxy.logger.warn('Meetings->handleLocusEvent#Locus event received for meeting, after it was ended.');

          return;
        }

        this.create(data.locus, _LOCUS_ID_).then((newMeeting) => {
          meeting = newMeeting;

          if (data.eventType === LOCUSEVENT.DIFFERENCE) {
            // its a delta object and we have a new meeting
            meeting.locusInfo.initialSetup(data.locus, meeting);
          }
          else {
            // Its a new meeting and have a fresh locus object
            meeting.locusInfo.initialSetup(data.locus);
          }
        })
          .finally(() => {
            // There will be cases where locus event comes in gets created and deleted because its a 1:1 and meeting gets deleted
            // because the other user left so before sending 'added' event make sure it exists in the collection

            if (this.getMeetingByType(_ID_, meeting.id)) {
            // Had to put `meeting:added here as the parsing of the locus object is done here
              Metrics.postEvent({event: eventType.NOTIFICATION_RECEIVED, meeting, data: {trigger: trigger.MERCURY_EVENT}});

              Metrics.postEvent({event: eventType.REMOTE_STARTED, meeting, data: {trigger: trigger.MERCURY_EVENT}});
              Trigger.trigger(
                this,
                {
                  file: 'meetings',
                  function: 'handleLocusEvent'
                },
                EVENT_TRIGGERS.MEETING_ADDED,
                {
                  meeting,
                  type: meeting.type === _MEETING_ ? _JOIN_ : _INCOMING_
                }
              );
            }
            else {
              // Meeting got added but was not found in the collection. It might have got destroyed
              LoggerProxy.logger.warn('Meetings->handleLocusEvent#Created and destroyed meeting object before sending an event');
            }
          });
      }
      else {
        Metrics.postEvent({event: eventType.NOTIFICATION_RECEIVED, meeting, data: {trigger: trigger.MERCURY_EVENT}});
        meeting.locusInfo.parse(meeting, data);
      }
    }

    /**
     * handles locus events through mercury that are not roap
     * @param {Object} envelope
     * @param {Object} envelope.data
     * @param {String} envelope.data.eventType
     * @returns {undefined}
     * @private
     * @memberof Meetings
     */
    handleLocusMercury(envelope) {
      const {data} = envelope;
      const {eventType} = data;

      if (eventType && eventType !== LOCUSEVENT.MESSAGE_ROAP) {
        this.handleLocusEvent(data);
      }
    }

    /**
     * registers for locus and roap mercury events
     * @returns {undefined}
     * @private
     * @memberof Meetings
     */
    listenForEvents() {
      this.webex.internal.mercury.on(LOCUSEVENT.LOCUS_MERCURY, (envelope) => {
        this.handleLocusMercury(envelope);
      });
      this.webex.internal.mercury.on(ROAP.ROAP_MERCURY, (envelope) => {
        MeetingsUtil.handleRoapMercury(envelope, this.meetingCollection);
      });
    }

    /**
     * stops listening for locus and roap mercury events
     * @returns {undefined}
     * @private
     * @memberof Meetings
     */
    stopListeningForEvents() {
      this.webex.internal.mercury.off(LOCUSEVENT.LOCUS_MERCURY);
      this.webex.internal.mercury.off(ROAP.ROAP_MERCURY);
    }

    /**
     * @returns {undefined}
     * @private
     * @memberof Meetings
     */
    onReady() {
      this.webex.once(READY, () => {
        StaticConfig.set(this.config);
        LoggerConfig.set(this.config.logging);
        LoggerProxy.set(this.webex.logger);
        Trigger.trigger(
          this,
          {
            file: 'meetings',
            function: 'onReady'
          },
          EVENT_TRIGGERS.MEETINGS_READY
        );
      });
    }

    /**
     * Explicitly sets up the meetings plugin by registering
     * the device, connecting to mercury, and listening for locus events.
     *
     * @returns {Promise}
     * @public
     * @memberof Meetings
     */
    register() {
      if (!this.webex.canAuthorize) {
        LoggerProxy.logger.error('meetings->register#ERROR, Unable to register, SDK cannot authorize');

        return Promise.reject(new Error('SDK cannot authorize'));
      }

      if (this.registered) {
        LoggerProxy.logger.info('meetings->register#INFO, Meetings plugin already registered');

        return Promise.resolve();
      }

      return this.webex.internal.device.register()
        .then(() => this.webex.internal.mercury.connect())
        .then(() => {
          this.listenForEvents();
          Trigger.trigger(
            this,
            {
              file: 'meetings',
              function: 'register'
            },
            EVENT_TRIGGERS.MEETINGS_REGISTERED
          );
          this.registered = true;
        })
        .catch((error) => {
          LoggerProxy.logger.error(`meetings->register#ERROR, Unable to register, ${error.message}`);

          return Promise.reject(error);
        });
    }

    /**
     * Explicitly tears down the meetings plugin by deregistering
     * the device, disconnecting from mercury, and stops listening to locus events
     *
     * @returns {Promise}
     * @public
     * @memberof Meetings
     */
    unregister() {
      if (!this.registered) {
        LoggerProxy.logger.info('meetings->unregister#INFO, Meetings plugin already unregistered');

        return Promise.resolve();
      }

      this.stopListeningForEvents();

      return this.webex.internal.mercury.disconnect()
        .then(() => this.webex.internal.device.unregister())
        .then(() => {
          Trigger.trigger(
            this,
            {
              file: 'meetings',
              function: 'unregister'
            },
            EVENT_TRIGGERS.MEETINGS_UNREGISTERED
          );
          this.registered = false;
        });
    }

    /**
     * initializes the reachability instance for Meetings
     * @returns {undefined}
     * @public
     * @memberof Meetings
     */
    setReachability() {
      this.reachability = new Reachability({}, {parent: this.webex});
    }

    /**
     * gets the reachability instance for Meetings
     * @returns {Reachability}
     * @public
     * @memberof Meetings
     */
    getReachability() {
      return this.reachability;
    }

    /**
     * initializes and starts gathering reachability for Meetings
     * @returns {undefined}
     * @public
     * @memberof Meetings
     */
    startReachability() {
      if (!this.reachability) {
        this.setReachability();
      }
      this.getReachability().gatherReachability();
    }

    /**
     * gets the personal meeting room instance, for saved PMR values for this user
     * @returns {PersonalMeetingRoom}
     * @public
     * @memberof Meetings
     */
    getPersonalMeetingRoom() {
      return this.personalMeetingRoom;
    }

    /**
     * @param {String} meetingId
     * @param {Object} reason
     * @param {String} type
     * @returns {Undefined}
     * @private
     * @memberof Meetings
     */
    destroy(meetingId, reason) {
      this.meetingCollection.delete(meetingId);
      Trigger.trigger(
        this,
        {
          file: 'meetings',
          function: 'destroy'
        },
        EVENT_TRIGGERS.MEETING_REMOVED,
        {
          meetingId,
          reason
        }
      );
    }

    /**
     * Create a meeting.
     * @param {string} destination - sipURL, spaceId, phonenumber, meeting link, or locus object}
     * @param {string} [type] - the optional specified type, such as locusId
     * @returns {Promise} A new Meeting.
     * @public
     * @memberof Meetings
     */
    create(destination, type = null) {
      // TODO: type should be from a dictionary
      // Check if there is already meeting
      const meeting = this.meetingCollection.getByKey(SIP_URI, destination);

      if (!meeting) {
        return this.createMeeting(destination, type)
          .then((meeting) => {
            if (meeting && meeting.on) {
              meeting.on(EVENTS.DESTROY_MEETING, (payload) => {
                this.destroy(payload.meetingId, payload.reason);
              });
            }
            else {
              LoggerProxy.logger.error(`meetings->create#ERROR, meeting does not have on method, will not be destroyed, meeting cleanup impossible for meeting: ${meeting}`);
            }

            return Promise.resolve(meeting);
          });
      }

      return Promise.resolve(meeting);
    }

    /**
     * @param {String} destination see create()
     * @param {String} type see create()
     * @returns {Promise} a new meeting instance complete with meeting info and destination
     * @private
     * @memberof Meetings
     */
    createMeeting(destination, type = null) {
      const meeting = new Meeting(
        {
          userId: this.webex.internal.device.userId,
          deviceUrl: this.webex.internal.device.url,
          roapSeq: 0,
          locus: type === _LOCUS_ID_ ? destination : null // pass the locus object if present
        },
        {
          parent: this.webex
        }
      );

      this.meetingCollection.set(meeting);

      return this.meetingInfo
        .fetchMeetingInfo(MeetingsUtil.extractDestination(destination, type), type)
        .then((info) => {
          meeting.parseMeetingInfo(info);
          meeting.meetingInfo = info ? info.body : null;

          return meeting;
        })
        .catch((err) => {
          // if there is no meeting info we assume its a 1:1 call or wireless share
          LoggerProxy.logger.info(`Meetings->createMeeting#Error ${err} fetching /meetingInfo for creation.`);
          LoggerProxy.logger.info('Meetings->createMeeting#Info assuming this destination is a 1:1 or wireless share');
          // We need to save this info for future reference
          meeting.destination = destination;

          return meeting;
        })
        .finally(() => {
          // For type LOCUS_ID we need to parse the locus object to get the information
          // about the caller and callee
          // TODO: check for a better solution
          if (type === _LOCUS_ID_) {
            return;
          }

          if (!meeting.sipUri) {
            meeting.setSipUri(destination);
          }
          // TODO: check if we have to move this to parser
          const meetingAddedType = MeetingsUtil.getMeetingAddedType(type);

          // We typically shouldn't need to trigger both and event and return a promise.
          // Is this a special case? We want to make the public API usage as simple as possible.
          Trigger.trigger(
            this,
            {
              file: 'meetings',
              function: 'createMeeting'
            },
            EVENT_TRIGGERS.MEETING_ADDED,
            {
              meeting,
              type: meetingAddedType
            }
          );
        });

      // Create the meeting calling the necessary service endpoints.

      // Internally, there are many more destinations:
      //
      // - locusID
      // - meetingURL
      // - globalMeetingID, e.g, *00*meetingID<commit_msg>fix(meetings): use async instead of finally<commit_after>/*!
  * Copyright (c) 2015-2019 Cisco Systems, Inc. See LICENSE file.
*/

import '@webex/internal-plugin-mercury';

import {WebexPlugin} from '@webex/webex-core';

import Metrics from '../metrics';
import {trigger, eventType} from '../metrics/config';
import LoggerConfig from '../common/logs/logger-config';
import StaticConfig from '../common/config';
import LoggerProxy from '../common/logs/logger-proxy';
import Trigger from '../common/events/trigger-proxy';
import {
  MEETINGS,
  EVENTS,
  EVENT_TRIGGERS,
  READY,
  LOCUSEVENT,
  LOCUS_URL,
  ROAP,
  _MEETING_,
  _JOIN_,
  _LOCUS_ID_,
  _INCOMING_,
  LOCUS,
  CORRELATION_ID,
  SIP_URI,
  _LEFT_,
  _ID_
} from '../constants';
import MeetingInfo from '../meeting-info';
import Meeting from '../meeting';
import PersonalMeetingRoom from '../personal-meeting-room';
import Reachability from '../reachability';
import Request from '../meetings/request';
import StatsAnalyzer from '../analyzer/analyzer';
import StatsCalculator from '../analyzer/calculator';

import MeetingCollection from './collection';
import MeetingsUtil from './util';


/**
   * Meetings Ready Event
   * Emitted when the meetings instance on webex is ready
   * @event meetings:ready
   * @instance
   * @memberof Meetings
   */

/**
 * Meetings Registered Event
 * Emitted when the meetings instance has been registered and listening
 * @event meetings:registered
 * @instance
 * @memberof Meetings
 */

/**
    * Meeting Removed Event
    * Emitted when a meeting was removed from the cache of meetings
    * @event meeting:removed
    * @instance
    * @type {Object}
    * @property {String} meetingId the removed meeting
    * @property {Object} response the server response
    * @property {String} type what type of meeting it was
    * @memberof Meetings
    */

/**
    * Meeting Added Event
    * Emitted when a meeting was added to the cache of meetings
    * @event meeting:added
    * @instance
    * @type {Object}
    * @property {String} meetingId the added meeting
    * @property {String} type what type of meeting it was
    * @memberof Meetings
    */

/**
   * Maintain a cache of meetings and sync with services.
   * @class
   */
export default class Meetings extends WebexPlugin {
    namespace = MEETINGS;

    /**
     * Initializes the Meetings Plugin
     * @constructor
     * @public
     * @memberof Meetings
     */
    constructor(...args) {
      super(...args);
      /**
       * The MeetingInfo object to interact with server
       * @instance
       * @type {Object}
       * @private
       * @memberof Meetings
       */
      this.meetingInfo = new MeetingInfo({}, {parent: this.webex});
      /**
       * The Meetings request to interact with server
       * @instance
       * @type {Object}
       * @private
       * @memberof Meetings
       */
      this.request = new Request({}, {parent: this.webex});
      this.meetingCollection = new MeetingCollection();
      /**
       * The PersonalMeetingRoom object to interact with server
       * @instance
       * @type {Object}
       * @public
       * @memberof Meetings
       */
      this.personalMeetingRoom = new PersonalMeetingRoom({}, {parent: this.webex});
      /**
       * The Reachability object to interact with server, starts as null until {@link Meeting#setReachability} is called
       * starts as null
       * @instance
       * @type {Object}
       * @private
       * @memberof Meetings
       */
      this.reachability = null;

      /**
       * If the meetings plugin has been registered and listening via {@link Meetings#register}
       * @instance
       * @type {Boolean}
       * @public
       * @memberof Meetings
       */
      this.registered = false;

      this.onReady();
      Metrics.initialSetup(this.meetingCollection, this.webex.version);
    }

    /**
     * handle locus events and takes meeting actions with them as they come in
     * @param {Object} data a locus event
     * @param {String} data.locusUrl
     * @param {Object} data.locus
     * @param {String} data.eventType
     * @returns {undefined}
     * @private
     * @memberof Meetings
     */
    handleLocusEvent(data) {
      let meeting = null;

      // getting meeting by correlationId. This will happen for the new event
      // Either the locus
      // TODO : Add check for the callBack Address
      meeting = this.meetingCollection.getByKey(LOCUS_URL, data.locusUrl) ||
      this.meetingCollection.getByKey(CORRELATION_ID, MeetingsUtil.checkForCorrelationId(this.webex.internal.device.url, data.locus)) ||
      this.meetingCollection.getByKey(SIP_URI, data.locus.self && data.locus.self.callbackInfo && data.locus.self.callbackInfo.callbackAddress);

      if (!meeting) {
        // TODO: create meeting when we get a meeting object
        // const checkForEnded = (locus) => {
        // TODO: you already ended the meeting but you got an event later
        // Mainly for 1:1 Callsor meeting
        // Happens mainly after refresh

        // 1:1 Meeting
        // 1)  You ended a call before but you got a mercury event
        // Make sure end the call and cleanup the meeting only if the mercury
        // event says so
        // 2) Maintain lastSync time in the meetings object which helps to compare
        // If the meeting came befor or after the sync . ANy meeting start time before the sync time is invalid

        // For space Meeting
        // Check the locus object and see who has joined

        // };
        // rather then locus object change to locus url

        if (data.locus && data.locus.fullState && data.locus.fullState.state === LOCUS.STATE.INACTIVE) {
          // just ignore the event as its already ended and not active
          LoggerProxy.logger.warn('Meetings->handleLocusEvent#Locus event received for meeting, after it was ended.');

          return;
        }


        // When its wireless share or guest and user leaves the meeting we dont have to keep the meeting object
        // Any future events will be neglected

        if (data.locus && data.locus.self && (data.locus.self.state === _LEFT_ && data.locus.self.removed === true)) {
          // just ignore the event as its already ended and not active
          LoggerProxy.logger.warn('Meetings->handleLocusEvent#Locus event received for meeting, after it was ended.');

          return;
        }

        this.create(data.locus, _LOCUS_ID_).then((newMeeting) => {
          meeting = newMeeting;

          if (data.eventType === LOCUSEVENT.DIFFERENCE) {
            // its a delta object and we have a new meeting
            meeting.locusInfo.initialSetup(data.locus, meeting);
          }
          else {
            // Its a new meeting and have a fresh locus object
            meeting.locusInfo.initialSetup(data.locus);
          }
        })
          .finally(() => {
            // There will be cases where locus event comes in gets created and deleted because its a 1:1 and meeting gets deleted
            // because the other user left so before sending 'added' event make sure it exists in the collection

            if (this.getMeetingByType(_ID_, meeting.id)) {
            // Had to put `meeting:added here as the parsing of the locus object is done here
              Metrics.postEvent({event: eventType.NOTIFICATION_RECEIVED, meeting, data: {trigger: trigger.MERCURY_EVENT}});

              Metrics.postEvent({event: eventType.REMOTE_STARTED, meeting, data: {trigger: trigger.MERCURY_EVENT}});
              Trigger.trigger(
                this,
                {
                  file: 'meetings',
                  function: 'handleLocusEvent'
                },
                EVENT_TRIGGERS.MEETING_ADDED,
                {
                  meeting,
                  type: meeting.type === _MEETING_ ? _JOIN_ : _INCOMING_
                }
              );
            }
            else {
              // Meeting got added but was not found in the collection. It might have got destroyed
              LoggerProxy.logger.warn('Meetings->handleLocusEvent#Created and destroyed meeting object before sending an event');
            }
          });
      }
      else {
        Metrics.postEvent({event: eventType.NOTIFICATION_RECEIVED, meeting, data: {trigger: trigger.MERCURY_EVENT}});
        meeting.locusInfo.parse(meeting, data);
      }
    }

    /**
     * handles locus events through mercury that are not roap
     * @param {Object} envelope
     * @param {Object} envelope.data
     * @param {String} envelope.data.eventType
     * @returns {undefined}
     * @private
     * @memberof Meetings
     */
    handleLocusMercury(envelope) {
      const {data} = envelope;
      const {eventType} = data;

      if (eventType && eventType !== LOCUSEVENT.MESSAGE_ROAP) {
        this.handleLocusEvent(data);
      }
    }

    /**
     * registers for locus and roap mercury events
     * @returns {undefined}
     * @private
     * @memberof Meetings
     */
    listenForEvents() {
      this.webex.internal.mercury.on(LOCUSEVENT.LOCUS_MERCURY, (envelope) => {
        this.handleLocusMercury(envelope);
      });
      this.webex.internal.mercury.on(ROAP.ROAP_MERCURY, (envelope) => {
        MeetingsUtil.handleRoapMercury(envelope, this.meetingCollection);
      });
    }

    /**
     * stops listening for locus and roap mercury events
     * @returns {undefined}
     * @private
     * @memberof Meetings
     */
    stopListeningForEvents() {
      this.webex.internal.mercury.off(LOCUSEVENT.LOCUS_MERCURY);
      this.webex.internal.mercury.off(ROAP.ROAP_MERCURY);
    }

    /**
     * @returns {undefined}
     * @private
     * @memberof Meetings
     */
    onReady() {
      this.webex.once(READY, () => {
        StaticConfig.set(this.config);
        LoggerConfig.set(this.config.logging);
        LoggerProxy.set(this.webex.logger);
        Trigger.trigger(
          this,
          {
            file: 'meetings',
            function: 'onReady'
          },
          EVENT_TRIGGERS.MEETINGS_READY
        );
      });
    }

    /**
     * Explicitly sets up the meetings plugin by registering
     * the device, connecting to mercury, and listening for locus events.
     *
     * @returns {Promise}
     * @public
     * @memberof Meetings
     */
    register() {
      if (!this.webex.canAuthorize) {
        LoggerProxy.logger.error('meetings->register#ERROR, Unable to register, SDK cannot authorize');

        return Promise.reject(new Error('SDK cannot authorize'));
      }

      if (this.registered) {
        LoggerProxy.logger.info('meetings->register#INFO, Meetings plugin already registered');

        return Promise.resolve();
      }

      return this.webex.internal.device.register()
        .then(() => this.webex.internal.mercury.connect())
        .then(() => {
          this.listenForEvents();
          Trigger.trigger(
            this,
            {
              file: 'meetings',
              function: 'register'
            },
            EVENT_TRIGGERS.MEETINGS_REGISTERED
          );
          this.registered = true;
        })
        .catch((error) => {
          LoggerProxy.logger.error(`meetings->register#ERROR, Unable to register, ${error.message}`);

          return Promise.reject(error);
        });
    }

    /**
     * Explicitly tears down the meetings plugin by deregistering
     * the device, disconnecting from mercury, and stops listening to locus events
     *
     * @returns {Promise}
     * @public
     * @memberof Meetings
     */
    unregister() {
      if (!this.registered) {
        LoggerProxy.logger.info('meetings->unregister#INFO, Meetings plugin already unregistered');

        return Promise.resolve();
      }

      this.stopListeningForEvents();

      return this.webex.internal.mercury.disconnect()
        .then(() => this.webex.internal.device.unregister())
        .then(() => {
          Trigger.trigger(
            this,
            {
              file: 'meetings',
              function: 'unregister'
            },
            EVENT_TRIGGERS.MEETINGS_UNREGISTERED
          );
          this.registered = false;
        });
    }

    /**
     * initializes the reachability instance for Meetings
     * @returns {undefined}
     * @public
     * @memberof Meetings
     */
    setReachability() {
      this.reachability = new Reachability({}, {parent: this.webex});
    }

    /**
     * gets the reachability instance for Meetings
     * @returns {Reachability}
     * @public
     * @memberof Meetings
     */
    getReachability() {
      return this.reachability;
    }

    /**
     * initializes and starts gathering reachability for Meetings
     * @returns {undefined}
     * @public
     * @memberof Meetings
     */
    startReachability() {
      if (!this.reachability) {
        this.setReachability();
      }
      this.getReachability().gatherReachability();
    }

    /**
     * gets the personal meeting room instance, for saved PMR values for this user
     * @returns {PersonalMeetingRoom}
     * @public
     * @memberof Meetings
     */
    getPersonalMeetingRoom() {
      return this.personalMeetingRoom;
    }

    /**
     * @param {String} meetingId
     * @param {Object} reason
     * @param {String} type
     * @returns {Undefined}
     * @private
     * @memberof Meetings
     */
    destroy(meetingId, reason) {
      this.meetingCollection.delete(meetingId);
      Trigger.trigger(
        this,
        {
          file: 'meetings',
          function: 'destroy'
        },
        EVENT_TRIGGERS.MEETING_REMOVED,
        {
          meetingId,
          reason
        }
      );
    }

    /**
     * Create a meeting.
     * @param {string} destination - sipURL, spaceId, phonenumber, meeting link, or locus object}
     * @param {string} [type] - the optional specified type, such as locusId
     * @returns {Promise} A new Meeting.
     * @public
     * @memberof Meetings
     */
    create(destination, type = null) {
      // TODO: type should be from a dictionary
      // Check if there is already meeting
      const meeting = this.meetingCollection.getByKey(SIP_URI, destination);

      if (!meeting) {
        return this.createMeeting(destination, type)
          .then((meeting) => {
            if (meeting && meeting.on) {
              meeting.on(EVENTS.DESTROY_MEETING, (payload) => {
                this.destroy(payload.meetingId, payload.reason);
              });
            }
            else {
              LoggerProxy.logger.error(`meetings->create#ERROR, meeting does not have on method, will not be destroyed, meeting cleanup impossible for meeting: ${meeting}`);
            }

            return Promise.resolve(meeting);
          });
      }

      return Promise.resolve(meeting);
    }

    /**
     * @param {String} destination see create()
     * @param {String} type see create()
     * @returns {Promise} a new meeting instance complete with meeting info and destination
     * @private
     * @memberof Meetings
     */
    async createMeeting(destination, type = null) {
      const meeting = new Meeting(
        {
          userId: this.webex.internal.device.userId,
          deviceUrl: this.webex.internal.device.url,
          roapSeq: 0,
          locus: type === _LOCUS_ID_ ? destination : null // pass the locus object if present
        },
        {
          parent: this.webex
        }
      );

      this.meetingCollection.set(meeting);

      try {
        const info = await this.meetingInfo.fetchMeetingInfo(MeetingsUtil.extractDestination(destination, type), type);

        meeting.parseMeetingInfo(info);
        meeting.meetingInfo = info ? info.body : null;
      }
      catch (err) {
        // if there is no meeting info we assume its a 1:1 call or wireless share
        LoggerProxy.logger.info(`Meetings->createMeeting#Error ${err} fetching /meetingInfo for creation.`);
        LoggerProxy.logger.info('Meetings->createMeeting#Info assuming this destination is a 1:1 or wireless share');
        // We need to save this info for future reference
        meeting.destination = destination;
      }
      finally {
        // For type LOCUS_ID we need to parse the locus object to get the information
        // about the caller and callee
        // Meeting Added event will be created in `handleLocusEvent`
        if (type !== _LOCUS_ID_) {
          if (!meeting.sipUri) {
            meeting.setSipUri(destination);
          }

          // TODO: check if we have to move this to parser
          const meetingAddedType = MeetingsUtil.getMeetingAddedType(type);

          // We typically shouldn't need to trigger both and event and return a promise.
          // Is this a special case? We want to make the public API usage as simple as possible.
          Trigger.trigger(
            this,
            {
              file: 'meetings',
              function: 'createMeeting'
            },
            EVENT_TRIGGERS.MEETING_ADDED,
            {
              meeting,
              type: meetingAddedType
            }
          );
        }
      }

      return meeting;

      // Create the meeting calling the necessary service endpoints.

      // Internally, there are many more destinations:
      //
      // - locusID
      // - meetingURL
      // - globalMeetingID, e.g, *00*meetingID
{'fraction_non_alphanumeric': 0.102724584421923, 'fraction_numerical': 0.00132066262811863, 'mean_word_length': 2.079480152064362, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 52, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2653373', 'n_tokens_mistral': 9842, 'n_tokens_neox': 9534, 'n_words': 2724}