import fs from "fs";

/**
 * Helper class for processing and using manifest for packing the extension.
 */
class ManifestProcessor {
  /**
   * Current state of the manifest object.
   * @type {Manifest}
   */
  #manifestObject;

  /**
   * @param {Manifest} parsedManifest Original manifest contents parsed as JSON object.
   */
  constructor(parsedManifest) {
    this.#manifestObject = parsedManifest;
  }

  /**
   * Collect all the content scripts & stylesheets for single build action.
   *
   * @returns {Set<string>}
   */
  collectContentScripts() {
    const contentScripts = this.#manifestObject.content_scripts;

    if (!contentScripts) {
      console.info('No content scripts to collect.');
      return new Set();
    }

    const entryPoints = new Set();

    for (let entry of contentScripts) {
      if (entry.js) {
        for (let jsPath of entry.js) {
          entryPoints.add(jsPath);
        }
      }

      if (entry.css) {
        for (let cssPath of entry.css) {
          entryPoints.add(cssPath);
        }
      }
    }

    return entryPoints;
  }

  /**
   * Map over every content script defined in the manifest. If no content scripts defined, no calls will be made to the
   * callback.
   *
   * @param {function(ContentScriptsEntry): Promise<ContentScriptsEntry>} mapCallback Processing function to call on
   * every entry. Entries should be modified and returned. Function should be asynchronous.
   *
   * @return {Promise<void>}
   */
  async mapContentScripts(mapCallback) {
    const contentScripts = this.#manifestObject.content_scripts;

    if (!contentScripts) {
      console.info('No content scripts to map over.');
      return;
    }

    for (let entryIndex = 0; entryIndex < contentScripts.length; entryIndex++) {
      contentScripts[entryIndex] = await mapCallback(contentScripts[entryIndex]);
    }
  }

  /**
   * Pass the version of the plugin from following package.json file.
   *
   * @param {string} packageFilePath Path to the JSON file to parse and extract the version from. If version is not
   * found, original version will be kept.
   */
  passVersionFromPackage(packageFilePath) {
    /** @type {PackageObject} */
    const packageObject = JSON.parse(fs.readFileSync(packageFilePath, 'utf8'));

    if (packageObject.version) {
      this.#manifestObject.version = packageObject.version;
    }
  }

  /**
   * Find all patterns in content scripts and host permissions and replace the hostname to the different one.
   *
   * @param {string|string[]} singleOrMultipleHostnames One or multiple hostnames to replace the original hostname with.
   */
  replaceHostTo(singleOrMultipleHostnames) {
    if (typeof singleOrMultipleHostnames === 'string') {
      singleOrMultipleHostnames = [singleOrMultipleHostnames];
    }

    this.#manifestObject.host_permissions = singleOrMultipleHostnames.map(hostname => `*://*.${hostname}/`);

    this.#manifestObject.content_scripts?.forEach(entry => {
      entry.matches = entry.matches.reduce((resultMatches, originalMatchPattern) => {
        for (const updatedHostname of singleOrMultipleHostnames) {
          resultMatches.push(
            originalMatchPattern.replace(
              /\*:\/\/\*\.[a-z]+\.[a-z]+\//,
              `*://*.${updatedHostname}/`
            ),
          );
        }

        return resultMatches;
      }, []);
    })
  }

  /**
   * Set different identifier for Gecko-based browsers (Firefox).
   *
   * @param {string} id ID of the extension to use.
   */
  setGeckoIdentifier(id) {
    this.#manifestObject.browser_specific_settings.gecko.id = id;
  }

  /**
   * Set the different extension name.
   *
   * @param {string} booruName
   */
  replaceBooruNameWith(booruName) {
    this.#manifestObject.name = this.#manifestObject.name.replaceAll('Furbooru', booruName);
    this.#manifestObject.description = this.#manifestObject.description.replaceAll('Furbooru', booruName);
  }

  /**
   * Save the current state of the manifest into the selected file.
   *
   * @param {string} manifestFilePath File to write the resulting manifest to. Should be called after all the
   * modifications.
   */
  saveTo(manifestFilePath) {
    fs.writeFileSync(
      manifestFilePath,
      JSON.stringify(this.#manifestObject, null, 2),
      {
        encoding: 'utf8'
      }
    );
  }
}

/**
 * Load the manifest and create a processor object.
 *
 * @param {string} filePath Path to the original manifest file.
 *
 * @return {ManifestProcessor} Object for manipulating manifest file.
 */
export function loadManifest(filePath) {
  const manifest = JSON.parse(fs.readFileSync(filePath, 'utf-8'));

  return new ManifestProcessor(manifest);
}

/**
 * @typedef {Object} Manifest
 * @property {string} name
 * @property {string} description
 * @property {string} version
 * @property {BrowserSpecificSettings} browser_specific_settings
 * @property {string[]} host_permissions
 * @property {ContentScriptsEntry[]|undefined} content_scripts
 */

/**
 * @typedef {Object} BrowserSpecificSettings
 * @property {GeckoSettings} gecko
 */

/**
 * @typedef {Object} GeckoSettings
 * @property {string} id
 */

/**
 * @typedef {Object} ContentScriptsEntry
 * @property {string[]} matches
 * @property {string[]|undefined} js
 * @property {string[]|undefined} css
 */

/**
 * @typedef {Object} PackageObject
 * @property {string|undefined} version
 */
