/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";

/**
 * Build: <objdir>/dist/@types/lib.gecko.xpcom.d.ts,
 *
 * from:  <objdir>/config/makefiles/xpidl/*.d.json type definition files,
 *        generated by a previous build step.
 */

const fs = require("fs");
const URL = "https://searchfox.org/mozilla-central/source/";

const HEADER = `/**
 * NOTE: Do not modify this file by hand.
 * Content was generated from source XPCOM .idl files.
 * If you're updating some of the sources, see README for instructions.
 */
`;

const FOOTER = `
// XPCOM internal utility types.

/** XPCOM inout param is passed in as a js object with a value property. */
type InOutParam<T> = { value: T };

/** XPCOM out param is written to the passed in object's value property. */
type OutParam<T> = { value?: T };

/** Enable interfaces to inherit from enums: pick variants as optional. */
type Enums<enums> = Partial<Pick<enums, keyof enums>>;

/** Callable accepts either form of a [function] interface. */
type Callable<iface> = iface | Extract<iface[keyof iface], Function>

export {};
`;

const PLATFORM_SPECIFIC = new Set([
  "nsIAboutThirdParty.idl",
  "nsIAboutWindowsMessages.idl",
  "nsIAccessibleMacInterface.idl",
  "nsIApplicationChooser.idl",
  "nsIDefaultAgent.idl",
  "nsIGeolocationUIUtilsWin.idl",
  "nsIGtkTaskbarProgress.idl",
  "nsIGNOMEShellService.idl",
  "nsIJumpListBuilder.idl",
  "nsIKeychainMigrationUtils.idl",
  "nsILocalFileMac.idl",
  "nsIMacDockSupport.idl",
  "nsIMacFinderProgress.idl",
  "nsIMacPreferencesReader.idl",
  "nsIMacSharingService.idl",
  "nsIMacShellService.idl",
  "nsIMacUserActivityUpdater.idl",
  "nsIMacWebAppUtils.idl",
  "nsINamedPipeService.idl",
  "nsIOpenTabsProvider.idl",
  "nsIPrintSettingsWin.idl",
  "nsIStandaloneNativeMenu.idl",
  "nsITaskbarOverlayIconController.idl",
  "nsITaskbarPreview.idl",
  "nsITaskbarPreviewButton.idl",
  "nsITaskbarPreviewController.idl",
  "nsITaskbarProgress.idl",
  "nsITaskbarTabPreview.idl",
  "nsITaskbarWindowPreview.idl",
  "nsITouchBarHelper.idl",
  "nsITouchBarInput.idl",
  "nsITouchBarUpdater.idl",
  "nsIWinAppHelper.idl",
  "nsIWinTaskbar.idl",
  "nsIWinTaskSchedulerService.idl",
  "nsIWindowsAlertsService.idl",
  "nsIWindowsMutex.idl",
  "nsIWindowsPackageManager.idl",
  "nsIWindowsRegKey.idl",
  "nsIWindowsShellService.idl",
  "nsIWindowsUIUtils.idl",
]);

// Emit a typescript interface, along with any related enums.
function ts_interface(iface) {
  let lines = [];
  let base = iface.base;
  iface.class = iface.id;

  // Make QueryInterface optional, enable plain objects to pass as nsISupports.
  let partial = iface.id === "nsISupports" ? "?" : "";

  let enums = iface.enums.map(e => `typeof ${iface.id}_${e.id}`).join(" & ");
  if (enums) {
    base += `, Enums<${enums}>`;
    iface.class += `, ${enums}`;

    // Close the global scope, avoid polluting it with the enum values.
    lines.push("}  // global\n");

    for (let e of iface.enums) {
      lines.push(`declare enum ${iface.id}_${e.id} {`);
      for (let v of e.variants) {
        lines.push(`  ${v.name} = ${v.value},`);
      }
      lines.push("}\n");
    }

    lines.push("declare global {\n");
    lines.push(`namespace ${iface.id} {`);

    for (let e of iface.enums) {
      lines.push(`  type ${e.id} = ${iface.id}_${e.id};`);
    }
    lines.push("}\n");
  }

  // Handle [function] interfaces.
  if (iface.callable) {
    lines.push(`type ${iface.id} = Callable<{`);
  } else {
    lines.push(`interface ${iface.id} ${base ? `extends ${base} ` : ""}{`);
  }

  for (let c of iface.consts) {
    lines.push(`  readonly ${c.name}?: ${c.value};`);
  }

  if (iface.consts.length && iface.members.length) {
    // For style points.
    lines.push("");
  }

  for (let m of iface.members) {
    if (!m.args) {
      lines.push(`  ${m.readonly ? "readonly " : ""}${m.name}: ${m.type};`);
    } else {
      let args = [];
      for (let arg of m.args) {
        // If this is the generic parameter, adjust its type.
        let type = arg.name === m.iid_is ? "T" : arg.type;
        args.push(`${arg.name}${arg.optional ? "?" : ""}: ${type}`);
      }
      let type = `(${args.join(", ")}): ${m.type}`;
      // Adjust signature if this is a generic method.
      let signature = m.iid_is ? `<T extends nsIID>${type}<T>` : type;
      lines.push(`  ${m.name}${partial}${signature};`);
    }
  }

  lines.push(iface.callable ? "}>\n" : "}\n");
  return lines;
}

// Link all generated .d.json files into a self-contained ts typelib.
function ts_link(dir, files, filter) {
  let lines = [HEADER, "declare global {\n"];
  let typedefs = {};
  let iids = [];

  for (let djson of files.sort()) {
    let modules = JSON.parse(fs.readFileSync(`${dir}/${djson}`, "utf8"));

    for (let mod of modules.filter(m => filter(m.path.split("/").at(-1)))) {
      lines.push(`// ${URL}${mod.path}\n`);
      Object.assign(typedefs, Object.fromEntries(mod.typedefs ?? []));

      for (let iface of mod.interfaces ?? []) {
        if (iface.id !== "nsIXPCComponents_Interfaces") {
          lines = lines.concat(ts_interface(iface));
          iids.push(`  ${iface.id}: nsJSIID<${iface.class}>;`);
        }
      }
    }
  }

  lines.push("interface nsIXPCComponents_Interfaces {");
  lines = lines.concat(iids);
  lines.push("}\n");

  lines.push("}  // global\n");

  lines.push("// Typedefs from xpidl.");
  for (let [id, type] of Object.entries(typedefs).sort()) {
    lines.push(`type ${id} = ${type};`);
  }
  lines.push(FOOTER);

  return lines.join("\n");
}

// For testing.
module.exports = { ts_link };

function main(lib_xpcom, djson_dir, ...djson_files) {
  let dts = ts_link(djson_dir, djson_files, m => !PLATFORM_SPECIFIC.has(m));
  console.log(`[INFO] ${lib_xpcom} (${dts.length.toLocaleString()} bytes)`);
  fs.writeFileSync(lib_xpcom, dts);

  let lib_plat = lib_xpcom.replace("xpcom", process.platform);
  let spec = ts_link(djson_dir, djson_files, m => PLATFORM_SPECIFIC.has(m));
  console.log(`[INFO] ${lib_plat} (${spec.length.toLocaleString()} bytes)`);
  fs.writeFileSync(lib_plat, spec);
}

if (require.main === module) {
  main(...process.argv.slice(2));
}
