/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { deserializeURLMatch, urlMatches } from '@isomorphic/urlMatch';
import { Page, Worker } from '../page';
import { Dispatcher } from './dispatcher';
import { parseError, serializeError } from '../errors';
import { ArtifactDispatcher } from './artifactDispatcher';
import { ElementHandleDispatcher } from './elementHandlerDispatcher';
import { FrameDispatcher } from './frameDispatcher';
import { JSHandleDispatcher, parseArgument, serializeResult } from './jsHandleDispatcher';
import { RequestDispatcher } from './networkDispatchers';
import { ResponseDispatcher } from './networkDispatchers';
import { RouteDispatcher, WebSocketDispatcher } from './networkDispatchers';
import { WebSocketRouteDispatcher } from './webSocketRouteDispatcher';
import { DisposableDispatcher } from './disposableDispatcher';
import { SdkObject } from '../instrumentation';
import { Recorder } from '../recorder';
import { disposeAll } from '../disposable';
import { VideoRecorder } from '../videoRecorder';
import { nullProgress } from '../progress';

import type { Artifact } from '../artifact';
import type { BrowserContext } from '../browserContext';
import type { CRCoverage } from '../chromium/crCoverage';
import type { Download } from '../download';
import type { FileChooser } from '../fileChooser';
import type { BrowserContextDispatcher } from './browserContextDispatcher';
import type { Frame } from '../frames';
import type { RouteHandler } from '../network';
import type { InitScript } from '../page';
import type { Disposable } from '../disposable';
import type { BrowserTypeDispatcher } from './browserTypeDispatcher';
import type { ConsoleMessage } from '../console';
import type * as channels from '@protocol/channels';
import type { Progress } from '@protocol/progress';
import type { URLMatch } from '@isomorphic/urlMatch';
import type { ScreencastFrame } from '../types';
import type { ScreencastClient } from '../screencast';

export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, BrowserContextDispatcher> implements channels.PageChannel {
  _type_EventTarget = true;
  _type_Page = true;
  private _page: Page;
  _subscriptions = new Set<channels.PageUpdateSubscriptionParams['event']>();
  _webSocketInterceptionPatterns: channels.PageSetWebSocketInterceptionPatternsParams['patterns'] = [];
  private _disposables: Disposable[] = [];
  private _requestInterceptor: RouteHandler;
  private _interceptionUrlMatchers: URLMatch[] = [];
  private _routeWebSocketInitScript: InitScript | undefined;
  private _locatorHandlers = new Set<number>();
  private _jsCoverageActive = false;
  private _cssCoverageActive = false;
  private _screencastClient: ScreencastClient | undefined;
  private _videoRecorder: VideoRecorder | undefined;

  static from(parentScope: BrowserContextDispatcher, page: Page): PageDispatcher {
    return PageDispatcher.fromNullable(parentScope, page)!;
  }

  static fromNullable(parentScope: BrowserContextDispatcher, page: Page | null | undefined): PageDispatcher | undefined {
    if (!page)
      return undefined;
    const result = parentScope.connection.existingDispatcher<PageDispatcher>(page);
    return result || new PageDispatcher(parentScope, page);
  }

  private constructor(parentScope: BrowserContextDispatcher, page: Page) {
    // TODO: theoretically, there could be more than one frame already.
    // If we split pageCreated and pageReady, there should be no main frame during pageCreated.

    // We will reparent it to the page below using adopt.
    const mainFrame = FrameDispatcher.from(parentScope, page.mainFrame());

    super(parentScope, page, 'Page', {
      mainFrame,
      viewportSize: page.emulatedSize()?.viewport,
      isClosed: page.isClosed(),
      opener: PageDispatcher.fromNullable(parentScope, page.opener()),
      video: page.video ? createVideoDispatcher(parentScope, page.video) : undefined
    });

    this.adopt(mainFrame);

    this._page = page;
    this._requestInterceptor = (route, request) => {
      const matchesSome = this._interceptionUrlMatchers.some(urlMatch => urlMatches(this._page.browserContext._options.baseURL, request.url(), urlMatch));
      if (!matchesSome) {
        route.continue({ isFallback: true }).catch(() => {});
        return;
      }
      this._dispatchEvent('route', { route: new RouteDispatcher(RequestDispatcher.from(this.parentScope(), request), route) });
    };

    this.addObjectListener(Page.Events.Close, () => {
      this._dispatchEvent('close');
      this._dispose();
    });
    this.addObjectListener(Page.Events.Crash, () => this._dispatchEvent('crash'));
    this.addObjectListener(Page.Events.Download, (download: Download) => {
      // Artifact can outlive the page, so bind to the context scope.
      this._dispatchEvent('download', { url: download.url, suggestedFilename: download.suggestedFilename(), artifact: ArtifactDispatcher.from(parentScope, download.artifact) });
    });
    this.addObjectListener(Page.Events.EmulatedSizeChanged, () => this._dispatchEvent('viewportSizeChanged', { viewportSize: page.emulatedSize()?.viewport }));
    this.addObjectListener(Page.Events.FileChooser, (fileChooser: FileChooser) => this._dispatchEvent('fileChooser', {
      element: ElementHandleDispatcher.from(mainFrame, fileChooser.element()),
      isMultiple: fileChooser.isMultiple()
    }));
    this.addObjectListener(Page.Events.FrameAttached, frame => this._onFrameAttached(frame));
    this.addObjectListener(Page.Events.FrameDetached, frame => this._onFrameDetached(frame));
    this.addObjectListener(Page.Events.LocatorHandlerTriggered, (uid: number) => this._dispatchEvent('locatorHandlerTriggered', { uid }));
    this.addObjectListener(Page.Events.WebSocket, webSocket => this._dispatchEvent('webSocket', { webSocket: new WebSocketDispatcher(this, webSocket) }));
    this.addObjectListener(Page.Events.Worker, worker => this._dispatchEvent('worker', { worker: new WorkerDispatcher(this, worker) }));
    // Ensure client knows about all frames.
    const frames = page.frameManager.frames();
    for (let i = 1; i < frames.length; i++)
      this._onFrameAttached(frames[i]);
  }

  page(): Page {
    return this._page;
  }

  async exposeBinding(params: channels.PageExposeBindingParams, progress: Progress): Promise<channels.PageExposeBindingResult> {
    const binding = await this._page.exposeBinding(progress, params.name, (source, ...args) => {
      // When reusing the context, we might have some bindings called late enough,
      // after context and page dispatchers have been disposed.
      if (this._disposed)
        return;
      const binding = new BindingCallDispatcher(this, params.name, source, args);
      this._dispatchEvent('bindingCall', { binding });
      return binding.promise();
    });
    this._disposables.push(binding);
    return { disposable: new DisposableDispatcher(this, binding) };
  }

  async setExtraHTTPHeaders(params: channels.PageSetExtraHTTPHeadersParams, progress: Progress): Promise<void> {
    await this._page.setExtraHTTPHeaders(progress, params.headers);
  }

  async reload(params: channels.PageReloadParams, progress: Progress): Promise<channels.PageReloadResult> {
    return { response: ResponseDispatcher.fromNullable(this.parentScope(), await this._page.reload(progress, params)) };
  }

  async goBack(params: channels.PageGoBackParams, progress: Progress): Promise<channels.PageGoBackResult> {
    return { response: ResponseDispatcher.fromNullable(this.parentScope(), await this._page.goBack(progress, params)) };
  }

  async goForward(params: channels.PageGoForwardParams, progress: Progress): Promise<channels.PageGoForwardResult> {
    return { response: ResponseDispatcher.fromNullable(this.parentScope(), await this._page.goForward(progress, params)) };
  }

  async requestGC(params: channels.PageRequestGCParams, progress: Progress): Promise<channels.PageRequestGCResult> {
    await this._page.requestGC(progress);
  }

  async registerLocatorHandler(params: channels.PageRegisterLocatorHandlerParams, progress: Progress): Promise<channels.PageRegisterLocatorHandlerResult> {
    const uid = this._page.registerLocatorHandler(params.selector, params.noWaitAfter);
    this._locatorHandlers.add(uid);
    return { uid };
  }

  async resolveLocatorHandlerNoReply(params: channels.PageResolveLocatorHandlerNoReplyParams, progress: Progress): Promise<void> {
    this._page.resolveLocatorHandler(params.uid, params.remove);
  }

  async unregisterLocatorHandler(params: channels.PageUnregisterLocatorHandlerParams, progress: Progress): Promise<void> {
    this._page.unregisterLocatorHandler(params.uid);
    this._locatorHandlers.delete(params.uid);
  }

  async emulateMedia(params: channels.PageEmulateMediaParams, progress: Progress): Promise<void> {
    await this._page.emulateMedia(progress, {
      media: params.media,
      colorScheme: params.colorScheme,
      reducedMotion: params.reducedMotion,
      forcedColors: params.forcedColors,
      contrast: params.contrast,
    });
  }

  async setViewportSize(params: channels.PageSetViewportSizeParams, progress: Progress): Promise<void> {
    await this._page.setViewportSize(progress, params.viewportSize);
  }

  async addInitScript(params: channels.PageAddInitScriptParams, progress: Progress): Promise<channels.PageAddInitScriptResult> {
    const initScript = await this._page.addInitScript(progress, params.source);
    this._disposables.push(initScript);
    return { disposable: new DisposableDispatcher(this, initScript) };
  }

  async setNetworkInterceptionPatterns(params: channels.PageSetNetworkInterceptionPatternsParams, progress: Progress): Promise<void> {
    const hadMatchers = this._interceptionUrlMatchers.length > 0;
    if (!params.patterns.length) {
      // Note: it is important to remove the interceptor when there are no patterns,
      // because that disables the slow-path interception in the browser itself.
      if (hadMatchers)
        await progress.race(this._page.removeRequestInterceptor(this._requestInterceptor));
      this._interceptionUrlMatchers = [];
    } else {
      this._interceptionUrlMatchers = params.patterns.map(deserializeURLMatch);
      if (!hadMatchers)
        await this._page.addRequestInterceptor(progress, this._requestInterceptor);
    }
  }

  async setWebSocketInterceptionPatterns(params: channels.PageSetWebSocketInterceptionPatternsParams, progress: Progress): Promise<void> {
    this._webSocketInterceptionPatterns = params.patterns;
    if (params.patterns.length && !this._routeWebSocketInitScript)
      this._routeWebSocketInitScript = await WebSocketRouteDispatcher.install(progress, this.connection, this._page);
  }

  async expectScreenshot(params: channels.PageExpectScreenshotParams, progress: Progress): Promise<channels.PageExpectScreenshotResult> {
    const mask: { frame: Frame, selector: string }[] = (params.mask || []).map(({ frame, selector }) => ({
      frame: (frame as FrameDispatcher)._object,
      selector,
    }));
    const locator: { frame: Frame, selector: string } | undefined = params.locator ? {
      frame: (params.locator.frame as FrameDispatcher)._object,
      selector: params.locator.selector,
    } : undefined;
    return await this._page.expectScreenshot(progress, {
      ...params,
      locator,
      mask,
    });
  }

  async screenshot(params: channels.PageScreenshotParams, progress: Progress): Promise<channels.PageScreenshotResult> {
    const mask: { frame: Frame, selector: string }[] = (params.mask || []).map(({ frame, selector }) => ({
      frame: (frame as FrameDispatcher)._object,
      selector,
    }));
    return { binary: await this._page.screenshot(progress, { ...params, mask }) };
  }

  async close(params: channels.PageCloseParams, progress: Progress): Promise<void> {
    if (!params.runBeforeUnload)
      progress.metadata.potentiallyClosesScope = true;
    await this._page.close(progress, params);
  }

  async updateSubscription(params: channels.PageUpdateSubscriptionParams, progress: Progress): Promise<void> {
    // Note: progress is ignored because this operation is not cancellable and should not block in the browser anyway.
    if (params.event === 'fileChooser')
      await this._page.setFileChooserInterceptedBy(progress, params.enabled, this);
    if (params.enabled)
      this._subscriptions.add(params.event);
    else
      this._subscriptions.delete(params.event);
  }

  async keyboardDown(params: channels.PageKeyboardDownParams, progress: Progress): Promise<void> {
    await this._page.keyboard.apiDown(progress, params.key);
  }

  async keyboardUp(params: channels.PageKeyboardUpParams, progress: Progress): Promise<void> {
    await this._page.keyboard.apiUp(progress, params.key);
  }

  async keyboardInsertText(params: channels.PageKeyboardInsertTextParams, progress: Progress): Promise<void> {
    await this._page.keyboard.apiInsertText(progress, params.text);
  }

  async keyboardType(params: channels.PageKeyboardTypeParams, progress: Progress): Promise<void> {
    await this._page.keyboard.apiType(progress, params.text, params);
  }

  async keyboardPress(params: channels.PageKeyboardPressParams, progress: Progress): Promise<void> {
    await this._page.keyboard.apiPress(progress, params.key, params);
  }

  async clearConsoleMessages(params: channels.PageClearConsoleMessagesParams, progress: Progress): Promise<channels.PageClearConsoleMessagesResult> {
    this._page.clearConsoleMessages();
  }

  async consoleMessages(params: channels.PageConsoleMessagesParams, progress: Progress): Promise<channels.PageConsoleMessagesResult> {
    // Send all future console messages to the client, so that it can reliably receive all of them.
    // Otherwise, if subscription is added in a different task from this call (either before or after),
    // there is a chance for a duplicate or a lost console message.
    this._subscriptions.add('console');
    return { messages: this._page.consoleMessages(params.filter).map(message => this.parentScope().serializeConsoleMessage(message, this)) };
  }

  async clearPageErrors(params: channels.PageClearPageErrorsParams, progress: Progress): Promise<channels.PageClearPageErrorsResult> {
    this._page.clearPageErrors();
  }

  async pageErrors(params: channels.PagePageErrorsParams, progress: Progress): Promise<channels.PagePageErrorsResult> {
    return { errors: this._page.pageErrors(params.filter).map(error => serializeError(error)) };
  }

  async mouseMove(params: channels.PageMouseMoveParams, progress: Progress): Promise<void> {
    await this._page.mouse.apiMove(progress, params.x, params.y, params);
  }

  async mouseDown(params: channels.PageMouseDownParams, progress: Progress): Promise<void> {
    await this._page.mouse.apiDown(progress, params);
  }

  async mouseUp(params: channels.PageMouseUpParams, progress: Progress): Promise<void> {
    await this._page.mouse.apiUp(progress, params);
  }

  async mouseClick(params: channels.PageMouseClickParams, progress: Progress): Promise<void> {
    await this._page.mouse.apiClick(progress, params.x, params.y, params);
  }

  async mouseWheel(params: channels.PageMouseWheelParams, progress: Progress): Promise<void> {
    await this._page.mouse.apiWheel(progress, params.deltaX, params.deltaY);
  }

  async touchscreenTap(params: channels.PageTouchscreenTapParams, progress: Progress): Promise<void> {
    progress.metadata.point = { x: params.x, y: params.y };
    await this._page.touchscreen.apiTap(progress, params.x, params.y);
  }

  async pdf(params: channels.PagePdfParams, progress: Progress): Promise<channels.PagePdfResult> {
    if (!this._page.pdf)
      throw new Error('PDF generation is only supported for Headless Chromium');
    const buffer = await progress.race(this._page.pdf(params));
    return { pdf: buffer };
  }

  async requests(params: channels.PageRequestsParams, progress: Progress): Promise<channels.PageRequestsResult> {
    // Send all future requests to the client, so that it can reliably receive all of them.
    // Otherwise, if subscription is added in a different task from this call (either before or after),
    // there is a chance for a duplicate or a lost request.
    this._subscriptions.add('request');
    return { requests: this._page.networkRequests().map(request => RequestDispatcher.from(this.parentScope(), request)) };
  }

  async bringToFront(params: channels.PageBringToFrontParams, progress: Progress): Promise<void> {
    await this._page.bringToFront(progress);
  }

  async pickLocator(params: channels.PagePickLocatorParams, progress: Progress): Promise<channels.PagePickLocatorResult> {
    const recorder = await progress.race(Recorder.forContext(this._page.browserContext, { omitCallTracking: true, hideToolbar: true }));
    const selector = await recorder.pickLocator(progress, this._page);
    return { selector };
  }

  async cancelPickLocator(params: channels.PageCancelPickLocatorParams, progress: Progress): Promise<void> {
    const recorder = await progress.race(Recorder.existingForContext(this._page.browserContext));
    if (recorder)
      await progress.race(recorder.setMode('none'));
  }

  async hideHighlight(params: channels.PageHideHighlightParams, progress: Progress): Promise<void> {
    await progress.race(this._page.hideHighlight());
  }

  async screencastShowOverlay(params: channels.PageScreencastShowOverlayParams): Promise<channels.PageScreencastShowOverlayResult> {
    const id = await this._page.overlay.show(params.html, params.duration);
    return { id };
  }

  async screencastRemoveOverlay(params: channels.PageScreencastRemoveOverlayParams): Promise<channels.PageScreencastRemoveOverlayResult> {
    await this._page.overlay.remove(params.id);
  }

  async screencastChapter(params: channels.PageScreencastChapterParams): Promise<channels.PageScreencastChapterResult> {
    await this._page.overlay.chapter(params);
  }

  async screencastSetOverlayVisible(params: channels.PageScreencastSetOverlayVisibleParams): Promise<channels.PageScreencastSetOverlayVisibleResult> {
    await this._page.overlay.setVisible(params.visible);
  }

  async screencastShowActions(params: channels.PageScreencastShowActionsParams): Promise<channels.PageScreencastShowActionsResult> {
    this._page.screencast.showActions({ duration: params.duration, position: params.position, fontSize: params.fontSize });
  }

  async screencastHideActions(): Promise<channels.PageScreencastHideActionsResult> {
    this._page.screencast.hideActions();
  }

  async screencastStart(params: channels.PageScreencastStartParams, progress?: Progress): Promise<channels.PageScreencastStartResult> {
    if (this._screencastClient || this._videoRecorder)
      throw new Error('Screencast is already running');

    if (params.sendFrames) {
      this._screencastClient = {
        onFrame: (frame: ScreencastFrame) => {
          this._dispatchEvent('screencastFrame', { data: frame.buffer });
        },
        dispose: () => {},
        size: params.size,
        quality: params.quality,
      };
      this._page.screencast.addClient(this._screencastClient);
    }

    let artifact: Artifact | undefined;
    if (params.record) {
      this._videoRecorder = new VideoRecorder(this._page.screencast);
      artifact = this._videoRecorder.start(params);
    }
    return { artifact: artifact ? createVideoDispatcher(this.parentScope(), artifact) : undefined };
  }

  async screencastStop(params: channels.PageScreencastStopParams, progress?: Progress): Promise<channels.PageScreencastStopResult> {
    if (this._videoRecorder) {
      await this._videoRecorder.stop();
      this._videoRecorder = undefined;
    }

    const client = this._screencastClient;
    this._screencastClient = undefined;
    if (client)
      this._page.screencast.removeClient(client);
  }

  async startJSCoverage(params: channels.PageStartJSCoverageParams, progress: Progress): Promise<void> {
    const coverage = this._page.coverage as CRCoverage;
    await coverage.startJSCoverage(progress, params);
    this._jsCoverageActive = true;
  }

  async stopJSCoverage(params: channels.PageStopJSCoverageParams, progress: Progress): Promise<channels.PageStopJSCoverageResult> {
    this._jsCoverageActive = false;
    const coverage = this._page.coverage as CRCoverage;
    return await progress.race(coverage.stopJSCoverage());
  }

  async startCSSCoverage(params: channels.PageStartCSSCoverageParams, progress: Progress): Promise<void> {
    const coverage = this._page.coverage as CRCoverage;
    await coverage.startCSSCoverage(progress, params);
    this._cssCoverageActive = true;
  }

  async stopCSSCoverage(params: channels.PageStopCSSCoverageParams, progress: Progress): Promise<channels.PageStopCSSCoverageResult> {
    this._cssCoverageActive = false;
    const coverage = this._page.coverage as CRCoverage;
    return await progress.race(coverage.stopCSSCoverage());
  }

  _onFrameAttached(frame: Frame) {
    this._dispatchEvent('frameAttached', { frame: FrameDispatcher.from(this.parentScope(), frame) });
  }

  _onFrameDetached(frame: Frame) {
    this._dispatchEvent('frameDetached', { frame: FrameDispatcher.from(this.parentScope(), frame) });
  }

  override _onDispose() {
    // Avoid protocol calls for the closed page.
    if (this._page.isClosedOrClosingOrCrashed())
      return;

    // Cleanup properly and leave the page in a good state. Other clients may still connect and use it.
    this._interceptionUrlMatchers = [];
    this._page.removeRequestInterceptor(this._requestInterceptor).catch(() => {});
    disposeAll(this._disposables).catch(() => {});
    if (this._routeWebSocketInitScript)
      WebSocketRouteDispatcher.uninstall(this.connection, this._page, this._routeWebSocketInitScript).catch(() => {});
    this._routeWebSocketInitScript = undefined;
    for (const uid of this._locatorHandlers)
      this._page.unregisterLocatorHandler(uid);
    this._locatorHandlers.clear();
    this._page.setFileChooserInterceptedBy(nullProgress, false, this).catch(() => {});
    if (this._jsCoverageActive)
      (this._page.coverage as CRCoverage).stopJSCoverage().catch(() => {});
    this._jsCoverageActive = false;
    if (this._cssCoverageActive)
      (this._page.coverage as CRCoverage).stopCSSCoverage().catch(() => {});
    this._cssCoverageActive = false;
    this.screencastStop({}, undefined).catch(() => {});
  }

  async setDockTile(params: channels.PageSetDockTileParams): Promise<void> {
    await this._page.setDockTile(params.image);
  }
}


export class WorkerDispatcher extends Dispatcher<Worker, channels.WorkerChannel, PageDispatcher | BrowserContextDispatcher | BrowserTypeDispatcher> implements channels.WorkerChannel {
  _type_Worker = true;
  _type_EventTarget = true;

  readonly _subscriptions = new Set<channels.WorkerUpdateSubscriptionParams['event']>();

  static fromNullable(scope: PageDispatcher | BrowserContextDispatcher | BrowserTypeDispatcher, worker: Worker | null): WorkerDispatcher | undefined {
    if (!worker)
      return undefined;
    const result = scope.connection.existingDispatcher<WorkerDispatcher>(worker);
    return result || new WorkerDispatcher(scope, worker);
  }

  constructor(scope: PageDispatcher | BrowserContextDispatcher | BrowserTypeDispatcher, worker: Worker) {
    super(scope, worker, 'Worker', {
      url: worker.url
    });
    this.addObjectListener(Worker.Events.Console, (message: ConsoleMessage) => {
      if (!this._subscriptions.has('console'))
        return;
      this._dispatchEvent('console', {
        type: message.type(),
        text: message.text(),
        args: message.args().map(a => JSHandleDispatcher.fromJSHandle(this, a)),
        location: message.location(),
        timestamp: message.timestamp(),
      });
    });
    this.addObjectListener(Worker.Events.Close, () => this._dispatchEvent('close'));
  }

  async disconnect(params: channels.WorkerDisconnectParams, progress: Progress): Promise<void> {
    progress.metadata.potentiallyClosesScope = true;
    await this._object.disconnect(progress, params);
  }

  async evaluateExpression(params: channels.WorkerEvaluateExpressionParams, progress: Progress): Promise<channels.WorkerEvaluateExpressionResult> {
    return { value: serializeResult(await this._object.evaluateExpression(progress, params.expression, params.isFunction, parseArgument(params.arg))) };
  }

  async evaluateExpressionHandle(params: channels.WorkerEvaluateExpressionHandleParams, progress: Progress): Promise<channels.WorkerEvaluateExpressionHandleResult> {
    return { handle: JSHandleDispatcher.fromJSHandle(this, await this._object.evaluateExpressionHandle(progress, params.expression, params.isFunction, parseArgument(params.arg))) };
  }

  async updateSubscription(params: channels.WorkerUpdateSubscriptionParams, progress: Progress): Promise<void> {
    if (params.enabled)
      this._subscriptions.add(params.event);
    else
      this._subscriptions.delete(params.event);
  }
}

export class BindingCallDispatcher extends Dispatcher<SdkObject, channels.BindingCallChannel, PageDispatcher | BrowserContextDispatcher> implements channels.BindingCallChannel {
  _type_BindingCall = true;
  private _resolve: ((arg: any) => void) | undefined;
  private _reject: ((error: any) => void) | undefined;
  private _promise: Promise<any>;

  constructor(scope: PageDispatcher, name: string, source: { context: BrowserContext, page: Page, frame: Frame }, args: any[]) {
    const frameDispatcher = FrameDispatcher.from(scope.parentScope(), source.frame);
    super(scope, new SdkObject(scope._object, 'bindingCall'), 'BindingCall', {
      frame: frameDispatcher,
      name,
      args: args.map(serializeResult),
    });
    this._promise = new Promise((resolve, reject) => {
      this._resolve = resolve;
      this._reject = reject;
    });
  }

  promise() {
    return this._promise;
  }

  async resolve(params: channels.BindingCallResolveParams, progress: Progress): Promise<void> {
    this._resolve!(parseArgument(params.result));
    this._dispose();
  }

  async reject(params: channels.BindingCallRejectParams, progress: Progress): Promise<void> {
    this._reject!(parseError(params.error));
    this._dispose();
  }
}

function createVideoDispatcher(parentScope: BrowserContextDispatcher, video: Artifact): ArtifactDispatcher {
  // Note: Video must outlive Page and BrowserContext, so that client can saveAs it
  // after closing the context. We use |scope| for it.
  return ArtifactDispatcher.from(parentScope.parentScope(), video);
}
