import { EJSON } from 'bson';
import { Chatty } from '@looker/chatty';
import _isEqual from 'lodash/isEqual';
import _isEmpty from 'lodash/isEmpty';

// Given an object `Target`, find all property names of type `Type`
// Given an object `Target`, filter out all properties that aren't of type `Type`
function createElement(name, props = {}, children = []) {
  const element = document.createElement(name);

  for (const [name, value] of Object.entries(props)) {
    if (name === 'style') {
      Object.assign(element.style, props.style);
    } else {
      element.setAttribute(name, value);
    }
  }

  for (const child of Array.isArray(children) ? children : [children]) {
    element.append(child);
  }

  return element;
}

/**
 * Shared options for embedding
 */

/**
 * The set of options that you can use when both creating an {@link EmbedSDK} object or using {@link EmbedSDK.createChart}.
 */

/**
 * The set of options that you can use when both creating an {@link EmbedSDK} object or using {@link EmbedSDK.createDashboard}.
 */
//TODO find a way to reuse types defined in "packages/charts-frontend/src/utils/chart/events/event-payload-types.ts"
let ThemeEnum;

(function (ThemeEnum) {
  ThemeEnum["DARK"] = "dark";
  ThemeEnum["LIGHT"] = "light";
})(ThemeEnum || (ThemeEnum = {}));

let ScalingEnum;

(function (ScalingEnum) {
  ScalingEnum["FIXED"] = "fixed";
  ScalingEnum["SCALE"] = "scale";
})(ScalingEnum || (ScalingEnum = {}));

/**
 * Retrieve embed options that are shared.
 *
 * Validates the values passed in as well.
 */
const getSharedEmbedOptions = options => {
  const {
    background,
    baseUrl,
    autoRefresh,
    maxDataAge,
    width,
    height,
    theme,
    showAttribution,
    getUserToken
  } = options;

  if (typeof baseUrl !== 'string' || baseUrl.length === 0) {
    throw new Error('Base URL must be a valid URL');
  }

  if (background !== undefined && typeof background !== 'string') {
    throw new Error('background must be a string if specified');
  }

  if (autoRefresh !== undefined && typeof autoRefresh !== 'boolean') {
    throw new Error('autoRefresh must be a boolean if specified');
  }

  if (maxDataAge !== undefined && typeof maxDataAge !== 'number') {
    throw new Error('maxDataAge must be a number if specified');
  }

  if (width !== undefined && !['number', 'string'].includes(typeof width)) {
    throw new Error('Width must be a string or number if specified');
  }

  if (height !== undefined && !['number', 'string'].includes(typeof height)) {
    throw new Error('Height must be a string or number if specified');
  }

  if (theme !== undefined && typeof theme !== 'string') {
    throw new Error('Theme must be a string if specified');
  }

  if (showAttribution !== undefined && typeof showAttribution !== 'boolean') {
    throw new Error('Attribution must be a boolean value if specified');
  }

  if (getUserToken !== undefined && typeof getUserToken !== 'function') {
    throw new Error('getUserToken must be a function');
  }

  return {
    background,
    baseUrl,
    autoRefresh,
    maxDataAge,
    width,
    height,
    theme,
    showAttribution,
    getUserToken
  };
};
const getPathname = (url, pathname) => {
  return [url.pathname, url.pathname.slice(-1) === '/' ? '' : '/', // Add trailing slash if not there
  pathname].join('');
};
/**
 * Constructs the chart iframe URL from the baseUrl, chartId & tenantId
 */

const getChartUrl = options => {
  try {
    const url = new URL(options.baseUrl);
    url.pathname = getPathname(url, 'embed/charts');
    url.search = `id=${options.chartId}&sdk=2`;

    if (options.autoRefresh === false) {
      url.search += `&autorefresh=false`;
    } else if (options.autoRefresh === undefined) {
      url.search += options.refreshInterval ? `&autorefresh=${options.refreshInterval}` : '';
    }

    if (options.maxDataAge !== undefined) {
      url.search += `&maxDataAge=${options.maxDataAge}`;
    }

    if (options.filter) {
      url.search += `&filter=${encodeURIComponent(EJSON.stringify(options.filter, {
        relaxed: false
      }))}`;
    }

    if (options.theme) {
      url.search += `&theme=${options.theme}`;
    }

    if (options.showAttribution === false) {
      url.search += `&attribution=false`;
    }

    return url.toString();
  } catch (e) {
    throw new Error('Base URL must be a valid URL');
  }
};
/**
 * Constructs the dashboard iframe URL from the baseUrl, dashboardId & tenantId
 */

const getDashboardUrl = options => {
  try {
    const url = new URL(options.baseUrl);
    url.pathname = getPathname(url, 'embed/dashboards');
    url.search = `id=${options.dashboardId}&sdk=1`;

    if (options.autoRefresh === false) {
      url.search += `&autoRefresh=false`;
    }

    if (options.maxDataAge !== undefined) {
      url.search += `&maxDataAge=${options.maxDataAge}`;
    }

    if (options.showTitleAndDesc === true) {
      url.search += `&showTitleAndDesc=true`;
    }

    if (options.widthMode) {
      url.search += `&scalingWidth=${options.widthMode}`;
    }

    if (options.heightMode) {
      url.search += `&scalingHeight=${options.heightMode}`;
    }

    if (options.theme) {
      url.search += `&theme=${options.theme}`;
    }

    if (options.chartsBackground) {
      url.search += `&chartsBackground=${options.chartsBackground}`;
    }

    if (options.showAttribution === false) {
      url.search += `&attribution=false`;
    }

    return url.toString();
  } catch (e) {
    throw new Error('Base URL must be a valid URL');
  }
};
/*
  Parses a CSS Measurement from an unknown value
  - if it's a string, we trust that it is well-formed
  - if it's a number, we assume the units are pixels
  - otherwise we return null
*/

const parseCSSMeasurement = value => {
  if (typeof value === 'string') return value;
  if (typeof value === 'number') return `${value}px`;
  return null;
};
/**
 * Returns the background after validation checks
 * or default background based on theme if not set
 */

const getBackground = (background, theme, lightBackground, darkBackground) => {
  if (typeof background === 'string' && background.length > 0) return background;
  if (theme === 'dark') return darkBackground;
  return lightBackground;
};

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

class BaseEmbedItem {
  constructor() {
    _defineProperty(this, "iframe", void 0);

    _defineProperty(this, "connection", void 0);

    _defineProperty(this, "name", void 0);

    _defineProperty(this, "ERRORS", void 0);

    _defineProperty(this, "COLOUR", void 0);

    _defineProperty(this, "options", void 0);
  }

  /**
   * Renders an embeddable item into the given `container`.
   *
   * This method should only be called once, and successive attempts to call `render`
   * will fail with an error.
   *
   * @returns a promise that will resolve once the item has successfully been embedded
   */
  async render(container) {
    if (this.iframe) {
      throw new Error(this.ERRORS.IFRAME);
    } // Create styled container


    const embedRoot = this._configureEmbedRoot(createElement('div', {
      style: {
        position: 'relative',
        overflow: 'hidden',
        minHeight: Boolean(this.options.height) ? 0 : '15px',
        width: parseCSSMeasurement(this.options.width) || '100%',
        height: parseCSSMeasurement(this.options.height) || '100%'
      }
    })); // Create host


    const host = this._configureHost(Chatty.createHost(this.getEmbedUrl()).withSandboxAttribute('allow-scripts').withSandboxAttribute('allow-same-origin').withSandboxAttribute('allow-popups').withSandboxAttribute('allow-popups-to-escape-sandbox').appendTo(embedRoot)).build(); // Customise IFrame styles


    host.iframe.setAttribute('aria-label', this.name);
    Object.assign(host.iframe.style, {
      position: 'absolute',
      top: 0,
      left: 0,
      border: 0,
      width: '100%',
      height: '100%'
    }); // Remove any existing nodes in our target container

    while (container.firstChild) container.removeChild(container.firstChild);

    container.appendChild(embedRoot); // connect to iframe

    this.connection = await host.connect();
    this.iframe = host.iframe;

    this._setBackground(this.options.background, this.options.theme); // configure token if needed


    await this._retrieveAndSetToken(); // Ready to actually render Embedded Item

    await this._send('ready');
  }
  /**
   * @returns whether auto refreshing is enabled
   */


  async isAutoRefresh() {
    const [result] = await this._send('get', 'autoRefresh'); // autoRefresh from embed chart may be a number when refreshInterval is set

    return typeof result === 'number' || typeof result === 'boolean' ? Boolean(result) : Promise.reject('unexpected response received from iframe');
  }
  /**
   * Enable/Disable auto refreshing.
   */


  async setAutoRefresh(value) {
    if (typeof value !== 'boolean') {
      return Promise.reject('autoRefresh property value should be a boolean');
    }

    await this._send('set', 'autoRefresh', value);
  }
  /**
   * @returns the number of seconds before a chart or dashboard's data expires
   */


  async getMaxDataAge() {
    const [result] = await this._send('get', 'maxDataAge');
    return typeof result === 'number' ? result : Promise.reject('unexpected response received from iframe');
  }
  /**
   * Set the number of seconds a chart or dashboard's data expires.
   */


  async setMaxDataAge(value) {
    if (typeof value !== 'number') {
      return Promise.reject('maxDataAge property value should be a number');
    }

    await this._send('set', 'maxDataAge', value);
  }
  /**
   * Sets the color scheme to apply to the chart or dashboard.
   *
   * If the theme is set to 'dark' and you have specified a custom background color, you should ensure that your background color has appropriate contrast.
   */


  async setTheme(value) {
    if (typeof value !== 'string') {
      return Promise.reject('theme property value should be a string');
    } // if invalid theme string is provided, default it to light


    const newTheme = Object.values(ThemeEnum).includes(value) ? value : ThemeEnum.LIGHT;
    await this._send('set', 'theme', newTheme);

    this._setBackground(this.options.background, newTheme);
  }
  /**
   * @returns the current theme applied to the chart or dashboard
   */


  async getTheme() {
    const [result] = await this._send('get', 'theme');
    return typeof result === 'string' ? result : Promise.reject('unexpected response received from iframe');
  }

  _configureHost(hostBuilder) {
    return hostBuilder.on('refreshToken', () => this._retrieveAndSetToken());
  }

  _configureEmbedRoot(embedRoot) {
    return embedRoot;
  }

  _setBackground(background, theme) {
    this.iframe.style.backgroundColor = getBackground(background, theme, this.COLOUR.LIGHT, this.COLOUR.DARK);
  }

  async _retrieveAndSetToken() {
    if (this.options.getUserToken) {
      const token = await this.options.getUserToken();
      await this._send('set', 'token', token);
    }
  }
  /**
   * Send message to embedded app.
   */


  _send(eventName, ...payload) {
    if (this.connection) {
      return this.connection.sendAndReceive(eventName, ...payload);
    }

    return Promise.reject(this.ERRORS.SEND);
  }

}

function _defineProperty$1(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

let eventHandlerIndex = Date.now();
function EventSource(Sender) {
  var _temp;

  return _temp = class extends Sender {
    constructor(...args) {
      super(...args);

      _defineProperty$1(this, "_eventHandlers", {
        click: {} // refresh: {} To be added soon

      });
    }

    /**
     * Handle the event sent from embedded app.
     */
    _handleEvent(event, payload, handlerIds) {
      const handlers = this._eventHandlers[event];

      for (const id of handlerIds) {
        try {
          var _handlers$id;

          // since communication between host and SDK is async,
          // it's possible that some handlers have been removed;
          // thus needs to check if handler still exists before calling
          (_handlers$id = handlers[id]) === null || _handlers$id === void 0 ? void 0 : _handlers$id.handle(payload);
        } catch (error) {
          console.warn(`Error calling handler for event [${event}]: ${error}`);
        }
      }
    }
    /**
     * Sets an event listener
     * @param event - the event you are subscribing to
     * @param eventHandler - the callback to be executed when the event is triggered
     * @param options - optional options object, can be used to customise when handler is called
     */


    addEventListener(event, eventHandler, options) {
      var _h$options$includes;

      const handlers = this._eventHandlers[event];

      if (!handlers) {
        throw new Error(`Not supported event: ${event}`);
      }

      const h = {
        handle: eventHandler,
        options: {
          includes: options === null || options === void 0 ? void 0 : options.includes
        }
      };

      if ((_h$options$includes = h.options.includes) !== null && _h$options$includes !== void 0 && _h$options$includes.every(f => _isEmpty(f))) {
        // eslint-disable-next-line no-console
        console.warn('Empty includes filters out all events. Event handler will never be called. Is this intended?');
      } // ignore if same handler and options have been added already


      if (!Object.keys(handlers).some(id => _isEqual(handlers[id], h))) {
        const handlerId = (++eventHandlerIndex).toString(36);
        handlers[handlerId] = h;
        return this._send('eventHandler', event, {
          handlerId,
          options: h.options
        });
      }

      return Promise.resolve();
    }
    /**
     * Removes an event listener
     * @param event - the event you are unsubscribing from
     * @param eventHandler - the event listener function you are unsubscribing from
     * @param options - optional options object used when addEventListener
     */


    removeEventListener(event, eventHandler, options) {
      const handlers = this._eventHandlers[event];

      if (!handlers) {
        throw new Error(`Not supported event: ${event}`);
      }

      const h = {
        handle: eventHandler,
        options: {
          includes: options === null || options === void 0 ? void 0 : options.includes
        }
      };
      const handlerId = Object.keys(handlers).find(id => _isEqual(handlers[id], h));

      if (handlerId) {
        delete handlers[handlerId];
        return this._send('eventHandler', event, {
          handlerId
        });
      }

      return Promise.resolve();
    }

  }, _temp;
}

function Refreshable(Sender) {
  return class extends Sender {
    /**
     * Triggers a refresh of the chart or dashboard (if it has been embedded).
     *
     * @returns a promise that resolves once the chart or dashboard updated its data
     */
    async refresh() {
      await this._send('refresh');
    }

  };
}

function _defineProperty$2(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

const getChartOptions = options => {
  if (typeof options !== 'object' || options === null) {
    throw new Error('Options argument must be an object');
  }

  const sharedEmbedOptions = getSharedEmbedOptions(options);
  const {
    chartId,
    filter,
    refreshInterval
  } = options; // Verify chart embed options

  if (typeof chartId !== 'string' || chartId.length === 0) {
    throw new Error('Chart ID must be specified');
  }

  if (filter !== undefined && (!filter || typeof filter !== 'object')) {
    throw new Error('Filter must be an object if specified');
  }

  if (refreshInterval !== undefined && typeof refreshInterval !== 'number') {
    throw new Error('refreshInterval interval must be a number if specified');
  }

  return { ...sharedEmbedOptions,
    chartId,
    filter,
    refreshInterval
  };
};

class ChartEventSender extends BaseEmbedItem {
  /** @ignore */
  constructor(options) {
    super();

    _defineProperty$2(this, "name", 'Embedded Chart');

    _defineProperty$2(this, "ERRORS", {
      SEND: 'Chart has not been rendered. Ensure that you wait for the promise returned by `chart.render()` before trying to manipulate a chart.',
      IFRAME: 'A chart can only be rendered into a container once'
    });

    _defineProperty$2(this, "COLOUR", {
      LIGHT: '#FFFFFF',
      DARK: '#21313C'
    });

    _defineProperty$2(this, "options", void 0);

    this.options = getChartOptions(options);
  }

  getEmbedUrl() {
    return getChartUrl(this.options);
  }

}
/**
 * # Chart
 *
 * Allows you to interact and embed charts into your application.
 *
 * ```js
 * const sdk = new EmbedSDK({ ... });
 * const chart = sdk.createChart({ ... });
 *
 * // renders a chart
 * chart.render(document.getElementById('embed-chart'));
 *
 * // dynamically set a filter
 * chart.setFilter({ age: { $gt: 50 } });
 * ```
 */


class Chart extends Refreshable(EventSource(ChartEventSender)) {
  /**
   * @returns the number of seconds a chart will wait before refreshing
   * @deprecated This method is deprecated. Please use the 'autoRefresh' option with the 'maxDataAge' option to configure how often the chart refreshes.
   */
  async getRefreshInterval() {
    const [result] = await this._send('get', 'autorefresh');
    console.warn("The 'getRefreshInterval' method is deprecated. Please use the 'autoRefresh' option with the 'maxDataAge' option to configure how often the chart refreshes.");
    return typeof result === 'number' ? result : Promise.reject('unexpected response received from iframe');
  }
  /**
   * Set the number of seconds a chart will wait before refreshing.
   *
   * The minimum refresh interval is 10 seconds. To disable, set the refresh interval to 0.
   * @deprecated This method is deprecated. Please use the 'autoRefresh' option with the 'maxDataAge' option to configure how often the chart refreshes.
   */


  async setRefreshInterval(value) {
    if (typeof value !== 'number') {
      return Promise.reject('refreshInterval property value should be a number');
    }

    console.warn("The 'setRefreshInterval' method is deprecated. Please use the 'autoRefresh' option with the 'maxDataAge' option to configure how often the chart refreshes.");
    await this._send('set', 'autorefresh', value);
  }
  /**
   * @returns the current filter applied to the embedded chart.
   */


  async getFilter() {
    const [result] = await this._send('get', 'filter');
    return typeof result === 'object' && result !== null ? result : Promise.reject('unexpected response received from iframe');
  }
  /**
   * Sets the filter to apply to the embedded chart.
   *
   * This expects an object that contains a valid [query operators](https://docs.mongodb.com/manual/reference/operator/query/#query-selectors).
   * Any fields referenced in this filter are expected to be whitelisted in the "Embed Chart" dialog for each Chart you wish to filter on.
   */


  async setFilter(value) {
    if (typeof value !== 'object' || value === null || Array.isArray(value)) {
      return Promise.reject('filter property value should be an object');
    }

    await this._send('set', 'filter', EJSON.stringify(value, {
      relaxed: false
    }));
  }
  /**
   * @returns the current highlight applied to the embedded chart.
   */


  async getHighlight() {
    const [result] = await this._send('get', 'highlight');
    return typeof result === 'object' && result !== null ? result : Promise.reject('unexpected response received from iframe');
  }
  /**
   * Sets the highlight to apply to the embedded chart.
   *
   * This is the exact same object that can be used in 'setFilter'.
   * However, it [doesn't support some query expressions](https://docs.mongodb.com/charts/saas/embedded-chart-options/)
   * @param value The highlight object to be applied to the chart
   */


  async setHighlight(value) {
    if (typeof value !== 'object' || value === null || Array.isArray(value)) {
      return Promise.reject('highlight property value should be an object');
    }

    await this._send('set', 'highlight', EJSON.stringify(value, {
      relaxed: false
    }));
  }

  _configureHost(hostBuilder) {
    return super._configureHost(hostBuilder).on('event', this._handleEvent.bind(this));
  }
  /**
   * @returns the data of the embedded chart.
   */


  async getData() {
    const [result] = await this._send('get', 'data');
    return typeof result === 'object' && result !== null ? result : Promise.reject('unexpected response received from iframe');
  }

}

class DashboardChartEventSender {
  constructor(chartId, dashboard) {
    this.chartId = chartId;
    this.dashboard = dashboard;
  }
  /**
   * Send message to embedded app via dashboard.
   */


  _send(msgName, ...payload) {
    return this.dashboard._send(msgName, ...payload, this.chartId);
  }

}

class DashboardChart extends Refreshable(EventSource(DashboardChartEventSender)) {}

function _defineProperty$3(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

const getDashboardOptions = options => {
  if (typeof options !== 'object' || options === null) {
    throw new Error('Options argument must be an object');
  }

  const sharedEmbedOptions = getSharedEmbedOptions(options);
  const {
    dashboardId,
    chartsBackground,
    widthMode,
    heightMode,
    showTitleAndDesc
  } = options; // Verify dashboard embed options

  if (typeof dashboardId !== 'string' || dashboardId.length === 0) {
    throw new Error('dashboardId must be specified');
  }

  if (chartsBackground !== undefined && typeof chartsBackground !== 'string') {
    throw new Error('chartsBackground must be a string if specified');
  }

  if (widthMode !== undefined && typeof widthMode !== 'string') {
    throw new Error('widthMode must be a string if specified');
  }

  if (widthMode !== undefined && widthMode !== ScalingEnum.FIXED && widthMode !== ScalingEnum.SCALE) {
    throw new Error(`widthMode must be "${ScalingEnum.FIXED}" or "${ScalingEnum.SCALE}"`);
  }

  if (heightMode !== undefined && typeof heightMode !== 'string') {
    throw new Error('heightMode must be a string if specified');
  }

  if (heightMode !== undefined && heightMode !== ScalingEnum.FIXED && heightMode !== ScalingEnum.SCALE) {
    throw new Error(`heightMode must be "${ScalingEnum.FIXED}" or "${ScalingEnum.SCALE}"`);
  }

  if (showTitleAndDesc !== undefined && typeof showTitleAndDesc !== 'boolean') {
    throw new Error('showTitleAndDesc must be a boolean value if specified');
  }

  return { ...sharedEmbedOptions,
    dashboardId,
    chartsBackground,
    widthMode,
    heightMode,
    showTitleAndDesc
  };
};

class DashboardEventSender extends BaseEmbedItem {
  /** @ignore */
  constructor(options) {
    super();

    _defineProperty$3(this, "name", 'Embedded Dashboard');

    _defineProperty$3(this, "ERRORS", {
      SEND: 'Dashboard has not been rendered. Ensure that you wait for the promise returned by `dashboard.render()` before trying to manipulate a dashboard.',
      IFRAME: 'A dashboard can only be rendered into a container once'
    });

    _defineProperty$3(this, "COLOUR", {
      LIGHT: '#F1F5F4',
      DARK: '#12212C'
    });

    _defineProperty$3(this, "options", void 0);

    this.options = getDashboardOptions(options);
  }

  getEmbedUrl() {
    return getDashboardUrl(this.options);
  }

}
/**
 * # Dashboard
 *
 * Allows you to interact and embed dashboards into your application.
 *
 * ```js
 * const sdk = new EmbedSDK({ ... });
 * const dashboard = sdk.createDashboard({ ... });
 *
 * // renders a dashboard
 * dashboard.render(document.getElementById('embed-dashboard'));
 *
 * ```
 */


class Dashboard extends Refreshable(DashboardEventSender) {
  constructor(...args) {
    super(...args);

    _defineProperty$3(this, "charts", {});
  }

  /**
   * @returns current chartsBackground or empty string if not set
   */
  async getChartsBackground() {
    const [result] = await this._send('get', 'chartsBackground');
    return typeof result === 'string' ? result : Promise.reject('unexpected response received from iframe');
  }
  /**
   * Set a custom background color for all charts.
   * To clear existing value, set it to empty string.
   */


  async setChartsBackground(value) {
    if (typeof value !== 'string') {
      return Promise.reject('chartsBackground property value should be a string');
    }

    await this._send('set', 'chartsBackground', value);
  }
  /**
   * @returns whether attribution logo should be shown
   */


  async isShowAttribution() {
    const [result] = await this._send('get', 'attribution');
    return typeof result === 'boolean' ? Boolean(result) : Promise.reject('unexpected response received from iframe');
  }
  /**
   * Enable/Disable attribution logo.
   */


  async setShowAttribution(value) {
    if (typeof value !== 'boolean') {
      return Promise.reject('showAttribution property value should be a boolean');
    }

    await this._send('set', 'attribution', value);
  }
  /**
   * @returns get width scaling mode of embedded dashboard
   */


  async getWidthMode() {
    const [result] = await this._send('get', 'scalingWidth');
    return result === ScalingEnum.FIXED || result === ScalingEnum.SCALE ? result : Promise.reject('unexpected response received from iframe');
  }
  /**
   * Set width scaling mode for embedded dashboard
   */


  async setWidthMode(value) {
    if (!['fixed', 'scale'].includes(value)) {
      return Promise.reject('widthMode property value should be a string value of "fixed" or "scale"');
    }

    await this._send('set', 'scalingWidth', value);
  }
  /**
   * @returns get height scaling mode of embedded dashboard
   */


  async getHeightMode() {
    const [result] = await this._send('get', 'scalingHeight');
    return result === 'fixed' || result === 'scale' ? result : Promise.reject('unexpected response received from iframe');
  }
  /**
   * Set height scaling mode for embedded dashboard
   */


  async setHeightMode(value) {
    if (!['fixed', 'scale'].includes(value)) {
      return Promise.reject('heightMode property value should be a string value of "fixed" or "scale"');
    }

    await this._send('set', 'scalingHeight', value);
  }
  /**
   * @returns get the dashboard chart with specified id
   */


  async getChart(id) {
    if (!this.charts[id]) {
      const [chartIds] = await this._send('get', 'charts', [id]);

      if (!Array.isArray(chartIds)) {
        return Promise.reject('unexpected response received from iframe');
      }

      if (chartIds.length !== 1) {
        return Promise.reject('Invalid chart id: ' + id);
      }

      this.charts[id] = new DashboardChart(id, this);
    }

    return this.charts[id];
  }
  /**
   * @returns all charts on the dashboard
   */


  async getAllCharts() {
    const [chartIds] = await this._send('get', 'charts');

    if (!Array.isArray(chartIds)) {
      return Promise.reject('unexpected response received from iframe');
    }

    const charts = [];
    chartIds.forEach(id => {
      if (!this.charts[id]) {
        this.charts[id] = new DashboardChart(id, this);
      }

      charts.push(this.charts[id]);
    });
    return charts;
  }

  _configureHost(hostBuilder) {
    return super._configureHost(hostBuilder).on('event', (event, payload, handlerIds) => {
      const chartId = payload.chartId;

      this.charts[chartId]._handleEvent(event, payload, handlerIds);
    });
  }

}

// Disabled temporarily to fix: https://github.com/mongodb-js/charts-embed-sdk/issues/14
// Until we come up with a better way to have strong typing for the Stitch client, while
// also not breaking normal TSC compiles of the SDK
// import type { StitchAppClient } from 'mongodb-stitch-browser-sdk';
const isJWTExpired = jwt => {
  try {
    const [header, payload, signature] = jwt.split('.');
    const {
      exp
    } = JSON.parse(atob(payload)); // Check the current time against the expiry (minus 5 minutes) in the token

    return Date.now() / 1000 >= exp - 300;
  } catch (e) {
    throw new Error('Failed to parse Realm token. Is the StitchClient configured correctly?');
  }
};
/**
 * A helper utility to support using [Realm Authentication](https://docs.mongodb.com/stitch/) with MongoDB Charts
 *
 * ```js
 * const client = Stitch.initializeDefaultAppClient('<your-client-app-id>');
 * client.auth.loginWithCredential(...)
 *
 * const sdk = new ChartsEmbedSDK({
 *   getUserToken: () => getRealmUserToken(client)
 * })
 * ```
 */


async function getRealmUserToken(stitchAppClient) {
  const client = stitchAppClient;

  if (!client.auth || !client.auth.authInfo) {
    throw new Error('Unfamiliar Stitch client version');
  }

  if (!client.auth.isLoggedIn) {
    throw new Error('Could not find a logged-in StitchUser. Is the StitchClient configured correctly?');
  }

  if (!client.auth.authInfo.accessToken) {
    throw new Error('Could not find a valid JWT. Is the StitchClient configured correctly?');
  }

  if (isJWTExpired(client.auth.authInfo.accessToken)) {
    // Attempt to refresh token using progression from public -> private apis
    if (client.auth.refreshCustomData) {
      await client.auth.refreshCustomData(); // supported from 4.8.0
    } else if (client.auth.refreshAccessToken) {
      await client.auth.refreshAccessToken(); // supported from 4.0.0
    } else {
      throw new Error('Could not refresh token. Unfamiliar Stitch client version');
    }
  }

  return client.auth.authInfo.accessToken;
}

function _defineProperty$4(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
/**
 * Creates an instance of the embedding SDK
 */

class EmbedSDK {
  /**
   * Accepts an optional {@link EmbedChartOptions} object to use as the
   * default options for any charts created using this SDK instance.
   *
   * ```js
   * const sdk = new EmbedSDK({
   *   baseUrl: "https://charts.mongodb.com",
   * })
   * ```
   */
  constructor(options) {
    _defineProperty$4(this, "defaultOptions", void 0);

    this.defaultOptions = options;
  }
  /**
   * Creates a new {@link Chart} instance that allows you to
   * interact with and embed charts into your application
   */


  createChart(options) {
    return new Chart({ ...this.defaultOptions,
      ...options
    });
  }
  /**
   * Creates a new {@link Dashboard} instance that allows you
   * to embed a dashboard into your application
   */


  createDashboard(options) {
    return new Dashboard({ ...this.defaultOptions,
      ...options
    });
  }

}

export default EmbedSDK;
export { getRealmUserToken };
