/**
 * @license
 * Copyright 2022 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import {
  SyntaxWorkerRequest,
  SyntaxWorkerInit,
  SyntaxWorkerResult,
  SyntaxWorkerMessageType,
  SyntaxLayerLine,
} from '../../types/syntax-worker-api';
import {prependOrigin} from '../../utils/url-util';
import {createWorker} from '../../utils/worker-util';
import {ReportingService} from '../gr-reporting/gr-reporting';
import {Finalizable} from '../registry';

const hljsLibUrl = `${
  window.STATIC_RESOURCE_PATH ?? ''
}/bower_components/highlightjs/highlight.min.js`;

const syntaxWorkerUrl = `${
  window.STATIC_RESOURCE_PATH ?? ''
}/workers/syntax-worker.js`;

/**
 * It is unlikely that a pool size greater than 3 will gain anything, because
 * the app also needs the resources to process the results.
 */
const WORKER_POOL_SIZE = 3;

/**
 * Safe guard for not killing the browser.
 */
export const CODE_MAX_LINES = 20 * 1000;

/**
 * Safe guard for not killing the browser. Maximum in number of chars.
 */
const CODE_MAX_LENGTH = 25 * CODE_MAX_LINES;

/**
 * Service for syntax highlighting. Maintains some HighlightJS workers doing
 * their job in the background.
 */
export class HighlightService implements Finalizable {
  // visible for testing
  poolIdle: Set<Worker> = new Set();

  // visible for testing
  poolBusy: Set<Worker> = new Set();

  // visible for testing
  /** Queue for waiting that a worker becomes available. */
  queueForWorker: Array<() => void> = [];

  // visible for testing
  /** Queue for waiting on the results of a worker. */
  queueForResult: Map<Worker, (r: SyntaxLayerLine[]) => void> = new Map();

  constructor(readonly reporting: ReportingService) {
    for (let i = 0; i < WORKER_POOL_SIZE; i++) {
      this.addWorker();
    }
  }

  /** Allows tests to produce fake workers. */
  protected createWorker() {
    return createWorker(prependOrigin(syntaxWorkerUrl));
  }

  /** Creates, initializes and then moves a worker to the idle pool. */
  private addWorker() {
    const worker = this.createWorker();
    // Will move to the idle pool after being initialized.
    this.poolBusy.add(worker);
    worker.onmessage = (e: MessageEvent<SyntaxWorkerResult>) => {
      this.handleResult(worker, e.data);
    };
    const initMsg: SyntaxWorkerInit = {
      type: SyntaxWorkerMessageType.INIT,
      url: prependOrigin(hljsLibUrl),
    };
    worker.postMessage(initMsg);
  }

  private moveIdleToBusy() {
    const worker = this.poolIdle.values().next().value;
    this.poolIdle.delete(worker);
    this.poolBusy.add(worker);
    return worker;
  }

  private moveBusyToIdle(worker: Worker) {
    this.poolBusy.delete(worker);
    this.poolIdle.add(worker);
    const resolver = this.queueForWorker.shift();
    if (resolver) resolver();
  }

  /**
   * If there is worker in the idle pool, then return it. Otherwise wait for a
   * worker to become a available.
   */
  private async requestWorker(): Promise<Worker> {
    if (this.poolIdle.size > 0) {
      const worker = this.moveIdleToBusy();
      return Promise.resolve(worker);
    }
    await new Promise<void>(r => this.queueForWorker.push(r));
    return this.requestWorker();
  }

  /**
   * A worker is done with its job. Move it back to the idle pool and notify the
   * resolver that is waiting for the results.
   */
  private handleResult(worker: Worker, result: SyntaxWorkerResult) {
    this.moveBusyToIdle(worker);
    if (result.error) {
      this.reporting.error(
        'Diff Syntax Layer',
        new Error(`syntax worker failed: ${result.error}`)
      );
    }
    const resolver = this.queueForResult.get(worker);
    this.queueForResult.delete(worker);
    if (resolver) resolver(result.ranges ?? []);
  }

  async highlight(
    language?: string,
    code?: string
  ): Promise<SyntaxLayerLine[]> {
    if (!language || !code) return [];
    if (code.length > CODE_MAX_LENGTH) return [];
    const worker = await this.requestWorker();
    const message: SyntaxWorkerRequest = {
      type: SyntaxWorkerMessageType.REQUEST,
      language,
      code,
    };
    const promise = new Promise<SyntaxLayerLine[]>(r => {
      this.queueForResult.set(worker, r);
    });
    worker.postMessage(message);
    return await promise;
  }

  finalize() {
    for (const worker of this.poolIdle) {
      worker.terminate();
    }
    this.poolIdle.clear();
    for (const worker of this.poolBusy) {
      worker.terminate();
    }
    this.poolBusy.clear();
    this.queueForResult.clear();
    this.queueForWorker.length = 0;
  }
}
