import debounce from 'debounce';
import Porthole from 'porthole-proxy';
import PropTypes from 'prop-types';
import { isEmpty, omit, path, prop } from 'ramda';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Placeholder } from 'semantic-ui-react';

import { withSite } from '../../addons/Site';
import DefaultItem from '../Item/Item';

/* Interface */
export const propTypes = {
  /** ID of the medium. */
  id: PropTypes.number.isRequired,

  /** Callback called when a SCORM event is received */
  onScormEvent: PropTypes.func,

  /** Content to display while awaiting data. */
  placeholder: PropTypes.element,

  /**
   * Loaded SCORM state
   * @private
   */
  scormState: PropTypes.object,

  /**
   * StoryShare `Site`.
   * @private
   */
  site: PropTypes.object.isRequired,
};

/* Defaults */
const defaultProps = {
  placeholder: <Placeholder><Placeholder.Image rectangular /></Placeholder>,
};

/**
 * SCORM Player
 *
 * This player uses a series of iFrames and postMessage techniques
 * to overcome cross-origin controls that affect CDN-hosted SCORM objects.
 * Communication between the frames is managed by the Porthole library.
 *
 * https://www.adlnet.gov/research/scorm/scorm-2004-4th-edition/
 *
 * The iFrames are arranged as follows:
 *
 * /----(top)---------------------------------------------------------------
 * |
 * | This component running on <app.example.com> receives cross-origin
 * | postMessages from the bridge frame which can be processed by this
 * | component or sent via callback to a parent component
 * |
 * | /----(bridge frame)----------------------------------------------------
 * | |
 * | | SCORM "bridge" loaded from <cdn.example.com> receives SCORM API
 * | | messages from the inner frame which are allowed via the same-origin
 * | | policy and converts these messages into cross-origin compliant
 * | | postMessages sent to its parent frame
 * | |
 * | | /----(container frame)-----------------------------------------------
 * | | |
 * | | | SCORM SCO container unpacked on <cdn.example.com> emits SCORM API
 * | | | messages to its parent frame
 * | | |
 *
 * Additional props will be passed through to the <iframe>
 * @returns {Object} component
 */
export class MediaScorm extends Component {
  state = { error: false };

  // Generate a random frame ID, required for the
  // Porthole iFrame to work correctly
  frameId = `scorm${Math.random().toString(36).substring(7)}`;

  events = {};

  handleError = () => this.setState({ error: true });

  componentDidMount() {
    const { id, site } = this.props;

    // Always attempt to read the SCORM state from the server
    site.req(`storyshare/v1/scorm/state/${id}`)
      .read();
  }

  /**
   * Runs when a SCORM event is emitted from the player
   */
  sendScormEvents = () => {
    const { id, site } = this.props;

    if (isEmpty(this.events)) {
      return;
    }

    site.req(`storyshare/v1/scorm/state/${id}`)
      .setParams(this.events)
      .update();

    this.events = {};
  };

  /**
   * Runs when the iFrame is loaded
   * @param {object} medium - The SCORM medium
   */
  handleLoad = (medium) => {
    const { id, onScormEvent, scormState } = this.props;

    const {
      proxy_url: proxyUrl,
      launch_url: launchUrl,
    } = medium.scorm;

    // Connect to the SCORM proxy
    const proxy = new Porthole.WindowProxy(proxyUrl, this.frameId);

    // Get ready to receive messages from the SCORM proxy
    proxy.addEventListener((event) => {
      const message = event.data.message;

      // Send events to any registered handler
      if (onScormEvent) {
        onScormEvent(event.data);
      }

      switch (event.data.type) {
        case 'SetValue':
          // SetValue can rapidly dispatch a sequence of events
          // from the artifact so they are debounced here
          this.events[message.key] = message.value;
          (debounce(this.sendScormEvents, 10))();
          break;

        // These are other events in the SCORM 2004 specification
        // that could be handled by this player
        case 'GetValue':
        case 'Initialize':
        case 'Terminate':
        case 'Commit':
        case 'GetLastError':
        case 'GetErrorString':
        case 'GetDiagnostic':
          break;
      }
    });

    // Boot the SCORM proxy by sending it the initial config
    proxy.post({
      type: 'config',
      message: {
        post_id: id,
        proxy_url: proxyUrl,
        scorm_content_url: launchUrl,
        values: scormState,
      },
    });
  };

  render() {
    const { id, placeholder, scormState } = this.props;

    // StoryShare sets `featured_media` to `0` if none has been selected
    if (id === 0) {
      return null;
    }

    const { error } = this.state;

    if (error) {
      return null;
    }

    return (
      <DefaultItem id={ id } typeSlug="media" placeholder={ placeholder }>
        { (medium) => {
          const proxyUrl = path(['scorm', 'proxy_url'], medium);

          if (!scormState || !proxyUrl) {
            return null;
          }

          return (
            <iframe
              id={ this.frameId }
              name={ this.frameId }
              frameBorder="0"
              allowFullScreen
              onError={ this.handleError }
              onLoad={ () => this.handleLoad(medium) }
              src={ proxyUrl }
              { ...omit([
                'dispatch',
                'id',
                'onScormEvent',
                'medium',
                'scormState',
                'site',
              ], this.props) }
            />
          );
        } }

      </DefaultItem>
    );
  }
}

MediaScorm.propTypes = propTypes;
MediaScorm.defaultProps = defaultProps;

export const mapStateToProps = (_, { id, site }) => ({
  scormState: prop(
    'body',
    site.getResponseByRoute(`storyshare/v1/scorm/state/${id}`)
  ),
});

export default withSite(connect(mapStateToProps)(MediaScorm));
