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

import type {TimeoutSettings} from '../common/TimeoutSettings.js';
import type {EvaluateFunc, HandleFor} from '../common/types.js';
import {TaskManager, WaitTask} from '../common/WaitTask.js';
import {disposeSymbol} from '../util/disposable.js';

import type {ElementHandle} from './ElementHandle.js';
import type {Environment} from './Environment.js';
import type {Extension} from './Extension.js';
import type {JSHandle} from './JSHandle.js';

/**
 * @public
 */
export abstract class Realm {
  /** @internal */
  protected readonly timeoutSettings: TimeoutSettings;
  /** @internal */
  readonly taskManager = new TaskManager();
  /** @internal */
  constructor(timeoutSettings: TimeoutSettings) {
    this.timeoutSettings = timeoutSettings;
  }
  /** @internal */
  abstract get environment(): Environment;

  /**
   * Returns the origin that created the Realm.
   * For example, if the realm was created by an extension content script,
   * this will return the origin of the extension
   * (e.g., `chrome-extension://<extension-id>`).
   *
   * @experimental
   * @example
   * `chrome-extension://<chrome-extension-id>`
   */
  abstract get origin(): string | undefined;
  /**
   * Returns the {@link Extension} that created this realm, if applicable.
   * This is typically populated when the realm was created by an extension
   * content script injected into a page.
   *
   * @returns A promise that resolves to the {@link Extension}
   * or `null` if not created by an extension.
   * @experimental
   */
  abstract extension(): Promise<Extension | null>;

  /** @internal */
  abstract adoptHandle<T extends JSHandle<Node>>(handle: T): Promise<T>;

  /** @internal */
  abstract transferHandle<T extends JSHandle<Node>>(handle: T): Promise<T>;

  /**
   * Evaluates a function in the realm's context and returns a
   * {@link JSHandle} to the result.
   *
   * If the function passed to `realm.evaluateHandle` returns a Promise,
   * the method will wait for the promise to resolve and return its value.
   *
   * {@link JSHandle} instances can be passed as arguments to the function.
   *
   * @example
   *
   * ```ts
   * const aHandle = await realm.evaluateHandle(() => document.body);
   * const resultHandle = await realm.evaluateHandle(
   *   body => body.innerHTML,
   *   aHandle,
   * );
   * ```
   *
   * @param pageFunction - A function to be evaluated in the realm.
   * @param args - Arguments to be passed to the `pageFunction`.
   * @returns A promise that resolves to a {@link JSHandle} containing
   * the result.
   * @public
   */
  abstract evaluateHandle<
    Params extends unknown[],
    Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
  >(
    pageFunction: Func | string,
    ...args: Params
  ): Promise<HandleFor<Awaited<ReturnType<Func>>>>;

  /**
   * Evaluates a function in the realm's context and returns the
   * resulting value.
   *
   * If the function passed to `realm.evaluate` returns a Promise,
   * the method will wait for the promise to resolve and return its value.
   *
   * {@link JSHandle} instances can be passed as arguments to the function.
   *
   * @example
   *
   * ```ts
   * const result = await realm.evaluate(() => {
   *   return Promise.resolve(8 * 7);
   * });
   * console.log(result); // prints "56"
   * ```
   *
   * @param pageFunction - A function to be evaluated in the realm.
   * @param args - Arguments to be passed to the `pageFunction`.
   * @returns A promise that resolves to the return value of the function.
   * @public
   */
  abstract evaluate<
    Params extends unknown[],
    Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
  >(
    pageFunction: Func | string,
    ...args: Params
  ): Promise<Awaited<ReturnType<Func>>>;

  /**
   * Waits for a function to return a truthy value when evaluated in
   * the realm's context.
   *
   * Arguments can be passed from Node.js to `pageFunction`.
   *
   * @example
   *
   * ```ts
   * const selector = '.foo';
   * await realm.waitForFunction(
   *   selector => !!document.querySelector(selector),
   *   {},
   *   selector,
   * );
   * ```
   *
   * @param pageFunction - A function to evaluate in the realm.
   * @param options - Options for polling and timeouts.
   * @param args - Arguments to pass to the function.
   * @returns A promise that resolves when the function returns a truthy
   * value.
   * @public
   */
  async waitForFunction<
    Params extends unknown[],
    Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
  >(
    pageFunction: Func | string,
    options: {
      polling?: 'raf' | 'mutation' | number;
      timeout?: number;
      root?: ElementHandle<Node>;
      signal?: AbortSignal;
    } = {},
    ...args: Params
  ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
    const {
      polling = 'raf',
      timeout = this.timeoutSettings.timeout(),
      root,
      signal,
    } = options;
    if (typeof polling === 'number' && polling < 0) {
      throw new Error('Cannot poll with non-positive interval');
    }
    const waitTask = new WaitTask(
      this,
      {
        polling,
        root,
        timeout,
        signal,
      },
      pageFunction as unknown as
        | ((...args: unknown[]) => Promise<Awaited<ReturnType<Func>>>)
        | string,
      ...args,
    );
    return await waitTask.result;
  }

  /** @internal */
  abstract adoptBackendNode(backendNodeId?: number): Promise<JSHandle<Node>>;

  /** @internal */
  get disposed(): boolean {
    return this.#disposed;
  }

  #disposed = false;
  /** @internal */
  dispose(): void {
    this.#disposed = true;
    this.taskManager.terminateAll(
      new Error('waitForFunction failed: frame got detached.'),
    );
  }

  /** @internal */
  [disposeSymbol](): void {
    this.dispose();
  }
}
