"use strict";

function _interopDefault(ex) {
  return ex && "object" == typeof ex && "default" in ex ? ex.default : ex;
}

Object.defineProperty(exports, "__esModule", {
  value: !0
});

var bson = require("bson"), chatty = require("@looker/chatty"), _isEqual = _interopDefault(require("lodash/isEqual")), _isEmpty = _interopDefault(require("lodash/isEmpty"));

function createElement(name, props = {}, children = []) {
  const element = document.createElement(name);
  for (const [name, value] of Object.entries(props)) "style" === name ? Object.assign(element.style, props.style) : element.setAttribute(name, value);
  for (const child of Array.isArray(children) ? children : [ children ]) element.append(child);
  return element;
}

let ThemeEnum, ScalingEnum;

!function(ThemeEnum) {
  ThemeEnum.DARK = "dark", ThemeEnum.LIGHT = "light";
}(ThemeEnum || (ThemeEnum = {})), function(ScalingEnum) {
  ScalingEnum.FIXED = "fixed", ScalingEnum.SCALE = "scale";
}(ScalingEnum || (ScalingEnum = {}));

const getSharedEmbedOptions = options => {
  const {background: background, baseUrl: baseUrl, autoRefresh: autoRefresh, maxDataAge: maxDataAge, width: width, height: height, theme: theme, showAttribution: showAttribution, getUserToken: getUserToken} = options;
  if ("string" != typeof baseUrl || 0 === baseUrl.length) throw new Error("Base URL must be a valid URL");
  if (void 0 !== background && "string" != typeof background) throw new Error("background must be a string if specified");
  if (void 0 !== autoRefresh && "boolean" != typeof autoRefresh) throw new Error("autoRefresh must be a boolean if specified");
  if (void 0 !== maxDataAge && "number" != typeof maxDataAge) throw new Error("maxDataAge must be a number if specified");
  if (void 0 !== width && ![ "number", "string" ].includes(typeof width)) throw new Error("Width must be a string or number if specified");
  if (void 0 !== height && ![ "number", "string" ].includes(typeof height)) throw new Error("Height must be a string or number if specified");
  if (void 0 !== theme && "string" != typeof theme) throw new Error("Theme must be a string if specified");
  if (void 0 !== showAttribution && "boolean" != typeof showAttribution) throw new Error("Attribution must be a boolean value if specified");
  if (void 0 !== getUserToken && "function" != typeof getUserToken) throw new Error("getUserToken must be a function");
  return {
    background: background,
    baseUrl: baseUrl,
    autoRefresh: autoRefresh,
    maxDataAge: maxDataAge,
    width: width,
    height: height,
    theme: theme,
    showAttribution: showAttribution,
    getUserToken: getUserToken
  };
}, getPathname = (url, pathname) => [ url.pathname, "/" === url.pathname.slice(-1) ? "" : "/", pathname ].join(""), getChartUrl = options => {
  try {
    const url = new URL(options.baseUrl);
    return url.pathname = getPathname(url, "embed/charts"), url.search = `id=${options.chartId}&sdk=2`, 
    !1 === options.autoRefresh ? url.search += "&autorefresh=false" : void 0 === options.autoRefresh && (url.search += options.refreshInterval ? `&autorefresh=${options.refreshInterval}` : ""), 
    void 0 !== options.maxDataAge && (url.search += `&maxDataAge=${options.maxDataAge}`), 
    options.filter && (url.search += `&filter=${encodeURIComponent(bson.EJSON.stringify(options.filter, {
      relaxed: !1
    }))}`), options.theme && (url.search += `&theme=${options.theme}`), !1 === options.showAttribution && (url.search += "&attribution=false"), 
    url.toString();
  } catch (e) {
    throw new Error("Base URL must be a valid URL");
  }
}, getDashboardUrl = options => {
  try {
    const url = new URL(options.baseUrl);
    return url.pathname = getPathname(url, "embed/dashboards"), url.search = `id=${options.dashboardId}&sdk=1`, 
    !1 === options.autoRefresh && (url.search += "&autoRefresh=false"), void 0 !== options.maxDataAge && (url.search += `&maxDataAge=${options.maxDataAge}`), 
    !0 === options.showTitleAndDesc && (url.search += "&showTitleAndDesc=true"), options.widthMode && (url.search += `&scalingWidth=${options.widthMode}`), 
    options.heightMode && (url.search += `&scalingHeight=${options.heightMode}`), options.theme && (url.search += `&theme=${options.theme}`), 
    options.chartsBackground && (url.search += `&chartsBackground=${options.chartsBackground}`), 
    !1 === options.showAttribution && (url.search += "&attribution=false"), url.toString();
  } catch (e) {
    throw new Error("Base URL must be a valid URL");
  }
}, parseCSSMeasurement = value => "string" == typeof value ? value : "number" == typeof value ? `${value}px` : null, getBackground = (background, theme, lightBackground, darkBackground) => "string" == typeof background && background.length > 0 ? background : "dark" === theme ? darkBackground : lightBackground;

function _defineProperty(obj, key, value) {
  return key in obj ? Object.defineProperty(obj, key, {
    value: value,
    enumerable: !0,
    configurable: !0,
    writable: !0
  }) : obj[key] = value, 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);
  }
  async render(container) {
    if (this.iframe) throw new Error(this.ERRORS.IFRAME);
    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%"
      }
    })), host = this._configureHost(chatty.Chatty.createHost(this.getEmbedUrl()).withSandboxAttribute("allow-scripts").withSandboxAttribute("allow-same-origin").withSandboxAttribute("allow-popups").withSandboxAttribute("allow-popups-to-escape-sandbox").appendTo(embedRoot)).build();
    for (host.iframe.setAttribute("aria-label", this.name), Object.assign(host.iframe.style, {
      position: "absolute",
      top: 0,
      left: 0,
      border: 0,
      width: "100%",
      height: "100%"
    }); container.firstChild; ) container.removeChild(container.firstChild);
    container.appendChild(embedRoot), this.connection = await host.connect(), this.iframe = host.iframe, 
    this._setBackground(this.options.background, this.options.theme), await this._retrieveAndSetToken(), 
    await this._send("ready");
  }
  async isAutoRefresh() {
    const [result] = await this._send("get", "autoRefresh");
    return "number" == typeof result || "boolean" == typeof result ? Boolean(result) : Promise.reject("unexpected response received from iframe");
  }
  async setAutoRefresh(value) {
    if ("boolean" != typeof value) return Promise.reject("autoRefresh property value should be a boolean");
    await this._send("set", "autoRefresh", value);
  }
  async getMaxDataAge() {
    const [result] = await this._send("get", "maxDataAge");
    return "number" == typeof result ? result : Promise.reject("unexpected response received from iframe");
  }
  async setMaxDataAge(value) {
    if ("number" != typeof value) return Promise.reject("maxDataAge property value should be a number");
    await this._send("set", "maxDataAge", value);
  }
  async setTheme(value) {
    if ("string" != typeof value) return Promise.reject("theme property value should be a string");
    const newTheme = Object.values(ThemeEnum).includes(value) ? value : ThemeEnum.LIGHT;
    await this._send("set", "theme", newTheme), this._setBackground(this.options.background, newTheme);
  }
  async getTheme() {
    const [result] = await this._send("get", "theme");
    return "string" == typeof result ? 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(eventName, ...payload) {
    return this.connection ? this.connection.sendAndReceive(eventName, ...payload) : Promise.reject(this.ERRORS.SEND);
  }
}

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

let eventHandlerIndex = Date.now();

function EventSource(Sender) {
  return class extends Sender {
    constructor(...args) {
      super(...args), _defineProperty$1(this, "_eventHandlers", {
        click: {}
      });
    }
    _handleEvent(event, payload, handlerIds) {
      const handlers = this._eventHandlers[event];
      for (const id of handlerIds) try {
        var _handlers$id;
        null === (_handlers$id = handlers[id]) || void 0 === _handlers$id || _handlers$id.handle(payload);
      } catch (error) {
        console.warn(`Error calling handler for event [${event}]: ${error}`);
      }
    }
    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: null == options ? void 0 : options.includes
        }
      };
      if (null !== (_h$options$includes = h.options.includes) && void 0 !== _h$options$includes && _h$options$includes.every(f => _isEmpty(f)) && console.warn("Empty includes filters out all events. Event handler will never be called. Is this intended?"), 
      !Object.keys(handlers).some(id => _isEqual(handlers[id], h))) {
        const handlerId = (++eventHandlerIndex).toString(36);
        return handlers[handlerId] = h, this._send("eventHandler", event, {
          handlerId: handlerId,
          options: h.options
        });
      }
      return Promise.resolve();
    }
    removeEventListener(event, eventHandler, options) {
      const handlers = this._eventHandlers[event];
      if (!handlers) throw new Error(`Not supported event: ${event}`);
      const h = {
        handle: eventHandler,
        options: {
          includes: null == options ? void 0 : options.includes
        }
      }, handlerId = Object.keys(handlers).find(id => _isEqual(handlers[id], h));
      return handlerId ? (delete handlers[handlerId], this._send("eventHandler", event, {
        handlerId: handlerId
      })) : Promise.resolve();
    }
  };
}

function Refreshable(Sender) {
  return class extends Sender {
    async refresh() {
      await this._send("refresh");
    }
  };
}

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

const getChartOptions = options => {
  if ("object" != typeof options || null === options) throw new Error("Options argument must be an object");
  const sharedEmbedOptions = getSharedEmbedOptions(options), {chartId: chartId, filter: filter, refreshInterval: refreshInterval} = options;
  if ("string" != typeof chartId || 0 === chartId.length) throw new Error("Chart ID must be specified");
  if (void 0 !== filter && (!filter || "object" != typeof filter)) throw new Error("Filter must be an object if specified");
  if (void 0 !== refreshInterval && "number" != typeof refreshInterval) throw new Error("refreshInterval interval must be a number if specified");
  return {
    ...sharedEmbedOptions,
    chartId: chartId,
    filter: filter,
    refreshInterval: refreshInterval
  };
};

class ChartEventSender extends BaseEmbedItem {
  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);
  }
}

class Chart extends(Refreshable(EventSource(ChartEventSender))){
  async getRefreshInterval() {
    const [result] = await this._send("get", "autorefresh");
    return console.warn("The 'getRefreshInterval' method is deprecated. Please use the 'autoRefresh' option with the 'maxDataAge' option to configure how often the chart refreshes."), 
    "number" == typeof result ? result : Promise.reject("unexpected response received from iframe");
  }
  async setRefreshInterval(value) {
    if ("number" != typeof value) 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);
  }
  async getFilter() {
    const [result] = await this._send("get", "filter");
    return "object" == typeof result && null !== result ? result : Promise.reject("unexpected response received from iframe");
  }
  async setFilter(value) {
    if ("object" != typeof value || null === value || Array.isArray(value)) return Promise.reject("filter property value should be an object");
    await this._send("set", "filter", bson.EJSON.stringify(value, {
      relaxed: !1
    }));
  }
  async getHighlight() {
    const [result] = await this._send("get", "highlight");
    return "object" == typeof result && null !== result ? result : Promise.reject("unexpected response received from iframe");
  }
  async setHighlight(value) {
    if ("object" != typeof value || null === value || Array.isArray(value)) return Promise.reject("highlight property value should be an object");
    await this._send("set", "highlight", bson.EJSON.stringify(value, {
      relaxed: !1
    }));
  }
  _configureHost(hostBuilder) {
    return super._configureHost(hostBuilder).on("event", this._handleEvent.bind(this));
  }
  async getData() {
    const [result] = await this._send("get", "data");
    return "object" == typeof result && null !== result ? result : Promise.reject("unexpected response received from iframe");
  }
}

class DashboardChartEventSender {
  constructor(chartId, dashboard) {
    this.chartId = chartId, this.dashboard = dashboard;
  }
  _send(msgName, ...payload) {
    return this.dashboard._send(msgName, ...payload, this.chartId);
  }
}

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

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

const getDashboardOptions = options => {
  if ("object" != typeof options || null === options) throw new Error("Options argument must be an object");
  const sharedEmbedOptions = getSharedEmbedOptions(options), {dashboardId: dashboardId, chartsBackground: chartsBackground, widthMode: widthMode, heightMode: heightMode, showTitleAndDesc: showTitleAndDesc} = options;
  if ("string" != typeof dashboardId || 0 === dashboardId.length) throw new Error("dashboardId must be specified");
  if (void 0 !== chartsBackground && "string" != typeof chartsBackground) throw new Error("chartsBackground must be a string if specified");
  if (void 0 !== widthMode && "string" != typeof widthMode) throw new Error("widthMode must be a string if specified");
  if (void 0 !== widthMode && widthMode !== ScalingEnum.FIXED && widthMode !== ScalingEnum.SCALE) throw new Error(`widthMode must be "${ScalingEnum.FIXED}" or "${ScalingEnum.SCALE}"`);
  if (void 0 !== heightMode && "string" != typeof heightMode) throw new Error("heightMode must be a string if specified");
  if (void 0 !== heightMode && heightMode !== ScalingEnum.FIXED && heightMode !== ScalingEnum.SCALE) throw new Error(`heightMode must be "${ScalingEnum.FIXED}" or "${ScalingEnum.SCALE}"`);
  if (void 0 !== showTitleAndDesc && "boolean" != typeof showTitleAndDesc) throw new Error("showTitleAndDesc must be a boolean value if specified");
  return {
    ...sharedEmbedOptions,
    dashboardId: dashboardId,
    chartsBackground: chartsBackground,
    widthMode: widthMode,
    heightMode: heightMode,
    showTitleAndDesc: showTitleAndDesc
  };
};

class DashboardEventSender extends BaseEmbedItem {
  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);
  }
}

class Dashboard extends(Refreshable(DashboardEventSender)){
  constructor(...args) {
    super(...args), _defineProperty$3(this, "charts", {});
  }
  async getChartsBackground() {
    const [result] = await this._send("get", "chartsBackground");
    return "string" == typeof result ? result : Promise.reject("unexpected response received from iframe");
  }
  async setChartsBackground(value) {
    if ("string" != typeof value) return Promise.reject("chartsBackground property value should be a string");
    await this._send("set", "chartsBackground", value);
  }
  async isShowAttribution() {
    const [result] = await this._send("get", "attribution");
    return "boolean" == typeof result ? Boolean(result) : Promise.reject("unexpected response received from iframe");
  }
  async setShowAttribution(value) {
    if ("boolean" != typeof value) return Promise.reject("showAttribution property value should be a boolean");
    await this._send("set", "attribution", value);
  }
  async getWidthMode() {
    const [result] = await this._send("get", "scalingWidth");
    return result === ScalingEnum.FIXED || result === ScalingEnum.SCALE ? result : Promise.reject("unexpected response received from iframe");
  }
  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);
  }
  async getHeightMode() {
    const [result] = await this._send("get", "scalingHeight");
    return "fixed" === result || "scale" === result ? result : Promise.reject("unexpected response received from iframe");
  }
  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);
  }
  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 (1 !== chartIds.length) return Promise.reject("Invalid chart id: " + id);
      this.charts[id] = new DashboardChart(id, this);
    }
    return this.charts[id];
  }
  async getAllCharts() {
    const [chartIds] = await this._send("get", "charts");
    if (!Array.isArray(chartIds)) return Promise.reject("unexpected response received from iframe");
    const charts = [];
    return chartIds.forEach(id => {
      this.charts[id] || (this.charts[id] = new DashboardChart(id, this)), charts.push(this.charts[id]);
    }), charts;
  }
  _configureHost(hostBuilder) {
    return super._configureHost(hostBuilder).on("event", (event, payload, handlerIds) => {
      const chartId = payload.chartId;
      this.charts[chartId]._handleEvent(event, payload, handlerIds);
    });
  }
}

const isJWTExpired = jwt => {
  try {
    const [header, payload, signature] = jwt.split("."), {exp: exp} = JSON.parse(atob(payload));
    return Date.now() / 1e3 >= exp - 300;
  } catch (e) {
    throw new Error("Failed to parse Realm token. Is the StitchClient configured correctly?");
  }
};

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)) if (client.auth.refreshCustomData) await client.auth.refreshCustomData(); else {
    if (!client.auth.refreshAccessToken) throw new Error("Could not refresh token. Unfamiliar Stitch client version");
    await client.auth.refreshAccessToken();
  }
  return client.auth.authInfo.accessToken;
}

function _defineProperty$4(obj, key, value) {
  return key in obj ? Object.defineProperty(obj, key, {
    value: value,
    enumerable: !0,
    configurable: !0,
    writable: !0
  }) : obj[key] = value, obj;
}

class EmbedSDK {
  constructor(options) {
    _defineProperty$4(this, "defaultOptions", void 0), this.defaultOptions = options;
  }
  createChart(options) {
    return new Chart({
      ...this.defaultOptions,
      ...options
    });
  }
  createDashboard(options) {
    return new Dashboard({
      ...this.defaultOptions,
      ...options
    });
  }
}

exports.default = EmbedSDK, exports.getRealmUserToken = getRealmUserToken;
