// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

interface Response {
  requestId: number;
  result: unknown;
  error: Error|null;
}

interface Event {
  event: string;
}

type Message = MessageEvent<Response|Event>;

export class ExtensionEndpoint {
  private readonly port: MessagePort;
  private nextRequestId = 0;
  private pendingRequests: Map<number, {
    resolve: (arg: unknown) => void,
    reject: (error: Error) => void,
  }>;

  constructor(port: MessagePort) {
    this.port = port;
    this.port.onmessage = this.onResponse.bind(this);
    this.pendingRequests = new Map();
  }

  sendRequest<ReturnType>(method: string, parameters: unknown): Promise<ReturnType> {
    return new Promise((resolve, reject) => {
      const requestId = this.nextRequestId++;
      this.pendingRequests.set(requestId, {resolve: resolve as (arg: unknown) => void, reject});
      this.port.postMessage({requestId, method, parameters});
    });
  }

  protected disconnect(): void {
    for (const {reject} of this.pendingRequests.values()) {
      reject(new Error('Extension endpoint disconnected'));
    }
    this.pendingRequests.clear();
    this.port.close();
  }

  private onResponse({data}: Message): void {
    if ('event' in data) {
      this.handleEvent(data);
      return;
    }
    const {requestId, result, error} = data;
    const pendingRequest = this.pendingRequests.get(requestId);
    if (!pendingRequest) {
      console.error(`No pending request ${requestId}`);
      return;
    }
    this.pendingRequests.delete(requestId);
    if (error) {
      pendingRequest.reject(new Error(error.message));
    } else {
      pendingRequest.resolve(result);
    }
  }

  protected handleEvent(_event: Event): void {
    throw new Error('handleEvent is not implemented');
  }
}
