/**
 * @license
 * Copyright 2023 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @fileoverview Helper functions for loading Blockly in tests.
 *
 * Used by playgrounds and test harnesses, both directly and via the
 * shims generated by the buildShims function in
 * scripts/gulpfiles/build_tasks.js.
 *
 */

if (typeof window !== 'object') {
  // Not running in a browser.  Maybe we wish to support this?
  // blockly_uncompressed formerly supported node.js, though it
  // appears that the code had not been working for some time (at
  // least since PR #5718 back in December 2021.  For now just throw
  // an error.
  throw new Error('Bootstrapping without window is not supported.');
}

/**
 * URL of the blockly repository.  This is needed for a few reasons:
 *
 * - We need an absolute path instead of relative path because the
 *   advanced_playground and the regular playground are in
 *   different folders.
 * - We need to get the root directory for blockly because it is
 *   different for github.io, appspot and local.
 *
 * The formulation given here will work so long as top-level page is loaded from
 * somewhere in tests/.
 */
export const ROOT = window.location.href.replace(/\/tests\/.*$/, '/');

/**
 * Decide whether to use compressed mode or not.
 *
 * Honours a "?compressed=" query parameter if present; otherwise uses
 * uncompressed for when loading from local machine and compressed
 * otherwise.  See issue #5557 for additional background.
 *
 * @return {boolean} True if should load in compressed mode.
 */
function compressed() {
  const param = location.search.match(/compressed=([^&]+)/)?.[1];
  if (param) {
    if (['y', 'yes', 'true', '1'].includes(param)) return true;
    if (['n', 'no', 'false', '0'].includes(param)) return false;
    console.error(`Unrecognised compressed parameter "${param}"`);
  }

  const LOCALHOSTS = ['localhost', '127.0.0.1', '[::1]'];
  const isLocalhost = LOCALHOSTS.includes(location.hostname);
  const isFileUrl = location.origin === 'file://';
  return !(isLocalhost || isFileUrl);
}

/** @type {boolean} Load in compressed mode. */
export const COMPRESSED = compressed();

/**
 * Load a chunk, either by importing its ESM entrypoint or loading the
 * compressed chunk script.
 *
 * When loading in uncompressed mode, if scriptExports is a simple
 * variable name (e.g. 'Blockly') then globalThis[scriptExports] will
 * be set to the chunk's Module object.  This attempts to provide
 * backward compatibility with loading the compressed chunk as a
 * script, where this is done by the compressed chunk's UMD wrapper.
 * The compatibility is not complete, however: since Module objects
 * are immutable, it is not possible to set (e.g.)
 * Blockly.libraryBlocks.
 *
 * The intention is to allow the chunk to be accessed either via
 * the returned Module / exports object (preferred) or via the global
 * scope (needed by dev-tools) regardless of whether it was loaded
 * compressed or uncompressed.
 *
 * Paths should be relative to the repository root.
 *
 * @param {string} modulePath Path to the chunk's ESM entry point.
 * @param {string} scriptPath Path to the chunk's _compressed.js file.
 * @param {string} scriptExports The global variable name (or
 *     dotted-identifier path relative from a global variable) in
 *     which the compressed chunk's UMD wrapper will save the module's
 *     exports object.  Should be the same as the correspodning
 *     chunks[].scriptExport property in
 *     scripts/gulpfiles/build_tasks.js.
 * @return {Module} The module's Module or exports object.
 */
export async function loadChunk(modulePath, scriptPath, scriptExports) {
  if (COMPRESSED) {
    await loadScript(`${ROOT}${scriptPath}`);
    return get(scriptExports);
  } else {
    const exports = await import(`../../${modulePath}`);
    if (!scriptExports.includes('.')) globalThis[scriptExports] = exports;
    return exports;
  }
}

/**
 * Load a given URL using a <script> tag.
 *
 * Unlike document.write('<script src=...>'), this is safe to use even
 * after page loading is complete.
 *
 * @param {string} src The src attribute for the <script> tag.
 * @return {Promise} A promise that resolves (or rejects) once the
 *     script is loaded.
 */
export function loadScript(src) {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = src;
    script.onload = resolve;
    script.onerror = reject;
    document.getElementsByTagName('head')[0].appendChild(script);
  });
}

/**
 * Get the value referred to by a dotted-identifier path
 * (e.g. foo.bar.baz).  Throws TypeError if path is is not valid,
 * i.e., if any component but the last does not exist).
 *
 * @param {string} path The path referring to the desired value.
 * @return {string|null} The value referred to.
 */
function get(path) {
  let obj = globalThis;
  for (const part of path.split('.')) {
    obj = obj[part];
  }
  return obj;
}