/**
 * Create and minimally track guest windows at the direction of the renderer
 * (via window.open). Here, "guest" roughly means "child" — it's not necessarily
 * emblematic of its process status; both in-process (same-origin) and
 * out-of-process (cross-origin) are created here. "Embedder" roughly means
 * "parent."
 */
import { parseFeatures } from '@electron/internal/browser/parse-features-string';

import { BrowserWindow } from 'electron/main';
import type { BrowserWindowConstructorOptions, Referrer, WebContents, LoadURLOptions } from 'electron/main';

type PostData = LoadURLOptions['postData'];
export type WindowOpenArgs = {
  url: string;
  frameName: string;
  features: string;
};

/**
 * `openGuestWindow` is called to create and setup event handling for the new
 * window.
 */
export function openGuestWindow({
  embedder,
  guest,
  referrer,
  disposition,
  postData,
  overrideBrowserWindowOptions,
  windowOpenArgs,
  outlivesOpener,
  createWindow,
  inheritedSandboxFlags
}: {
  embedder: WebContents;
  guest?: WebContents;
  referrer: Referrer;
  disposition: string;
  postData?: PostData;
  overrideBrowserWindowOptions?: BrowserWindowConstructorOptions;
  windowOpenArgs: WindowOpenArgs;
  outlivesOpener: boolean;
  createWindow?: Electron.CreateWindowFunction;
  inheritedSandboxFlags?: number;
}): void {
  const { url, frameName, features } = windowOpenArgs;
  const { options: parsedOptions } = parseFeatures(features);
  const browserWindowOptions = {
    show: true,
    width: 800,
    height: 600,
    ...parsedOptions,
    ...overrideBrowserWindowOptions
  };

  // When the opening frame is sandboxed without
  // 'allow-popups-to-escape-sandbox', the new window must inherit the
  // opener's sandbox restrictions. They are applied when the window's
  // WebContents is created; sandbox flags can only be added this way, never
  // cleared.
  if (inheritedSandboxFlags) {
    browserWindowOptions.webPreferences = {
      ...browserWindowOptions.webPreferences,
      openerSandboxFlags: inheritedSandboxFlags
    };
  }

  if (createWindow) {
    const webContents = createWindow({
      webContents: guest,
      ...browserWindowOptions
    });

    if (guest != null) {
      if (webContents !== guest) {
        throw new Error(
          'Invalid webContents. Created window should be connected to webContents passed with options object.'
        );
      }

      handleWindowLifecycleEvents({ embedder, guest, outlivesOpener });
    }

    return;
  }

  const window = new BrowserWindow({
    webContents: guest,
    ...browserWindowOptions
  });

  if (!guest) {
    // When we open a new window from a link (via OpenURLFromTab),
    // the browser process is responsible for initiating navigation
    // in the new window.
    window.loadURL(url, {
      httpReferrer: referrer,
      ...(postData && {
        postData,
        extraHeaders: formatPostDataHeaders(postData as Electron.UploadRawData[])
      })
    });
  }

  handleWindowLifecycleEvents({ embedder, guest: window.webContents, outlivesOpener });

  embedder.emit('did-create-window', window, {
    url,
    frameName,
    options: browserWindowOptions,
    disposition,
    referrer,
    postData
  });
}

/**
 * Manage the relationship between embedder window and guest window. When the
 * guest is destroyed, notify the embedder. When the embedder is destroyed, so
 * too is the guest destroyed; this is Electron convention and isn't based in
 * browser behavior.
 */
const handleWindowLifecycleEvents = function ({
  embedder,
  guest,
  outlivesOpener
}: {
  embedder: WebContents;
  guest: WebContents;
  outlivesOpener: boolean;
}) {
  const closedByEmbedder = function () {
    guest.removeListener('destroyed', closedByUser);
    guest.destroy();
  };

  const closedByUser = function () {
    // Embedder might have been closed
    if (!embedder.isDestroyed() && !outlivesOpener) {
      embedder.removeListener('current-render-view-deleted' as any, closedByEmbedder);
    }
  };
  if (!outlivesOpener) {
    embedder.once('current-render-view-deleted' as any, closedByEmbedder);
  }
  guest.once('destroyed', closedByUser);
};

// Security options that child windows will always inherit from parent windows
const securityWebPreferences: { [key: string]: boolean } = {
  contextIsolation: true,
  javascript: false,
  nodeIntegration: false,
  sandbox: true,
  webviewTag: false,
  nodeIntegrationInSubFrames: false,
  enableWebSQL: false
};

export function makeWebPreferences({
  embedder,
  secureOverrideWebPreferences = {},
  insecureParsedWebPreferences: parsedWebPreferences = {}
}: {
  embedder: WebContents;
  insecureParsedWebPreferences?: ReturnType<typeof parseFeatures>['webPreferences'];
  // Note that override preferences are considered elevated, and should only be
  // sourced from the main process, as they override security defaults. If you
  // have unvetted prefs, use parsedWebPreferences.
  secureOverrideWebPreferences?: BrowserWindowConstructorOptions['webPreferences'];
}) {
  const parentWebPreferences = embedder.getLastWebPreferences()!;
  const securityWebPreferencesFromParent = Object.keys(securityWebPreferences).reduce((map, key) => {
    if (securityWebPreferences[key] === parentWebPreferences[key as keyof Electron.WebPreferences]) {
      (map as any)[key] = parentWebPreferences[key as keyof Electron.WebPreferences];
    }
    return map;
  }, {} as Electron.WebPreferences);

  return {
    ...parsedWebPreferences,
    // Note that order is key here, we want to disallow the renderer's
    // ability to change important security options but allow main (via
    // setWindowOpenHandler) to change them.
    ...securityWebPreferencesFromParent,
    ...secureOverrideWebPreferences
  };
}

function formatPostDataHeaders(postData: PostData) {
  if (!postData) return;

  const { contentType, boundary } = parseContentTypeFormat(postData);
  if (boundary != null) {
    return `content-type: ${contentType}; boundary=${boundary}`;
  }

  return `content-type: ${contentType}`;
}

const MULTIPART_CONTENT_TYPE = 'multipart/form-data';
const URL_ENCODED_CONTENT_TYPE = 'application/x-www-form-urlencoded';

// Figure out appropriate headers for post data.
export const parseContentTypeFormat = function (postData: Exclude<PostData, undefined>) {
  if (postData.length) {
    if (postData[0].type === 'rawData') {
      // For multipart forms, the first element will start with the boundary
      // notice, which looks something like `------WebKitFormBoundary12345678`
      // Note, this regex would fail when submitting a urlencoded form with an
      // input attribute of name="--theKey", but, uhh, don't do that?
      const postDataFront = postData[0].bytes.toString();
      const boundary = /^--.*[^-\r\n]/.exec(postDataFront);
      if (boundary) {
        return {
          boundary: boundary[0].substr(2),
          contentType: MULTIPART_CONTENT_TYPE
        };
      }
    }
  }
  // Either the form submission didn't contain any inputs (the postData array
  // was empty), or we couldn't find the boundary and thus we can assume this is
  // a key=value style form.
  return {
    contentType: URL_ENCODED_CONTENT_TYPE
  };
};
