/*jshint multistr: true */
// TODO: Should we split up the generic parts needed by the editor(and others), and the parts needed to "run" H5Ps?

/** @namespace */
var H5P = window.H5P = window.H5P || {};

/**
 * Tells us if we're inside of an iframe.
 * @member {boolean}
 */
H5P.isFramed = (window.self !== window.parent);

/**
 * jQuery instance of current window.
 * @member {H5P.jQuery}
 */
H5P.$window = H5P.jQuery(window);

/**
 * List over H5P instances on the current page.
 * @member {Array}
 */
H5P.instances = [];

// Detect if we support fullscreen, and what prefix to use.
if (document.documentElement.requestFullscreen) {
  /**
   * Browser prefix to use when entering fullscreen mode.
   * undefined means no fullscreen support.
   * @member {string}
   */
  H5P.fullScreenBrowserPrefix = '';
}
else if (document.documentElement.webkitRequestFullScreen) {
  H5P.safariBrowser = navigator.userAgent.match(/version\/([.\d]+)/i);
  H5P.safariBrowser = (H5P.safariBrowser === null ? 0 : parseInt(H5P.safariBrowser[1]));

  // Do not allow fullscreen for safari < 7.
  if (H5P.safariBrowser === 0 || H5P.safariBrowser > 6) {
    H5P.fullScreenBrowserPrefix = 'webkit';
  }
}
else if (document.documentElement.mozRequestFullScreen) {
  H5P.fullScreenBrowserPrefix = 'moz';
}
else if (document.documentElement.msRequestFullscreen) {
  H5P.fullScreenBrowserPrefix = 'ms';
}

/**
 * Keep track of when the H5Ps where started.
 *
 * @type {Object[]}
 */
H5P.opened = {};

/**
 * Initialize H5P content.
 * Scans for ".h5p-content" in the document and initializes H5P instances where found.
 *
 * @param {Object} target DOM Element
 */
H5P.init = function (target) {
  // Useful jQuery object.
  if (H5P.$body === undefined) {
    H5P.$body = H5P.jQuery(document.body);
  }

  // Determine if we can use full screen
  if (H5P.fullscreenSupported === undefined) {
    /**
     * Use this variable to check if fullscreen is supported. Fullscreen can be
     * restricted when embedding since not all browsers support the native
     * fullscreen, and the semi-fullscreen solution doesn't work when embedded.
     * @type {boolean}
     */
    H5P.fullscreenSupported = !H5PIntegration.fullscreenDisabled && !H5P.fullscreenDisabled && (!(H5P.isFramed && H5P.externalEmbed !== false) || !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled));
    // -We should consider document.msFullscreenEnabled when they get their
    // -element sizing corrected. Ref. https://connect.microsoft.com/IE/feedback/details/838286/ie-11-incorrectly-reports-dom-element-sizes-in-fullscreen-mode-when-fullscreened-element-is-within-an-iframe
    // Update: Seems to be no need as they've moved on to Webkit
  }

  // Deprecated variable, kept to maintain backwards compatability
  if (H5P.canHasFullScreen === undefined) {
    /**
     * @deprecated since version 1.11
     * @type {boolean}
     */
    H5P.canHasFullScreen = H5P.fullscreenSupported;
  }

  // H5Ps added in normal DIV.
  H5P.jQuery('.h5p-content:not(.h5p-initialized)', target).each(function () {
    var $element = H5P.jQuery(this).addClass('h5p-initialized');
    var $container = H5P.jQuery('<div class="h5p-container"></div>').appendTo($element);
    var contentId = $element.data('content-id');
    var contentData = H5PIntegration.contents['cid-' + contentId];
    if (contentData === undefined) {
      return H5P.error('No data for content id ' + contentId + '. Perhaps the library is gone?');
    }
    var library = {
      library: contentData.library,
      params: JSON.parse(contentData.jsonContent),
      metadata: contentData.metadata
    };

    // Apply theme density
    let density = H5PIntegration.theme?.density ?? 'large';
    $element.addClass('h5p-' + density);

    H5P.getUserData(contentId, 'state', function (err, previousState) {
      if (previousState) {
        library.userDatas = {
          state: previousState
        };
      }
      else if (previousState === null && H5PIntegration.saveFreq) {
        // Content has been reset. Display dialog.
        delete contentData.contentUserData;
        var dialog = new H5P.Dialog('content-user-data-reset', 'Data Reset', '<p>' + H5P.t('contentChanged') + '</p><p>' + H5P.t('startingOver') + '</p><div class="h5p-dialog-ok-button" tabIndex="0" role="button">OK</div>', $container);
        H5P.jQuery(dialog).on('dialog-opened', function (event, $dialog) {

          var closeDialog = function (event) {
            if (event.type === 'click' || event.which === 32) {
              dialog.close();
              H5P.deleteUserData(contentId, 'state', 0);
            }
          };

          $dialog.find('.h5p-dialog-ok-button').click(closeDialog).keypress(closeDialog);
          H5P.trigger(instance, 'resize');
        }).on('dialog-closed', function () {
          H5P.trigger(instance, 'resize');
        });
        dialog.open();
      }
      // If previousState is false we don't have a previous state
    });

    // Create new instance.
    var instance = H5P.newRunnable(library, contentId, $container, true, {standalone: true});

    H5P.offlineRequestQueue = new H5P.OfflineRequestQueue({instance: instance});

    // Check if we should add and display a fullscreen button for this H5P.
    if (contentData.fullScreen == 1 && H5P.fullscreenSupported) {
      H5P.jQuery(
        '<div class="h5p-content-controls">' +
          '<div role="button" ' +
                'tabindex="0" ' +
                'class="h5p-enable-fullscreen" ' +
                'aria-label="' + H5P.t('fullscreen') + '" ' +
                'title="' + H5P.t('fullscreen') + '">' +
          '</div>' +
        '</div>')
        .prependTo($container)
          .children()
          .click(function () {
            H5P.fullScreen($container, instance);
          })
        .keydown(function (e) {
          if (e.which === 32 || e.which === 13) {
            H5P.fullScreen($container, instance);
            return false;
          }
        })
      ;
    }

    /**
     * Create action bar
     */
    var displayOptions = contentData.displayOptions;
    var displayFrame = false;
    if (displayOptions.frame) {
      // Special handling of copyrights
      if (displayOptions.copyright) {
        var copyrights = H5P.getCopyrights(instance, library.params, contentId, library.metadata);
        if (!copyrights) {
          displayOptions.copyright = false;
        }
      }

      // Create action bar
      var actionBar = new H5P.ActionBar(displayOptions);
      var $actions = actionBar.getDOMElement();

      actionBar.on('reuse', function () {
        H5P.openReuseDialog($actions, contentData, {
          library: contentData.library,
          params: JSON.parse(contentData.jsonContent),
          metadata: contentData.metadata
        }, instance, contentId);
        instance.triggerXAPI('accessed-reuse');
      });
      actionBar.on('copyrights', function () {
        var dialog = new H5P.Dialog('copyrights', H5P.t('copyrightInformation'), copyrights, $container, $actions.find('.h5p-copyrights')[0]);
        dialog.open(true);
        instance.triggerXAPI('accessed-copyright');
      });
      actionBar.on('embed', function () {
        H5P.openEmbedDialog($actions, contentData.embedCode, contentData.resizeCode, {
          width: $element.width(),
          height: $element.height()
        }, instance);
        instance.triggerXAPI('accessed-embed');
      });

      if (actionBar.hasActions()) {
        displayFrame = true;
        $actions.insertAfter($container);
      }
    }

    $element.addClass(displayFrame ? 'h5p-frame' : 'h5p-no-frame');

    // Keep track of when we started
    H5P.opened[contentId] = new Date();

    // Handle events when the user finishes the content. Useful for logging exercise results.
    H5P.on(instance, 'finish', function (event) {
      if (event.data !== undefined) {
        H5P.setFinished(contentId, event.data.score, event.data.maxScore, event.data.time);
      }
    });

    // Listen for xAPI events.
    H5P.on(instance, 'xAPI', H5P.xAPICompletedListener);

    // Auto save current state if supported
    if (H5PIntegration.saveFreq !== false && (
        instance.getCurrentState instanceof Function ||
        typeof instance.getCurrentState === 'function')) {

      var saveTimer, save = function () {
        var state = instance.getCurrentState();
        if (state !== undefined) {
          H5P.setUserData(contentId, 'state', state, {deleteOnChange: true});
        }
        if (H5PIntegration.saveFreq) {
          // Continue autosave
          saveTimer = setTimeout(save, H5PIntegration.saveFreq * 1000);
        }
      };

      if (H5PIntegration.saveFreq) {
        // Start autosave
        saveTimer = setTimeout(save, H5PIntegration.saveFreq * 1000);
      }

      // xAPI events will schedule a save in three seconds.
      H5P.on(instance, 'xAPI', function (event) {
        var verb = event.getVerb();
        if (verb === 'completed' || verb === 'progressed') {
          clearTimeout(saveTimer);
          saveTimer = setTimeout(save, 3000);
        }
      });
    }

    if (H5P.isFramed) {
      var resizeDelay;
      if (H5P.externalEmbed === false) {
        // Internal embed
        // Make it possible to resize the iframe when the content changes size. This way we get no scrollbars.
        var iframe = window.frameElement;
        var resizeIframe = function () {
          if (window.parent.H5P.isFullscreen) {
            return; // Skip if full screen.
          }

          // Retain parent size to avoid jumping/scrolling
          var parentHeight = iframe.parentElement.style.height;
          iframe.parentElement.style.height = iframe.parentElement.clientHeight + 'px';

          // Note:  Force layout reflow
          //        This fixes a flickering bug for embedded content on iPads
          //        @see https://github.com/h5p/h5p-moodle-plugin/issues/237
          iframe.getBoundingClientRect();

          // Reset iframe height, in case content has shrinked.
          iframe.style.height = '1px';

          // Resize iframe so all content is visible.
          iframe.style.height = (iframe.contentDocument.body.scrollHeight) + 'px';

          // Free parent
          iframe.parentElement.style.height = parentHeight;
        };

        H5P.on(instance, 'resize', function () {
          // Use a delay to make sure iframe is resized to the correct size.
          clearTimeout(resizeDelay);
          resizeDelay = setTimeout(function () {
            resizeIframe();
          }, 1);
        });
      }
      else if (H5P.communicator) {
        // External embed
        var parentIsFriendly = false;

        // Handle that the resizer is loaded after the iframe
        H5P.communicator.on('ready', function () {
          H5P.communicator.send('hello');
        });

        // Handle hello message from our parent window
        H5P.communicator.on('hello', function () {
          // Initial setup/handshake is done
          parentIsFriendly = true;

          // Make iframe responsive
          document.body.style.height = 'auto';

          // Hide scrollbars for correct size
          document.body.style.overflow = 'hidden';

          // Content need to be resized to fit the new iframe size
          H5P.trigger(instance, 'resize');
        });

        // When resize has been prepared tell parent window to resize
        H5P.communicator.on('resizePrepared', function () {
          H5P.communicator.send('resize', {
            scrollHeight: document.body.scrollHeight
          });
        });

        H5P.communicator.on('resize', function () {
          H5P.trigger(instance, 'resize');
        });

        H5P.on(instance, 'resize', function () {
          if (H5P.isFullscreen) {
            return; // Skip iframe resize
          }

          // Use a delay to make sure iframe is resized to the correct size.
          clearTimeout(resizeDelay);
          resizeDelay = setTimeout(function () {
            // Only resize if the iframe can be resized
            if (parentIsFriendly) {
              H5P.communicator.send('prepareResize', {
                scrollHeight: document.body.scrollHeight,
                clientHeight: document.body.clientHeight
              });
            }
            else {
              H5P.communicator.send('hello');
            }
          }, 0);
        });
      }
    }

    if (!H5P.isFramed || H5P.externalEmbed === false) {
      // Resize everything when window is resized.
      H5P.jQuery(window.parent).resize(function () {
        H5P.trigger(instance, 'resize');
      });
    }

    H5P.instances.push(instance);

    // Resize content.
    H5P.trigger(instance, 'resize');

    // Logic for hiding focus effects when using mouse
    $element.addClass('using-mouse');
    $element.on('mousedown keydown keyup', function (event) {
      $element.toggleClass('using-mouse', event.type === 'mousedown');
    });

    if (H5P.externalDispatcher) {
      H5P.externalDispatcher.trigger('initialized');
    }
  });

  // Insert H5Ps that should be in iframes.
  H5P.jQuery('iframe.h5p-iframe:not(.h5p-initialized)', target).each(function () {
    const iframe = this;
    const $iframe = H5P.jQuery(iframe);

    const contentId = $iframe.data('content-id');
    const contentData = H5PIntegration.contents['cid-' + contentId];
    const contentLanguage = contentData && contentData.metadata && contentData.metadata.defaultLanguage
      ? contentData.metadata.defaultLanguage : 'en';

    const writeDocument = function () {
      iframe.contentDocument.open();
      iframe.contentDocument.write('<!doctype html><html class="h5p-iframe" lang="' + contentLanguage + '"><head>' + H5P.getHeadTags(contentId) + '</head><body><div class="h5p-content" data-content-id="' + contentId + '"/></body></html>');
      iframe.contentDocument.close();
    };

    $iframe.addClass('h5p-initialized')
    if (iframe.contentDocument === null) {
      // In some Edge cases the iframe isn't always loaded when the page is ready.
      $iframe.on('load', writeDocument);
      $iframe.attr('src', 'about:blank');
    }
    else {
      writeDocument();
    }
  });
};

/**
 * Loop through assets for iframe content and create a set of tags for head.
 *
 * @private
 * @param {number} contentId
 * @returns {string} HTML
 */
H5P.getHeadTags = function (contentId) {
  var createStyleTags = function (styles) {
    var tags = '';
    for (var i = 0; i < styles.length; i++) {
      tags += '<link rel="stylesheet" href="' + styles[i] + '">';
    }
    return tags;
  };

  var createScriptTags = function (scripts) {
    var tags = '';
    for (var i = 0; i < scripts.length; i++) {
      tags += '<script src="' + scripts[i] + '"></script>';
    }
    return tags;
  };

  return '<base target="_parent">' +
         createStyleTags(H5PIntegration.core.styles) +
         createStyleTags(H5PIntegration.contents['cid-' + contentId].styles) +
         createScriptTags(H5PIntegration.core.scripts) +
         createScriptTags(H5PIntegration.contents['cid-' + contentId].scripts) +
         '<script>H5PIntegration = window.parent.H5PIntegration; var H5P = H5P || {}; H5P.externalEmbed = false;</script>';
};

/**
 * When embedded the communicator helps talk to the parent page.
 *
 * @type {Communicator}
 */
H5P.communicator = (function () {
  /**
   * @class
   * @private
   */
  function Communicator() {
    var self = this;

    // Maps actions to functions
    var actionHandlers = {};

    // Register message listener
    window.addEventListener('message', function receiveMessage(event) {
      if (window.parent !== event.source || event.data.context !== 'h5p') {
        return; // Only handle messages from parent and in the correct context
      }

      if (actionHandlers[event.data.action] !== undefined) {
        actionHandlers[event.data.action](event.data);
      }
    } , false);


    /**
     * Register action listener.
     *
     * @param {string} action What you are waiting for
     * @param {function} handler What you want done
     */
    self.on = function (action, handler) {
      actionHandlers[action] = handler;
    };

    /**
     * Send a message to the all mighty father.
     *
     * @param {string} action
     * @param {Object} [data] payload
     */
    self.send = function (action, data) {
      if (data === undefined) {
        data = {};
      }
      data.context = 'h5p';
      data.action = action;

      // Parent origin can be anything
      window.parent.postMessage(data, '*');
    };
  }

  return (window.postMessage && window.addEventListener ? new Communicator() : undefined);
})();

/**
 * Enter semi fullscreen for the given H5P instance
 *
 * @param {H5P.jQuery} $element Content container.
 * @param {Object} instance
 * @param {function} exitCallback Callback function called when user exits fullscreen.
 * @param {H5P.jQuery} $body For internal use. Gives the body of the iframe.
 */
H5P.semiFullScreen = function ($element, instance, exitCallback, body) {
  H5P.fullScreen($element, instance, exitCallback, body, true);
};

/**
 * Enter fullscreen for the given H5P instance.
 *
 * @param {H5P.jQuery} $element Content container.
 * @param {Object} instance
 * @param {function} exitCallback Callback function called when user exits fullscreen.
 * @param {H5P.jQuery} $body For internal use. Gives the body of the iframe.
 * @param {Boolean} forceSemiFullScreen
 */
H5P.fullScreen = function ($element, instance, exitCallback, body, forceSemiFullScreen) {
  if (H5P.exitFullScreen !== undefined) {
    return; // Cannot enter new fullscreen until previous is over
  }

  if (H5P.isFramed && H5P.externalEmbed === false) {
    // Trigger resize on wrapper in parent window.
    window.parent.H5P.fullScreen($element, instance, exitCallback, H5P.$body.get(), forceSemiFullScreen);
    H5P.isFullscreen = true;
    H5P.exitFullScreen = function () {
      window.parent.H5P.exitFullScreen();
    };
    H5P.on(instance, 'exitFullScreen', function () {
      H5P.isFullscreen = false;
      H5P.exitFullScreen = undefined;
    });
    return;
  }

  var $container = $element;
  var $classes, $iframe, $body;
  if (body === undefined)  {
    $body = H5P.$body;
  }
  else {
    // We're called from an iframe.
    $body = H5P.jQuery(body);
    $classes = $body.add($element.get());
    var iframeSelector = '#h5p-iframe-' + $element.parent().data('content-id');
    $iframe = H5P.jQuery(iframeSelector);
    $element = $iframe.parent(); // Put iframe wrapper in fullscreen, not container.
  }

  $classes = $element.add(H5P.$body).add($classes);

  /**
   * Prepare for resize by setting the correct styles.
   *
   * @private
   * @param {string} classes CSS
   */
  var before = function (classes) {
    $classes.addClass(classes);

    if ($iframe !== undefined) {
      // Set iframe to its default size(100%).
      $iframe.css('height', '');
    }
  };

  /**
   * Gets called when fullscreen mode has been entered.
   * Resizes and sets focus on content.
   *
   * @private
   */
  var entered = function () {
    // Do not rely on window resize events.
    H5P.trigger(instance, 'resize');
    H5P.trigger(instance, 'focus');
    H5P.trigger(instance, 'enterFullScreen');
  };

  /**
   * Gets called when fullscreen mode has been exited.
   * Resizes and sets focus on content.
   *
   * @private
   * @param {string} classes CSS
   */
  var done = function (classes) {
    H5P.isFullscreen = false;
    $classes.removeClass(classes);

    // Do not rely on window resize events.
    H5P.trigger(instance, 'resize');
    H5P.trigger(instance, 'focus');

    H5P.exitFullScreen = undefined;
    if (exitCallback !== undefined) {
      exitCallback();
    }

    H5P.trigger(instance, 'exitFullScreen');
  };

  H5P.isFullscreen = true;
  if (H5P.fullScreenBrowserPrefix === undefined || forceSemiFullScreen === true) {
    // Create semi fullscreen.

    if (H5P.isFramed) {
      return; // TODO: Should we support semi-fullscreen for IE9 & 10 ?
    }

    before('h5p-semi-fullscreen');
    var $disable = H5P.jQuery('<div role="button" tabindex="0" class="h5p-disable-fullscreen" title="' + H5P.t('disableFullscreen') + '" aria-label="' + H5P.t('disableFullscreen') + '"></div>').appendTo($container.find('.h5p-content-controls'));
    var keyup, disableSemiFullscreen = H5P.exitFullScreen = function () {
      if (prevViewportContent) {
        // Use content from the previous viewport tag
        h5pViewport.content = prevViewportContent;
      }
      else {
        // Remove viewport tag
        head.removeChild(h5pViewport);
      }
      $disable.remove();
      $body.unbind('keyup', keyup);
      done('h5p-semi-fullscreen');
    };
    keyup = function (event) {
      if (event.keyCode === 27) {
        disableSemiFullscreen();
      }
    };
    $disable.click(disableSemiFullscreen);
    $body.keyup(keyup);

    // Disable zoom
    var prevViewportContent, h5pViewport;
    var metaTags = document.getElementsByTagName('meta');
    for (var i = 0; i < metaTags.length; i++) {
      if (metaTags[i].name === 'viewport') {
        // Use the existing viewport tag
        h5pViewport = metaTags[i];
        prevViewportContent = h5pViewport.content;
        break;
      }
    }
    if (!prevViewportContent) {
      // Create a new viewport tag
      h5pViewport = document.createElement('meta');
      h5pViewport.name = 'viewport';
    }
    h5pViewport.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0';
    if (!prevViewportContent) {
      // Insert the new viewport tag
      var head = document.getElementsByTagName('head')[0];
      head.appendChild(h5pViewport);
    }

    entered();
  }
  else {
    // Create real fullscreen.

    before('h5p-fullscreen');
    var first, eventName = (H5P.fullScreenBrowserPrefix === 'ms' ? 'MSFullscreenChange' : H5P.fullScreenBrowserPrefix + 'fullscreenchange');
    document.addEventListener(eventName, function fullscreenCallback() {
      if (first === undefined) {
        // We are entering fullscreen mode
        first = false;
        entered();
        return;
      }

      // We are exiting fullscreen
      done('h5p-fullscreen');
      document.removeEventListener(eventName, fullscreenCallback, false);
    });

    if (H5P.fullScreenBrowserPrefix === '') {
      $element[0].requestFullscreen();
    }
    else {
      var method = (H5P.fullScreenBrowserPrefix === 'ms' ? 'msRequestFullscreen' : H5P.fullScreenBrowserPrefix + 'RequestFullScreen');
      var params = (H5P.fullScreenBrowserPrefix === 'webkit' && H5P.safariBrowser === 0 ? Element.ALLOW_KEYBOARD_INPUT : undefined);
      $element[0][method](params);
    }

    // Allows everone to exit
    H5P.exitFullScreen = function () {
      if (H5P.fullScreenBrowserPrefix === '') {
        document.exitFullscreen();
      }
      else if (H5P.fullScreenBrowserPrefix === 'moz') {
        document.mozCancelFullScreen();
      }
      else {
        document[H5P.fullScreenBrowserPrefix + 'ExitFullscreen']();
      }
    };
  }
};

(function () {
  /**
   * Helper for adding a query parameter to an existing path that may already
   * contain one or a hash.
   *
   * @param {string} path
   * @param {string} parameter
   * @return {string}
   */
  H5P.addQueryParameter = function (path, parameter) {
    let newPath, secondSplit;
    const firstSplit = path.split('?');
    if (firstSplit[1]) {
      // There is already an existing query
      secondSplit = firstSplit[1].split('#');
      newPath = firstSplit[0] + '?' + secondSplit[0] + '&';
    }
    else {
      // No existing query, just need to take care of the hash
      secondSplit = firstSplit[0].split('#');
      newPath = secondSplit[0] + '?';
    }
    newPath += parameter;
    if (secondSplit[1]) {
      // Add back the hash
      newPath += '#' + secondSplit[1];
    }
    return newPath;
  };

  /**
   * Helper for setting the crossOrigin attribute + the complete correct source.
   * Note: This will start loading the resource.
   *
   * @param {Element} element DOM element, typically img, video or audio
   * @param {Object} source File object from parameters/json_content (created by H5PEditor)
   * @param {number} contentId Needed to determine the complete correct file path
   */
  H5P.setSource = function (element, source, contentId) {
    let path = source.path;

    const crossOrigin = H5P.getCrossOrigin(source);
    if (crossOrigin) {
      element.crossOrigin = crossOrigin;

      if (H5PIntegration.crossoriginCacheBuster) {
        // Some sites may want to add a cache buster in case the same resource
        // is used elsewhere without the crossOrigin attribute
        path = H5P.addQueryParameter(path, H5PIntegration.crossoriginCacheBuster);
      }
    }
    else {
      // In case this element has been used before.
      element.removeAttribute('crossorigin');
    }

    element.src = H5P.getPath(path, contentId);
  };

  /**
   * Check if the given path has a protocol.
   *
   * @private
   * @param {string} path
   * @return {string}
   */
  var hasProtocol = function (path) {
    return path.match(/^[a-z0-9]+:\/\//i);
  };

  /**
   * Get the crossOrigin policy to use for img, video and audio tags on the current site.
   *
   * @param {Object|string} source File object from parameters/json_content - Can also be URL(deprecated usage)
   * @returns {string|null} crossOrigin attribute value required by the source
   */
  H5P.getCrossOrigin = function (source) {
    if (typeof source !== 'object') {
      // Deprecated usage.
      return H5PIntegration.crossorigin && H5PIntegration.crossoriginRegex && source.match(H5PIntegration.crossoriginRegex) ? H5PIntegration.crossorigin : null;
    }

    if (H5PIntegration.crossorigin && !hasProtocol(source.path)) {
      // This is a local file, use the local crossOrigin policy.
      return H5PIntegration.crossorigin;
      // Note: We cannot use this for all external sources since we do not know
      // each server's individual policy. We could add support for a list of
      // external sources and their policy later on.
    }
  };

  /**
   * Find the path to the content files based on the id of the content.
   * Also identifies and returns absolute paths.
   *
   * @param {string} path
   *   Relative to content folder or absolute.
   * @param {number} contentId
   *   ID of the content requesting the path.
   * @returns {string}
   *   Complete URL to path.
   */
  H5P.getPath = function (path, contentId) {
    if (hasProtocol(path)) {
      return path;
    }

    var prefix;
    var isTmpFile = (path.substr(-4,4) === '#tmp');
    if (contentId !== undefined && !isTmpFile) {
      // Check for custom override URL
      if (H5PIntegration.contents !== undefined &&
          H5PIntegration.contents['cid-' + contentId]) {
        prefix = H5PIntegration.contents['cid-' + contentId].contentUrl;
      }
      if (!prefix) {
        prefix = H5PIntegration.url + '/content/' + contentId;
      }
    }
    else if (window.H5PEditor !== undefined) {
      prefix = H5PEditor.filesPath;
    }
    else {
      return;
    }

    if (!hasProtocol(prefix)) {
      // Use absolute urls
      prefix = window.location.protocol + "//" + window.location.host + prefix;
    }

    return prefix + '/' + path;
  };
})();

/**
 * THIS FUNCTION IS DEPRECATED, USE getPath INSTEAD
 * Will be remove march 2016.
 *
 * Find the path to the content files folder based on the id of the content
 *
 * @deprecated
 *   Will be removed march 2016.
 * @param contentId
 *   Id of the content requesting the path
 * @returns {string}
 *   URL
 */
H5P.getContentPath = function (contentId) {
  return H5PIntegration.url + '/content/' + contentId;
};

/**
 * Get library class constructor from H5P by classname.
 * Note that this class will only work for resolve "H5P.NameWithoutDot".
 * Also check out {@link H5P.newRunnable}
 *
 * Used from libraries to construct instances of other libraries' objects by name.
 *
 * @param {string} name Name of library
 * @returns {Object} Class constructor
 */
H5P.classFromName = function (name) {
  var arr = name.split(".");
  return this[arr[arr.length - 1]];
};

/**
 * A safe way of creating a new instance of a runnable H5P.
 *
 * @param {Object} library
 *   Library/action object form params.
 * @param {number} contentId
 *   Identifies the content.
 * @param {H5P.jQuery} [$attachTo]
 *   Element to attach the instance to.
 * @param {boolean} [skipResize]
 *   Skip triggering of the resize event after attaching.
 * @param {Object} [extras]
 *   Extra parameters for the H5P content constructor
 * @returns {Object}
 *   Instance.
 */
H5P.newRunnable = function (library, contentId, $attachTo, skipResize, extras) {
  var nameSplit, versionSplit, machineName;
  try {
    nameSplit = library.library.split(' ', 2);
    machineName = nameSplit[0];
    versionSplit = nameSplit[1].split('.', 2);
  }
  catch (err) {
    return H5P.error('Invalid library string: ' + library.library);
  }

  if ((library.params instanceof Object) !== true || (library.params instanceof Array) === true) {
    H5P.error('Invalid library params for: ' + library.library);
    return H5P.error(library.params);
  }

  // Find constructor function
  var constructor;
  try {
    nameSplit = nameSplit[0].split('.');
    constructor = window;
    for (var i = 0; i < nameSplit.length; i++) {
      constructor = constructor[nameSplit[i]];
    }
    if (typeof constructor !== 'function') {
      throw null;
    }
  }
  catch (err) {
    return H5P.error('Unable to find constructor for: ' + library.library);
  }

  if (extras === undefined) {
    extras = {};
  }
  if (library.subContentId) {
    extras.subContentId = library.subContentId;
  }

  if (library.userDatas && library.userDatas.state && H5PIntegration.saveFreq) {
    extras.previousState = library.userDatas.state;
  }

  if (library.metadata) {
    extras.metadata = library.metadata;
  }

  // Makes all H5P libraries extend H5P.ContentType:
  var standalone = extras.standalone || false;
  // This order makes it possible for an H5P library to override H5P.ContentType functions!
  constructor.prototype = H5P.jQuery.extend({}, H5P.ContentType(standalone).prototype, constructor.prototype);

  var instance;
  // Some old library versions have their own custom third parameter.
  // Make sure we don't send them the extras.
  // (they will interpret it as something else)
  if (H5P.jQuery.inArray(library.library, ['H5P.CoursePresentation 1.0', 'H5P.CoursePresentation 1.1', 'H5P.CoursePresentation 1.2', 'H5P.CoursePresentation 1.3']) > -1) {
    instance = new constructor(library.params, contentId);
  }
  else {
    instance = new constructor(library.params, contentId, extras);
  }

  if (instance.$ === undefined) {
    instance.$ = H5P.jQuery(instance);
  }

  if (instance.contentId === undefined) {
    instance.contentId = contentId;
  }
  if (instance.subContentId === undefined && library.subContentId) {
    instance.subContentId = library.subContentId;
  }
  if (instance.parent === undefined && extras && extras.parent) {
    instance.parent = extras.parent;
  }
  if (instance.libraryInfo === undefined) {
    instance.libraryInfo = {
      versionedName: library.library,
      versionedNameNoSpaces: machineName + '-' + versionSplit[0] + '.' + versionSplit[1],
      machineName: machineName,
      majorVersion: versionSplit[0],
      minorVersion: versionSplit[1]
    };
  }

  if ($attachTo !== undefined) {
    $attachTo.toggleClass('h5p-standalone', standalone);
    instance.attach($attachTo);
    H5P.trigger(instance, 'domChanged', {
      '$target': $attachTo,
      'library': machineName,
      'key': 'newLibrary'
    }, {'bubbles': true, 'external': true});

    if (skipResize === undefined || !skipResize) {
      // Resize content.
      H5P.trigger(instance, 'resize');
    }
  }
  return instance;
};

/**
 * Used to print useful error messages. (to JavaScript error console)
 *
 * @param {*} err Error to print.
 */
H5P.error = function (err) {
  if (window.console !== undefined && console.error !== undefined) {
    console.error(err.stack ? err.stack : err);
  }
};

/**
 * Translate text strings.
 *
 * @param {string} key
 *   Translation identifier, may only contain a-zA-Z0-9. No spaces or special chars.
 * @param {Object} [vars]
 *   Data for placeholders.
 * @param {string} [ns]
 *   Translation namespace. Defaults to H5P.
 * @returns {string}
 *   Translated text
 */
H5P.t = function (key, vars, ns) {
  if (ns === undefined) {
    ns = 'H5P';
  }

  if (H5PIntegration.l10n[ns] === undefined) {
    return '[Missing translation namespace "' + ns + '"]';
  }

  if (H5PIntegration.l10n[ns][key] === undefined) {
    return '[Missing translation "' + key + '" in "' + ns + '"]';
  }

  var translation = H5PIntegration.l10n[ns][key];

  if (vars !== undefined) {
    // Replace placeholder with variables.
    for (var placeholder in vars) {
      translation = translation.replace(placeholder, vars[placeholder]);
    }
  }

  return translation;
};

/**
 * Creates a new popup dialog over the H5P content.
 *
 * @class
 * @param {string} name
 *   Used for html class.
 * @param {string} title
 *   Used for header.
 * @param {string} content
 *   Displayed inside the dialog.
 * @param {H5P.jQuery} $element
 *   Which DOM element the dialog should be inserted after.
 * @param {H5P.jQuery} $returnElement
 *   Which DOM element the focus should be moved to on close
 */
H5P.Dialog = function (name, title, content, $element, $returnElement) {
  /** @alias H5P.Dialog# */
  var self = this;
  this.activeElement = document.activeElement;
  var $dialog = H5P.jQuery('<div class="h5p-popup-dialog h5p-' + name + '-dialog" aria-labelledby="' + name + '-dialog-header" aria-modal="true" role="dialog" tabindex="-1">\
                              <div class="h5p-inner">\
                                <h2 id="' + name + '-dialog-header">' + title + '</h2>\
                                <div class="h5p-scroll-content">' + content + '</div>\
                                <div class="h5p-close" role="button" tabindex="0" aria-label="' + H5P.t('close') + '" title="' + H5P.t('close') + '"></div>\
                              </div>\
                            </div>')
    .insertAfter($element)
    .click(function (e) {
      if (e && e.originalEvent && e.originalEvent.preventClosing) {
        return;
      }

      self.close();
    })
    .children('.h5p-inner')
      .click(function (e) {
        e.originalEvent.preventClosing = true;
      })
      .find('.h5p-close')
        .click(function () {
          self.close();
        })
        .keypress(function (e) {
          if (e.which === 13 || e.which === 32) {
            self.close();
            return false;
          }
        })
        .end()
      .find('a')
        .click(function (e) {
          e.stopPropagation();
        })
      .end()
    .end();

  /**
   * Opens the dialog.
   */
  self.open = function (scrollbar) {
    if (scrollbar) {
      $dialog.css('height', '100%');
    }
    setTimeout(function () {
      $dialog.addClass('h5p-open'); // Fade in
      // Triggering an event, in case something has to be done after dialog has been opened.
      H5P.jQuery(self).trigger('dialog-opened', [$dialog]);
      $dialog.focus();
    }, 1);
  };

  /**
   * Closes the dialog.
   */
  self.close = function () {
    $dialog.removeClass('h5p-open'); // Fade out
    setTimeout(function () {
      $dialog.remove();
      H5P.jQuery(self).trigger('dialog-closed', [$dialog]);
      $element.attr('tabindex', '-1');
      if ($returnElement) {
        $returnElement.focus();
      }
      else if(self.activeElement) {
        self.activeElement.focus();
      }
      else {
        $element.focus();
      }
    }, 200);
  };
};

/**
 * Gather copyright information for the given content.
 *
 * @param {Object} instance
 *   H5P instance to get copyright information for.
 * @param {Object} parameters
 *   Parameters of the content instance.
 * @param {number} contentId
 *   Identifies the H5P content
 * @param {Object} metadata
 *   Metadata of the content instance.
 * @returns {string} Copyright information.
 */
H5P.getCopyrights = function (instance, parameters, contentId, metadata) {
  var copyrights;

  if (instance.getCopyrights !== undefined) {
    try {
      // Use the instance's own copyright generator
      copyrights = instance.getCopyrights();
    }
    catch (err) {
      // Failed, prevent crashing page.
    }
  }

  if (copyrights === undefined) {
    // Create a generic flat copyright list
    copyrights = new H5P.ContentCopyrights();
    H5P.findCopyrights(copyrights, parameters, contentId);
  }

  var metadataCopyrights = H5P.buildMetadataCopyrights(metadata, instance.libraryInfo.machineName);
  if (metadataCopyrights !== undefined) {
    copyrights.addMediaInFront(metadataCopyrights);
  }

  if (copyrights !== undefined) {
    // Convert to string
    copyrights = copyrights.toString();
  }
  return copyrights;
};

/**
 * Gather a flat list of copyright information from the given parameters.
 *
 * @param {H5P.ContentCopyrights} info
 *   Used to collect all information in.
 * @param {(Object|Array)} parameters
 *   To search for file objects in.
 * @param {number} contentId
 *   Used to insert thumbnails for images.
 * @param {Object} extras - Extras.
 * @param {object} extras.metadata - Metadata
 * @param {object} extras.machineName - Library name of some kind.
 *   Metadata of the content instance.
 */
H5P.findCopyrights = function (info, parameters, contentId, extras) {
  // If extras are
  if (extras) {
    extras.params = parameters;
    buildFromMetadata(extras, extras.machineName, contentId);
  }

  var lastContentTypeName;
  // Cycle through parameters
  for (var field in parameters) {
    if (!parameters.hasOwnProperty(field)) {
      continue; // Do not check
    }

    /**
     * @deprecated This hack should be removed after 2017-11-01
     * The code that was using this was removed by HFP-574
     * This note was seen on 2018-04-04, and consultation with
     * higher authorities lead to keeping the code for now ;-)
     */
    if (field === 'overrideSettings') {
      console.warn("The semantics field 'overrideSettings' is DEPRECATED and should not be used.");
      console.warn(parameters);
      continue;
    }

    var value = parameters[field];

    if (value && value.library && typeof value.library === 'string') {
      lastContentTypeName = value.library.split(' ')[0];
    }
    else if (value && value.library && typeof value.library === 'object') {
      lastContentTypeName = (value.library.library && typeof value.library.library === 'string') ? value.library.library.split(' ')[0] : lastContentTypeName;
    }

    if (value instanceof Array) {
      // Cycle through array
      H5P.findCopyrights(info, value, contentId);
    }
    else if (value instanceof Object) {
      buildFromMetadata(value, lastContentTypeName, contentId);

      // Check if object is a file with copyrights (old core)
      if (value.copyright === undefined ||
          value.copyright.license === undefined ||
          value.path === undefined ||
          value.mime === undefined) {

        // Nope, cycle throught object
        H5P.findCopyrights(info, value, contentId);
      }
      else {
        // Found file, add copyrights
        var copyrights = new H5P.MediaCopyright(value.copyright);
        if (value.width !== undefined && value.height !== undefined) {
          copyrights.setThumbnail(new H5P.Thumbnail(H5P.getPath(value.path, contentId), value.width, value.height));
        }
        info.addMedia(copyrights);
      }
    }
  }

  function buildFromMetadata(data, name, contentId) {
    if (data.metadata) {
      const metadataCopyrights = H5P.buildMetadataCopyrights(data.metadata, name);
      if (metadataCopyrights !== undefined) {
        if (data.params && data.params.contentName === 'Image' && data.params.file) {
          const path = data.params.file.path;
          const width = data.params.file.width;
          const height = data.params.file.height;
          metadataCopyrights.setThumbnail(new H5P.Thumbnail(H5P.getPath(path, contentId), width, height, data.params.alt));
        }
        info.addMedia(metadataCopyrights);
      }
    }
  }
};

H5P.buildMetadataCopyrights = function (metadata) {
  if (metadata && metadata.license !== undefined && metadata.license !== 'U') {
    var dataset = {
      contentType: metadata.contentType,
      title: metadata.title,
      author: (metadata.authors && metadata.authors.length > 0) ? metadata.authors.map(function (author) {
        return (author.role) ? author.name + ' (' + author.role + ')' : author.name;
      }).join(', ') : undefined,
      source: metadata.source,
      year: (metadata.yearFrom) ? (metadata.yearFrom + ((metadata.yearTo) ? '-' + metadata.yearTo: '')) : undefined,
      license: metadata.license,
      version: metadata.licenseVersion,
      licenseExtras: metadata.licenseExtras,
      changes: (metadata.changes && metadata.changes.length > 0) ? metadata.changes.map(function (change) {
        return change.log + (change.author ? ', ' + change.author : '') + (change.date ? ', ' + change.date : '');
      }).join(' / ') : undefined
    };

    return new H5P.MediaCopyright(dataset);
  }
};

/**
 * Display a dialog containing the download button and copy button.
 *
 * @param {H5P.jQuery} $element
 * @param {Object} contentData
 * @param {Object} library
 * @param {Object} instance
 * @param {number} contentId
 */
H5P.openReuseDialog = function ($element, contentData, library, instance, contentId) {
  let html = '';
  let buttonCount = 0;
  if (contentData.displayOptions.export) {
    html += '<button type="button" class="h5p-big-button h5p-download-button"><div class="h5p-button-title">Download as an .h5p file</div><div class="h5p-button-description">.h5p files may be uploaded to any web-site where H5P content may be created.</div></button>';
    buttonCount += 1;
  }
  if (contentData.displayOptions.export && contentData.displayOptions.copy) {
    html += '<div class="h5p-horizontal-line-text"><span>or</span></div>';
  }
  if (contentData.displayOptions.copy) {
    html += '<button type="button" class="h5p-big-button h5p-copy-button"><div class="h5p-button-title">Copy content</div><div class="h5p-button-description">Copied content may be pasted anywhere this content type is supported on this website.</div></button>';
    buttonCount += 1;
  }

  const dialog = new H5P.Dialog('reuse', H5P.t('reuseContent'), html, $element);

  // Selecting embed code when dialog is opened
  H5P.jQuery(dialog).on('dialog-opened', function (e, $dialog) {
    const scrollContent = $dialog[0].querySelector('.h5p-scroll-content');
    scrollContent.style.setProperty('--button-count', buttonCount);
    H5P.jQuery('<a href="https://h5p.org/node/442225" target="_blank">More Info</a>').click(function (e) {
      e.stopPropagation();
    }).appendTo($dialog.find('h2'));
    $dialog.find('.h5p-download-button').click(function () {
      window.location.href = contentData.exportUrl;
      instance.triggerXAPI('downloaded');
      dialog.close();
    });
    $dialog.find('.h5p-copy-button').click(function () {
      const item = new H5P.ClipboardItem(library);
      item.contentId = contentId;
      H5P.setClipboard(item);
      instance.triggerXAPI('copied');
      dialog.close();
      H5P.attachToastTo(
        H5P.jQuery('.h5p-content:first')[0],
        H5P.t('contentCopied'),
        {
          position: {
            horizontal: 'centered',
            vertical: 'centered',
            noOverflowX: true
          }
        }
      );
    });
    H5P.trigger(instance, 'resize');
  }).on('dialog-closed', function () {
    H5P.trigger(instance, 'resize');
  });

  dialog.open();
};

/**
 * Display a dialog containing the embed code.
 *
 * @param {H5P.jQuery} $element
 *   Element to insert dialog after.
 * @param {string} embedCode
 *   The embed code.
 * @param {string} resizeCode
 *   The advanced resize code
 * @param {Object} size
 *   The content's size.
 * @param {number} size.width
 * @param {number} size.height
 */
H5P.openEmbedDialog = function ($element, embedCode, resizeCode, size, instance) {
  var fullEmbedCode = embedCode + resizeCode;
  var dialog = new H5P.Dialog('embed', H5P.t('embed'), '<textarea class="h5p-embed-code-container" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>' + H5P.t('size') + ': <input aria-label="'+ H5P.t('width') +'" type="text" value="' + Math.ceil(size.width) + '" class="h5p-embed-size"/> × <input aria-label="'+ H5P.t('width') +'" type="text" value="' + Math.ceil(size.height) + '" class="h5p-embed-size"/> px<br/><div role="button" tabindex="0" class="h5p-expander">' + H5P.t('showAdvanced') + '</div><div class="h5p-expander-content"><p>' + H5P.t('advancedHelp') + '</p><textarea class="h5p-embed-code-container" autocorrect="off" autocapitalize="off" spellcheck="false">' + resizeCode + '</textarea></div>', $element);

  // Selecting embed code when dialog is opened
  H5P.jQuery(dialog).on('dialog-opened', function (event, $dialog) {
    var $inner = $dialog.find('.h5p-inner');
    var $scroll = $inner.find('.h5p-scroll-content');
    var diff = $scroll.outerHeight() - $scroll.innerHeight();
    var positionInner = function () {
      H5P.trigger(instance, 'resize');
    };

    // Handle changing of width/height
    var $w = $dialog.find('.h5p-embed-size:eq(0)');
    var $h = $dialog.find('.h5p-embed-size:eq(1)');
    var getNum = function ($e, d) {
      var num = parseFloat($e.val());
      if (isNaN(num)) {
        return d;
      }
      return Math.ceil(num);
    };
    var updateEmbed = function () {
      $dialog.find('.h5p-embed-code-container:first').val(fullEmbedCode.replace(':w', getNum($w, size.width)).replace(':h', getNum($h, size.height)));
    };

    $w.change(updateEmbed);
    $h.change(updateEmbed);
    updateEmbed();

    // Select text and expand textareas
    $dialog.find('.h5p-embed-code-container').each(function () {
      H5P.jQuery(this).css('height', this.scrollHeight + 'px').focus(function () {
        H5P.jQuery(this).select();
      });
    });
    $dialog.find('.h5p-embed-code-container').eq(0).select();
    positionInner();

    // Expand advanced embed
    var expand = function () {
      var $expander = H5P.jQuery(this);
      var $content = $expander.next();
      if ($content.is(':visible')) {
        $expander.removeClass('h5p-open').text(H5P.t('showAdvanced')).attr('aria-expanded', 'true');
        $content.hide();
      }
      else {
        $expander.addClass('h5p-open').text(H5P.t('hideAdvanced')).attr('aria-expanded', 'false');
        $content.show();
      }
      $dialog.find('.h5p-embed-code-container').each(function () {
        H5P.jQuery(this).css('height', this.scrollHeight + 'px');
      });
      positionInner();
    };
    $dialog.find('.h5p-expander').click(expand).keypress(function (event) {
      if (event.keyCode === 32) {
        expand.apply(this);
        return false;
      }
    });
  }).on('dialog-closed', function () {
    H5P.trigger(instance, 'resize');
  });

  dialog.open();
};

/**
 * Show a toast message.
 *
 * The reference element could be dom elements the toast should be attached to,
 * or e.g. the document body for general toast messages.
 *
 * @param {DOM} element Reference element to show toast message for.
 * @param {string} message Message to show.
 * @param {object} [config] Configuration.
 * @param {string} [config.style=h5p-toast] Style name for the tooltip.
 * @param {number} [config.duration=3000] Toast message length in ms.
 * @param {object} [config.position] Relative positioning of the toast.
 * @param {string} [config.position.horizontal=centered] [before|left|centered|right|after].
 * @param {string} [config.position.vertical=below] [above|top|centered|bottom|below].
 * @param {number} [config.position.offsetHorizontal=0] Extra horizontal offset.
 * @param {number} [config.position.offsetVertical=0] Extra vetical offset.
 * @param {boolean} [config.position.noOverflowLeft=false] True to prevent overflow left.
 * @param {boolean} [config.position.noOverflowRight=false] True to prevent overflow right.
 * @param {boolean} [config.position.noOverflowTop=false] True to prevent overflow top.
 * @param {boolean} [config.position.noOverflowBottom=false] True to prevent overflow bottom.
 * @param {boolean} [config.position.noOverflowX=false] True to prevent overflow left and right.
 * @param {boolean} [config.position.noOverflowY=false] True to prevent overflow top and bottom.
 * @param {object} [config.position.overflowReference=document.body] DOM reference for overflow.
 */
H5P.attachToastTo = function (element, message, config) {
  if (element === undefined || message === undefined) {
    return;
  }

  const eventPath = function (evt) {
    var path = (evt.composedPath && evt.composedPath()) || evt.path;
    var target = evt.target;

    if (path != null) {
      // Safari doesn't include Window, but it should.
      return (path.indexOf(window) < 0) ? path.concat(window) : path;
    }

    if (target === window) {
      return [window];
    }

    function getParents(node, memo) {
      memo = memo || [];
      var parentNode = node.parentNode;

      if (!parentNode) {
        return memo;
      }
      else {
        return getParents(parentNode, memo.concat(parentNode));
      }
    }

    return [target].concat(getParents(target), window);
  };

  /**
   * Handle click while toast is showing.
   */
  const clickHandler = function (event) {
    /*
     * A common use case will be to attach toasts to buttons that are clicked.
     * The click would remove the toast message instantly without this check.
     * Children of the clicked element are also ignored.
     */
    var path = eventPath(event);
    if (path.indexOf(element) !== -1) {
      return;
    }
    clearTimeout(timer);
    removeToast();
  };



  /**
   * Remove the toast message.
   */
  const removeToast = function () {
    document.removeEventListener('click', clickHandler);
    if (toast.parentNode) {
      toast.parentNode.removeChild(toast);
    }
  };

  /**
   * Get absolute coordinates for the toast.
   *
   * @param {DOM} element Reference element to show toast message for.
   * @param {DOM} toast Toast element.
   * @param {object} [position={}] Relative positioning of the toast message.
   * @param {string} [position.horizontal=centered] [before|left|centered|right|after].
   * @param {string} [position.vertical=below] [above|top|centered|bottom|below].
   * @param {number} [position.offsetHorizontal=0] Extra horizontal offset.
   * @param {number} [position.offsetVertical=0] Extra vetical offset.
   * @param {boolean} [position.noOverflowLeft=false] True to prevent overflow left.
   * @param {boolean} [position.noOverflowRight=false] True to prevent overflow right.
   * @param {boolean} [position.noOverflowTop=false] True to prevent overflow top.
   * @param {boolean} [position.noOverflowBottom=false] True to prevent overflow bottom.
   * @param {boolean} [position.noOverflowX=false] True to prevent overflow left and right.
   * @param {boolean} [position.noOverflowY=false] True to prevent overflow top and bottom.
   * @return {object}
   */
  const getToastCoordinates = function (element, toast, position) {
    position = position || {};
    position.offsetHorizontal = position.offsetHorizontal || 0;
    position.offsetVertical = position.offsetVertical || 0;

    const toastRect = toast.getBoundingClientRect();
    const elementRect = element.getBoundingClientRect();

    let left = 0;
    let top = 0;

    // Compute horizontal position
    switch (position.horizontal) {
      case 'before':
        left = elementRect.left - toastRect.width - position.offsetHorizontal;
        break;
      case 'after':
        left = elementRect.left + elementRect.width + position.offsetHorizontal;
        break;
      case 'left':
        left = elementRect.left + position.offsetHorizontal;
        break;
      case 'right':
        left = elementRect.left + elementRect.width - toastRect.width - position.offsetHorizontal;
        break;
      case 'centered':
        left = elementRect.left + elementRect.width / 2 - toastRect.width / 2 + position.offsetHorizontal;
        break;
      default:
        left = elementRect.left + elementRect.width / 2 - toastRect.width / 2 + position.offsetHorizontal;
    }

    // Compute vertical position
    switch (position.vertical) {
      case 'above':
        top = elementRect.top - toastRect.height - position.offsetVertical;
        break;
      case 'below':
        top = elementRect.top + elementRect.height + position.offsetVertical;
        break;
      case 'top':
        top = elementRect.top + position.offsetVertical;
        break;
      case 'bottom':
        top = elementRect.top + elementRect.height - toastRect.height - position.offsetVertical;
        break;
      case 'centered':
        top = elementRect.top + elementRect.height / 2 - toastRect.height / 2 + position.offsetVertical;
        break;
      default:
        top = elementRect.top + elementRect.height + position.offsetVertical;
    }

    // Prevent overflow
    const overflowElement = document.body;
    const bounds = overflowElement.getBoundingClientRect();
    if ((position.noOverflowLeft || position.noOverflowX) && (left < bounds.x)) {
      left = bounds.x;
    }
    if ((position.noOverflowRight || position.noOverflowX) && ((left + toastRect.width) > (bounds.x + bounds.width))) {
      left = bounds.x + bounds.width - toastRect.width;
    }
    if ((position.noOverflowTop || position.noOverflowY) && (top < bounds.y)) {
      top = bounds.y;
    }
    if ((position.noOverflowBottom || position.noOverflowY) && ((top + toastRect.height) > (bounds.y + bounds.height))) {
      left = bounds.y + bounds.height - toastRect.height;
    }

    return {left: left, top: top};
  };

  // Sanitization
  config = config || {};
  config.style = config.style || 'h5p-toast';
  config.duration = config.duration || 3000;

  // Build toast
  const toast = document.createElement('div');
  toast.setAttribute('id', config.style);
  toast.classList.add('h5p-toast-disabled');
  toast.classList.add(config.style);

  const msg = document.createElement('span');
  msg.innerHTML = message;
  toast.appendChild(msg);

  document.body.appendChild(toast);

  // The message has to be set before getting the coordinates
  const coordinates = getToastCoordinates(element, toast, config.position);
  toast.style.left = Math.round(coordinates.left) + 'px';
  toast.style.top = Math.round(coordinates.top) + 'px';

  toast.classList.remove('h5p-toast-disabled');
  const timer = setTimeout(removeToast, config.duration);

  // The toast can also be removed by clicking somewhere
  document.addEventListener('click', clickHandler);
};

/**
 * Copyrights for a H5P Content Library.
 *
 * @class
 */
H5P.ContentCopyrights = function () {
  var label;
  var media = [];
  var content = [];

  /**
   * Set label.
   *
   * @param {string} newLabel
   */
  this.setLabel = function (newLabel) {
    label = newLabel;
  };

  /**
   * Add sub content.
   *
   * @param {H5P.MediaCopyright} newMedia
   */
  this.addMedia = function (newMedia) {
    if (newMedia !== undefined) {
      media.push(newMedia);
    }
  };

  /**
   * Add sub content in front.
   *
   * @param {H5P.MediaCopyright} newMedia
   */
  this.addMediaInFront = function (newMedia) {
    if (newMedia !== undefined) {
      media.unshift(newMedia);
    }
  };

  /**
   * Add sub content.
   *
   * @param {H5P.ContentCopyrights} newContent
   */
  this.addContent = function (newContent) {
    if (newContent !== undefined) {
      content.push(newContent);
    }
  };

  /**
   * Print content copyright.
   *
   * @returns {string} HTML.
   */
  this.toString = function () {
    var html = '';

    // Add media rights
    for (var i = 0; i < media.length; i++) {
      html += media[i];
    }

    // Add sub content rights
    for (i = 0; i < content.length; i++) {
      html += content[i];
    }


    if (html !== '') {
      // Add a label to this info
      if (label !== undefined) {
        html = '<h3>' + label + '</h3>' + html;
      }

      // Add wrapper
      html = '<div class="h5p-content-copyrights">' + html + '</div>';
    }

    return html;
  };
};

/**
 * A ordered list of copyright fields for media.
 *
 * @class
 * @param {Object} copyright
 *   Copyright information fields.
 * @param {Object} [labels]
 *   Translation of labels.
 * @param {Array} [order]
 *   Order of the fields.
 * @param {Object} [extraFields]
 *   Add extra copyright fields.
 */
H5P.MediaCopyright = function (copyright, labels, order, extraFields) {
  var thumbnail;
  var list = new H5P.DefinitionList();

  /**
   * Get translated label for field.
   *
   * @private
   * @param {string} fieldName
   * @returns {string}
   */
  var getLabel = function (fieldName) {
    if (labels === undefined || labels[fieldName] === undefined) {
      return H5P.t(fieldName);
    }

    return labels[fieldName];
  };

  /**
   * Get humanized value for the license field.
   *
   * @private
   * @param {string} license
   * @param {string} [version]
   * @returns {string}
   */
  var humanizeLicense = function (license, version) {
    var copyrightLicense = H5P.copyrightLicenses[license];

    // Build license string
    var value = '';
    if (!(license === 'PD' && version)) {
      // Add license label
      value += (copyrightLicense.hasOwnProperty('label') ? copyrightLicense.label : copyrightLicense);
    }

    // Check for version info
    var versionInfo;
    if (copyrightLicense.versions) {
      if (copyrightLicense.versions.default && (!version || !copyrightLicense.versions[version])) {
        version = copyrightLicense.versions.default;
      }
      if (version && copyrightLicense.versions[version]) {
        versionInfo = copyrightLicense.versions[version];
      }
    }

    if (versionInfo) {
      // Add license version
      if (value) {
        value += ' ';
      }
      value += (versionInfo.hasOwnProperty('label') ? versionInfo.label : versionInfo);
    }

    // Add link if specified
    var link;
    if (copyrightLicense.hasOwnProperty('link')) {
      link = copyrightLicense.link.replace(':version', copyrightLicense.linkVersions ? copyrightLicense.linkVersions[version] : version);
    }
    else if (versionInfo && copyrightLicense.hasOwnProperty('link')) {
      link = versionInfo.link;
    }
    if (link) {
      value = '<a href="' + link + '" target="_blank">' + value + '</a>';
    }

    // Generate parenthesis
    var parenthesis = '';
    if (license !== 'PD' && license !== 'C') {
      parenthesis += license;
    }
    if (version && version !== 'CC0 1.0') {
      if (parenthesis && license !== 'GNU GPL') {
        parenthesis += ' ';
      }
      parenthesis += version;
    }
    if (parenthesis) {
      value += ' (' + parenthesis + ')';
    }
    if (license === 'C') {
      value += ' &copy;';
    }

    return value;
  };

  if (copyright !== undefined) {
    // Add the extra fields
    for (var field in extraFields) {
      if (extraFields.hasOwnProperty(field)) {
        copyright[field] = extraFields[field];
      }
    }

    if (order === undefined) {
      // Set default order
      order = ['contentType', 'title', 'license', 'author', 'year', 'source', 'licenseExtras', 'changes'];
    }

    for (var i = 0; i < order.length; i++) {
      var fieldName = order[i];
      if (copyright[fieldName] !== undefined && copyright[fieldName] !== '') {
        var humanValue = copyright[fieldName];
        if (fieldName === 'license') {
          humanValue = humanizeLicense(copyright.license, copyright.version);
        }
        if (fieldName === 'source') {
          humanValue = (humanValue) ? '<a href="' + humanValue + '" target="_blank">' + humanValue + '</a>' : undefined;
        }
        list.add(new H5P.Field(getLabel(fieldName), humanValue));
      }
    }
  }

  /**
   * Set thumbnail.
   *
   * @param {H5P.Thumbnail} newThumbnail
   */
  this.setThumbnail = function (newThumbnail) {
    thumbnail = newThumbnail;
  };

  /**
   * Checks if this copyright is undisclosed.
   * I.e. only has the license attribute set, and it's undisclosed.
   *
   * @returns {boolean}
   */
  this.undisclosed = function () {
    if (list.size() === 1) {
      var field = list.get(0);
      if (field.getLabel() === getLabel('license') && field.getValue() === humanizeLicense('U')) {
        return true;
      }
    }
    return false;
  };

  /**
   * Print media copyright.
   *
   * @returns {string} HTML.
   */
  this.toString = function () {
    var html = '';

    if (this.undisclosed()) {
      return html; // No need to print a copyright with a single undisclosed license.
    }

    if (thumbnail !== undefined) {
      html += thumbnail;
    }
    html += list;

    if (html !== '') {
      html = '<div class="h5p-media-copyright">' + html + '</div>';
    }

    return html;
  };
};

/**
 * A simple and elegant class for creating thumbnails of images.
 *
 * @class
 * @param {string} source
 * @param {number} width
 * @param {number} height
 * @param {string} alt
 *  alternative text for the thumbnail
 */
H5P.Thumbnail = function (source, width, height, alt) {
  var thumbWidth, thumbHeight = 100;
  if (width !== undefined) {
    thumbWidth = Math.round(thumbHeight * (width / height));
  }

  /**
   * Print thumbnail.
   *
   * @returns {string} HTML.
   */
  this.toString = function () {
    return '<img src="' + source + '" alt="' + (alt ? alt : '') + '" class="h5p-thumbnail" height="' + thumbHeight + '"' + (thumbWidth === undefined ? '' : ' width="' + thumbWidth + '"') + '/>';
  };
};

/**
 * Simple data structure class for storing a single field.
 *
 * @class
 * @param {string} label
 * @param {string} value
 */
H5P.Field = function (label, value) {
  /**
   * Public. Get field label.
   *
   * @returns {String}
   */
  this.getLabel = function () {
    return label;
  };

  /**
   * Public. Get field value.
   *
   * @returns {String}
   */
  this.getValue = function () {
    return value;
  };
};

/**
 * Simple class for creating a definition list.
 *
 * @class
 */
H5P.DefinitionList = function () {
  var fields = [];

  /**
   * Add field to list.
   *
   * @param {H5P.Field} field
   */
  this.add = function (field) {
    fields.push(field);
  };

  /**
   * Get Number of fields.
   *
   * @returns {number}
   */
  this.size = function () {
    return fields.length;
  };

  /**
   * Get field at given index.
   *
   * @param {number} index
   * @returns {H5P.Field}
   */
  this.get = function (index) {
    return fields[index];
  };

  /**
   * Print definition list.
   *
   * @returns {string} HTML.
   */
  this.toString = function () {
    var html = '';
    for (var i = 0; i < fields.length; i++) {
      var field = fields[i];
      html += '<dt>' + field.getLabel() + '</dt><dd>' + field.getValue() + '</dd>';
    }
    return (html === '' ? html : '<dl class="h5p-definition-list">' + html + '</dl>');
  };
};

/**
 * THIS FUNCTION/CLASS IS DEPRECATED AND WILL BE REMOVED.
 *
 * Helper object for keeping coordinates in the same format all over.
 *
 * @deprecated
 *   Will be removed march 2016.
 * @class
 * @param {number} x
 * @param {number} y
 * @param {number} w
 * @param {number} h
 */
H5P.Coords = function (x, y, w, h) {
  if ( !(this instanceof H5P.Coords) )
    return new H5P.Coords(x, y, w, h);

  /** @member {number} */
  this.x = 0;
  /** @member {number} */
  this.y = 0;
  /** @member {number} */
  this.w = 1;
  /** @member {number} */
  this.h = 1;

  if (typeof(x) === 'object') {
    this.x = x.x;
    this.y = x.y;
    this.w = x.w;
    this.h = x.h;
  }
  else {
    if (x !== undefined) {
      this.x = x;
    }
    if (y !== undefined) {
      this.y = y;
    }
    if (w !== undefined) {
      this.w = w;
    }
    if (h !== undefined) {
      this.h = h;
    }
  }
  return this;
};

/**
 * Parse library string into values.
 *
 * @param {string} library
 *   library in the format "machineName majorVersion.minorVersion"
 * @returns {Object}
 *   library as an object with machineName, majorVersion and minorVersion properties
 *   return false if the library parameter is invalid
 */
H5P.libraryFromString = function (library) {
  var regExp = /(.+)\s(\d+)\.(\d+)$/g;
  var res = regExp.exec(library);
  if (res !== null) {
    return {
      'machineName': res[1],
      'majorVersion': parseInt(res[2]),
      'minorVersion': parseInt(res[3])
    };
  }
  else {
    return false;
  }
};

/**
 * Get the path to the library
 *
 * @param {string} library
 *   The library identifier in the format "machineName-majorVersion.minorVersion".
 * @returns {string}
 *   The full path to the library.
 */
H5P.getLibraryPath = function (library) {
  if (H5PIntegration &&
      H5PIntegration.libraryDirectories &&
      library in H5PIntegration.libraryDirectories) {
    // Use H5PIntegration.libraryDirectories if it exists for this library
    library = H5PIntegration.libraryDirectories[library];
  }

  if (H5PIntegration.urlLibraries !== undefined) {
    // This is an override for those implementations that has a different libraries URL, e.g. Moodle
    return H5PIntegration.urlLibraries + '/' + library;
  }
  else {
    return H5PIntegration.url + '/libraries/' + library;
  }
};

/**
 * Recursivly clone the given object.
 *
 * @param {Object|Array} object
 *   Object to clone.
 * @param {boolean} [recursive]
 * @returns {Object|Array}
 *   A clone of object.
 */
H5P.cloneObject = function (object, recursive) {
  // TODO: Consider if this needs to be in core. Doesn't $.extend do the same?
  var clone = object instanceof Array ? [] : {};

  for (var i in object) {
    if (object.hasOwnProperty(i)) {
      if (recursive !== undefined && recursive && typeof object[i] === 'object') {
        clone[i] = H5P.cloneObject(object[i], recursive);
      }
      else {
        clone[i] = object[i];
      }
    }
  }

  return clone;
};

/**
 * Remove all empty spaces before and after the value.
 *
 * @param {string} value
 * @returns {string}
 */
H5P.trim = function (value) {
  return value.replace(/^\s+|\s+$/g, '');

  // TODO: Only include this or String.trim(). What is best?
  // I'm leaning towards implementing the missing ones: http://kangax.github.io/compat-table/es5/
  // So should we make this function deprecated?
};

/**
 * Recursive function that detects deep empty structures.
 *
 * @param {*} value
 * @returns {bool}
 */
H5P.isEmpty = value => {
  if (!value && value !== 0 && value !== false) {
    return true; // undefined, null, NaN and empty strings.
  }
  else if (Array.isArray(value)) {
    for (let i = 0; i < value.length; i++) {
      if (!H5P.isEmpty(value[i])) {
        return false; // Array contains a non-empty value
      }
    }
    return true; // Empty array
  }
  else if (typeof value === 'object') {
    for (let prop in value) {
      if (value.hasOwnProperty(prop) && !H5P.isEmpty(value[prop])) {
        return false; // Object contains a non-empty value
      }
    }
    return true; // Empty object
  }
  return false;
};

/**
 * Check if JavaScript path/key is loaded.
 *
 * @param {string} path
 * @returns {boolean}
 */
H5P.jsLoaded = function (path) {
  H5PIntegration.loadedJs = H5PIntegration.loadedJs || [];
  return H5P.jQuery.inArray(path, H5PIntegration.loadedJs) !== -1;
};

/**
 * Check if styles path/key is loaded.
 *
 * @param {string} path
 * @returns {boolean}
 */
H5P.cssLoaded = function (path) {
  H5PIntegration.loadedCss = H5PIntegration.loadedCss || [];
  return H5P.jQuery.inArray(path, H5PIntegration.loadedCss) !== -1;
};

/**
 * Shuffle an array in place.
 *
 * @param {Array} array
 *   Array to shuffle
 * @returns {Array}
 *   The passed array is returned for chaining.
 */
H5P.shuffleArray = function (array) {
  // TODO: Consider if this should be a part of core. I'm guessing very few libraries are going to use it.
  if (!(array instanceof Array)) {
    return;
  }

  var i = array.length, j, tempi, tempj;
  if ( i === 0 ) return false;
  while ( --i ) {
    j       = Math.floor( Math.random() * ( i + 1 ) );
    tempi   = array[i];
    tempj   = array[j];
    array[i] = tempj;
    array[j] = tempi;
  }
  return array;
};

/**
 * Post finished results for user.
 *
 * @deprecated
 *   Do not use this function directly, trigger the finish event instead.
 *   Will be removed march 2016
 * @param {number} contentId
 *   Identifies the content
 * @param {number} score
 *   Achieved score/points
 * @param {number} maxScore
 *   The maximum score/points that can be achieved
 * @param {number} [time]
 *   Reported time consumption/usage
 */
H5P.setFinished = function (contentId, score, maxScore, time) {
  var validScore = typeof score === 'number' || score instanceof Number;
  if (validScore && H5PIntegration.postUserStatistics === true) {
    /**
     * Return unix timestamp for the given JS Date.
     *
     * @private
     * @param {Date} date
     * @returns {Number}
     */
    var toUnix = function (date) {
      return Math.round(date.getTime() / 1000);
    };

    // Post the results
    const data = {
      contentId: contentId,
      score: score,
      maxScore: maxScore,
      opened: toUnix(H5P.opened[contentId]),
      finished: toUnix(new Date()),
      time: time
    };
    H5P.jQuery.post(H5PIntegration.ajax.setFinished, data)
      .fail(function () {
        H5P.offlineRequestQueue.add(H5PIntegration.ajax.setFinished, data);
      });
  }
};

// Add indexOf to browsers that lack them. (IEs)
if (!Array.prototype.indexOf) {
  Array.prototype.indexOf = function (needle) {
    for (var i = 0; i < this.length; i++) {
      if (this[i] === needle) {
        return i;
      }
    }
    return -1;
  };
}

// Need to define trim() since this is not available on older IEs,
// and trim is used in several libs
if (String.prototype.trim === undefined) {
  String.prototype.trim = function () {
    return H5P.trim(this);
  };
}

/**
 * Trigger an event on an instance
 *
 * Helper function that triggers an event if the instance supports event handling
 *
 * @param {Object} instance
 *   Instance of H5P content
 * @param {string} eventType
 *   Type of event to trigger
 * @param {*} data
 * @param {Object} extras
 */
H5P.trigger = function (instance, eventType, data, extras) {
  // Try new event system first
  if (instance.trigger !== undefined) {
    instance.trigger(eventType, data, extras);
  }
  // Try deprecated event system
  else if (instance.$ !== undefined && instance.$.trigger !== undefined) {
    instance.$.trigger(eventType);
  }
};

/**
 * Register an event handler
 *
 * Helper function that registers an event handler for an event type if
 * the instance supports event handling
 *
 * @param {Object} instance
 *   Instance of H5P content
 * @param {string} eventType
 *   Type of event to listen for
 * @param {H5P.EventCallback} handler
 *   Callback that gets triggered for events of the specified type
 */
H5P.on = function (instance, eventType, handler) {
  // Try new event system first
  if (instance.on !== undefined) {
    instance.on(eventType, handler);
  }
  // Try deprecated event system
  else if (instance.$ !== undefined && instance.$.on !== undefined) {
    instance.$.on(eventType, handler);
  }
};

/**
 * Generate random UUID
 *
 * @returns {string} UUID
 */
H5P.createUUID = function () {
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (char) {
    var random = Math.random()*16|0, newChar = char === 'x' ? random : (random&0x3|0x8);
    return newChar.toString(16);
  });
};

/**
 * Create title
 *
 * @param {string} rawTitle
 * @param {number} maxLength
 * @returns {string}
 */
H5P.createTitle = function (rawTitle, maxLength) {
  if (!rawTitle) {
    return '';
  }
  if (maxLength === undefined) {
    maxLength = 60;
  }
  var title = H5P.jQuery('<div></div>')
    .text(
      // Strip tags
      rawTitle.replace(/(<([^>]+)>)/ig,"")
    // Escape
    ).text();
  if (title.length > maxLength) {
    title = title.substr(0, maxLength - 3) + '...';
  }
  return title;
};

// Wrap in privates
(function ($) {

  /**
   * Creates ajax requests for inserting, updateing and deleteing
   * content user data.
   *
   * @private
   * @param {number} contentId What content to store the data for.
   * @param {string} dataType Identifies the set of data for this content.
   * @param {string} subContentId Identifies sub content
   * @param {function} [done] Callback when ajax is done.
   * @param {object} [data] To be stored for future use.
   * @param {boolean} [preload=false] Data is loaded when content is loaded.
   * @param {boolean} [invalidate=false] Data is invalidated when content changes.
   * @param {boolean} [async=true]
   */
  function contentUserDataAjax(contentId, dataType, subContentId, done, data, preload, invalidate, async) {
    if (H5PIntegration.user === undefined) {
      // Not logged in, no use in saving.
      done('Not signed in.');
      return;
    }

    var options = {
      url: H5PIntegration.ajax.contentUserData.replace(':contentId', contentId).replace(':dataType', dataType).replace(':subContentId', subContentId ? subContentId : 0),
      dataType: 'json',
      async: async === undefined ? true : async
    };
    if (data !== undefined) {
      options.type = 'POST';
      options.data = {
        data: (data === null ? 0 : data),
        preload: (preload ? 1 : 0),
        invalidate: (invalidate ? 1 : 0)
      };
    }
    else {
      options.type = 'GET';
    }
    if (done !== undefined) {
      options.error = function (xhr, error) {
        done(error);
      };
      options.success = function (response) {
        if (!response.success) {
          done(response.message);
          return;
        }

        if (response.data === false || response.data === undefined) {
          done();
          return;
        }

        done(undefined, response.data);
      };
    }

    $.ajax(options);
  }

  /**
   * Get user data for given content.
   *
   * @param {number} contentId
   *   What content to get data for.
   * @param {string} dataId
   *   Identifies the set of data for this content.
   * @param {function} done
   *   Callback with error and data parameters.
   * @param {string} [subContentId]
   *   Identifies which data belongs to sub content.
   */
  H5P.getUserData = function (contentId, dataId, done, subContentId) {
    if (!subContentId) {
      subContentId = 0; // Default
    }

    H5PIntegration.contents = H5PIntegration.contents || {};
    var content = H5PIntegration.contents['cid-' + contentId] || {};
    var preloadedData = content.contentUserData;
    if (preloadedData && preloadedData[subContentId] && preloadedData[subContentId][dataId] !== undefined) {
      if (preloadedData[subContentId][dataId] === 'RESET') {
        done(undefined, null);
        return;
      }
      try {
        done(undefined, JSON.parse(preloadedData[subContentId][dataId]));
      }
      catch (err) {
        done(err);
      }
    }
    else {
      contentUserDataAjax(contentId, dataId, subContentId, function (err, data) {
        if (err || data === undefined) {
          done(err, data);
          return; // Error or no data
        }

        // Cache in preloaded
        if (content.contentUserData === undefined) {
          content.contentUserData = preloadedData = {};
        }
        if (preloadedData[subContentId] === undefined) {
          preloadedData[subContentId] = {};
        }
        preloadedData[subContentId][dataId] = data;

        // Done. Try to decode JSON
        try {
          done(undefined, JSON.parse(data));
        }
        catch (e) {
          done(e);
        }
      });
    }
  };

  /**
   * Async error handling.
   *
   * @callback H5P.ErrorCallback
   * @param {*} error
   */

  /**
   * Set user data for given content.
   *
   * @param {number} contentId
   *   What content to get data for.
   * @param {string} dataId
   *   Identifies the set of data for this content.
   * @param {Object} data
   *   The data that is to be stored.
   * @param {Object} [extras]
   *   Extra properties
   * @param {string} [extras.subContentId]
   *   Identifies which data belongs to sub content.
   * @param {boolean} [extras.preloaded=true]
   *   If the data should be loaded when content is loaded.
   * @param {boolean} [extras.deleteOnChange=false]
   *   If the data should be invalidated when the content changes.
   * @param {H5P.ErrorCallback} [extras.errorCallback]
   *   Callback with error as parameters.
   * @param {boolean} [extras.async=true]
   */
  H5P.setUserData = function (contentId, dataId, data, extras) {
    var options = H5P.jQuery.extend(true, {}, {
      subContentId: 0,
      preloaded: true,
      deleteOnChange: false,
      async: true
    }, extras);

    try {
      data = JSON.stringify(data);
    }
    catch (err) {
      if (options.errorCallback) {
        options.errorCallback(err);
      }
      return; // Failed to serialize.
    }

    var content = H5PIntegration.contents['cid-' + contentId];
    if (content === undefined) {
      content = H5PIntegration.contents['cid-' + contentId] = {};
    }
    if (!content.contentUserData) {
      content.contentUserData = {};
    }
    var preloadedData = content.contentUserData;
    if (preloadedData[options.subContentId] === undefined) {
      preloadedData[options.subContentId] = {};
    }
    if (data === preloadedData[options.subContentId][dataId]) {
      return; // No need to save this twice.
    }

    preloadedData[options.subContentId][dataId] = data;
    contentUserDataAjax(contentId, dataId, options.subContentId, function (error) {
      if (options.errorCallback && error) {
        options.errorCallback(error);
      }
    }, data, options.preloaded, options.deleteOnChange, options.async);
  };

  /**
   * Delete user data for given content.
   *
   * @param {number} contentId
   *   What content to remove data for.
   * @param {string} dataId
   *   Identifies the set of data for this content.
   * @param {string} [subContentId]
   *   Identifies which data belongs to sub content.
   */
  H5P.deleteUserData = function (contentId, dataId, subContentId) {
    if (!subContentId) {
      subContentId = 0; // Default
    }

    // Remove from preloaded/cache
    var preloadedData = H5PIntegration.contents['cid-' + contentId].contentUserData;
    if (preloadedData && preloadedData[subContentId] && preloadedData[subContentId][dataId]) {
      delete preloadedData[subContentId][dataId];
    }

    contentUserDataAjax(contentId, dataId, subContentId, undefined, null);
  };

  /**
   * Function for getting content for a certain ID
   *
   * @param {number} contentId
   * @return {Object}
   */
  H5P.getContentForInstance = function (contentId) {
    var key = 'cid-' + contentId;
    var exists = H5PIntegration && H5PIntegration.contents &&
                 H5PIntegration.contents[key];

    return exists ? H5PIntegration.contents[key] : undefined;
  };

  /**
   * Prepares the content parameters for storing in the clipboard.
   *
   * @class
   * @param {Object} parameters The parameters for the content to store
   * @param {string} [genericProperty] If only part of the parameters are generic, which part
   * @param {string} [specificKey] If the parameters are specific, what content type does it fit
   * @returns {Object} Ready for the clipboard
   */
  H5P.ClipboardItem = function (parameters, genericProperty, specificKey) {
    var self = this;

    /**
     * Set relative dimensions when params contains a file with a width and a height.
     * Very useful to be compatible with wysiwyg editors.
     *
     * @private
     */
    var setDimensionsFromFile = function () {
      if (!self.generic) {
        return;
      }
      var params = self.specific[self.generic];
      if (!params.params.file || !params.params.file.width || !params.params.file.height) {
        return;
      }

      self.width = 20; // %
      self.height = (params.params.file.height / params.params.file.width) * self.width;
    };

    if (!genericProperty) {
      genericProperty = 'action';
      parameters = {
        action: parameters
      };
    }

    self.specific = parameters;

    if (genericProperty && parameters[genericProperty]) {
      self.generic = genericProperty;
    }
    if (specificKey) {
      self.from = specificKey;
    }

    if (window.H5PEditor && H5PEditor.contentId) {
      self.contentId = H5PEditor.contentId;
    }

    if (!self.specific.width && !self.specific.height) {
      setDimensionsFromFile();
    }
  };

  /**
   * Store item in the H5P Clipboard.
   *
   * @param {H5P.ClipboardItem|*} clipboardItem
   */
  H5P.clipboardify = function (clipboardItem) {
    if (!(clipboardItem instanceof H5P.ClipboardItem)) {
      clipboardItem = new H5P.ClipboardItem(clipboardItem);
    }
    H5P.setClipboard(clipboardItem);
  };

  /**
   * Retrieve parsed clipboard data.
   *
   * @return {Object}
   */
  H5P.getClipboard = function () {
    return parseClipboard();
  };

  /**
   * Set item in the H5P Clipboard.
   *
   * @param {H5P.ClipboardItem|object} clipboardItem - Data to be set.
   */
  H5P.setClipboard = function (clipboardItem) {
    localStorage.setItem('h5pClipboard', JSON.stringify(clipboardItem));

    // Trigger an event so all 'Paste' buttons may be enabled.
    H5P.externalDispatcher.trigger('datainclipboard', {reset: false});
  };

  /**
   * Get config for a library
   *
   * @param string machineName
   * @return Object
   */
  H5P.getLibraryConfig = function (machineName) {
    var hasConfig = H5PIntegration.libraryConfig && H5PIntegration.libraryConfig[machineName];
    return hasConfig ? H5PIntegration.libraryConfig[machineName] : {};
  };

  /**
   * Get item from the H5P Clipboard.
   *
   * @private
   * @return {Object}
   */
  var parseClipboard = function () {
    var clipboardData = localStorage.getItem('h5pClipboard');
    if (!clipboardData) {
      return;
    }

    // Try to parse clipboard dat
    try {
      clipboardData = JSON.parse(clipboardData);
    }
    catch (err) {
      console.error('Unable to parse JSON from clipboard.', err);
      return;
    }

    // Update file URLs and reset content Ids
    recursiveUpdate(clipboardData.specific, function (path) {
      var isTmpFile = (path.substr(-4, 4) === '#tmp');
      if (!isTmpFile && clipboardData.contentId && !path.match(/^https?:\/\//i)) {
        // Comes from existing content

        let prefix;
        if (H5PEditor.contentId) {
          // .. to existing content
          prefix = '../' + clipboardData.contentId + '/';
        }
        else {
          // .. to new content
          prefix = (H5PEditor.contentRelUrl ? H5PEditor.contentRelUrl : '../content/') + clipboardData.contentId + '/';
        }
        return path.substr(0, prefix.length) === prefix ? path : prefix + path;
      }

      return path; // Will automatically be looked for in tmp folder
    });


    if (clipboardData.generic) {
      // Use reference instead of key
      clipboardData.generic = clipboardData.specific[clipboardData.generic];
    }

    return clipboardData;
  };

  /**
   * Update file URLs and reset content IDs.
   * Useful when copying content.
   *
   * @private
   * @param {object} params Reference
   * @param {function} handler Modifies the path to work when pasted
   */
  var recursiveUpdate = function (params, handler) {
    for (var prop in params) {
      if (params.hasOwnProperty(prop) && params[prop] instanceof Object) {
        var obj = params[prop];
        if (obj.path !== undefined && obj.mime !== undefined) {
          obj.path = handler(obj.path);
        }
        else {
          if (obj.library !== undefined && obj.subContentId !== undefined) {
            // Avoid multiple content with same ID
            delete obj.subContentId;
          }
          recursiveUpdate(obj, handler);
        }
      }
    }
  };

  // Init H5P when page is fully loadded
  $(document).ready(function () {

    window.addEventListener('storage', function (event) {
      // Pick up clipboard changes from other tabs
      if (event.key === 'h5pClipboard') {
        // Trigger an event so all 'Paste' buttons may be enabled.
        H5P.externalDispatcher.trigger('datainclipboard', {reset: event.newValue === null});
      }
    });

    var ccVersions = {
      'default': '4.0',
      '4.0': H5P.t('licenseCC40'),
      '3.0': H5P.t('licenseCC30'),
      '2.5': H5P.t('licenseCC25'),
      '2.0': H5P.t('licenseCC20'),
      '1.0': H5P.t('licenseCC10'),
    };

    /**
     * Maps copyright license codes to their human readable counterpart.
     *
     * @type {Object}
     */
    H5P.copyrightLicenses = {
      'U': H5P.t('licenseU'),
      'CC BY': {
        label: H5P.t('licenseCCBY'),
        link: 'http://creativecommons.org/licenses/by/:version',
        versions: ccVersions
      },
      'CC BY-SA': {
        label: H5P.t('licenseCCBYSA'),
        link: 'http://creativecommons.org/licenses/by-sa/:version',
        versions: ccVersions
      },
      'CC BY-ND': {
        label: H5P.t('licenseCCBYND'),
        link: 'http://creativecommons.org/licenses/by-nd/:version',
        versions: ccVersions
      },
      'CC BY-NC': {
        label: H5P.t('licenseCCBYNC'),
        link: 'http://creativecommons.org/licenses/by-nc/:version',
        versions: ccVersions
      },
      'CC BY-NC-SA': {
        label: H5P.t('licenseCCBYNCSA'),
        link: 'http://creativecommons.org/licenses/by-nc-sa/:version',
        versions: ccVersions
      },
      'CC BY-NC-ND': {
        label: H5P.t('licenseCCBYNCND'),
        link: 'http://creativecommons.org/licenses/by-nc-nd/:version',
        versions: ccVersions
      },
      'CC0 1.0': {
        label: H5P.t('licenseCC010'),
        link: 'https://creativecommons.org/publicdomain/zero/1.0/'
      },
      'GNU GPL': {
        label: H5P.t('licenseGPL'),
        link: 'http://www.gnu.org/licenses/gpl-:version-standalone.html',
        linkVersions: {
          'v3': '3.0',
          'v2': '2.0',
          'v1': '1.0'
        },
        versions: {
          'default': 'v3',
          'v3': H5P.t('licenseV3'),
          'v2': H5P.t('licenseV2'),
          'v1': H5P.t('licenseV1')
        }
      },
      'PD': {
        label: H5P.t('licensePD'),
        versions: {
          'CC0 1.0': {
            label: H5P.t('licenseCC010'),
            link: 'https://creativecommons.org/publicdomain/zero/1.0/'
          },
          'CC PDM': {
            label: H5P.t('licensePDM'),
            link: 'https://creativecommons.org/publicdomain/mark/1.0/'
          }
        }
      },
      'ODC PDDL': '<a href="http://opendatacommons.org/licenses/pddl/1.0/" target="_blank">Public Domain Dedication and Licence</a>',
      'CC PDM': {
        label: H5P.t('licensePDM'),
        link: 'https://creativecommons.org/publicdomain/mark/1.0/'
      },
      'C': H5P.t('licenseC'),
    };

    /**
     * Indicates if H5P is embedded on an external page using iframe.
     * @member {boolean} H5P.externalEmbed
     */

    // Relay events to top window. This must be done before H5P.init
    // since events may be fired on initialization.
    if (H5P.isFramed && H5P.externalEmbed === false) {
      H5P.externalDispatcher.on('*', function (event) {
        window.parent.H5P.externalDispatcher.trigger.call(this, event);
      });
    }

    /**
     * Prevent H5P Core from initializing. Must be overriden before document ready.
     * @member {boolean} H5P.preventInit
     */
    if (!H5P.preventInit) {
      // Note that this start script has to be an external resource for it to
      // load in correct order in IE9.
      H5P.init(document.body);
    }

    if (H5PIntegration.saveFreq !== false) {
      // When was the last state stored
      var lastStoredOn = 0;
      // Store the current state of the H5P when leaving the page.
      var storeCurrentState = function () {
        // Make sure at least 250 ms has passed since last save
        var currentTime = new Date().getTime();
        if (currentTime - lastStoredOn > 250) {
          lastStoredOn = currentTime;
          for (var i = 0; i < H5P.instances.length; i++) {
            var instance = H5P.instances[i];
            if (instance.getCurrentState instanceof Function ||
                typeof instance.getCurrentState === 'function') {
              var state = instance.getCurrentState();
              if (state !== undefined) {
                // Async is not used to prevent the request from being cancelled.
                H5P.setUserData(instance.contentId, 'state', state, {deleteOnChange: true, async: false});
              }
            }
          }
        }
      };
      // iPad does not support beforeunload, therefore using unload
      H5P.$window.one('beforeunload unload', function () {
        // Only want to do this once
        H5P.$window.off('pagehide beforeunload unload');
        storeCurrentState();
      });
      // pagehide is used on iPad when tabs are switched
      H5P.$window.on('pagehide', storeCurrentState);
    }
  });

})(H5P.jQuery);
