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

import * as Common from '../../core/common/common.js';
import * as Root from '../root/root.js';

import {EmulationModel} from './EmulationModel.js';
import {type SDKModelObserver, TargetManager} from './TargetManager.js';

export class CPUThrottlingManager extends Common.ObjectWrapper.ObjectWrapper<EventTypes> implements
    SDKModelObserver<EmulationModel> {
  readonly #targetManager: TargetManager;
  #cpuThrottlingRate: number;
  #hardwareConcurrency?: number;
  #pendingMainTargetPromise?: (r: number) => void;

  constructor(settings: Common.Settings.Settings, targetManager: TargetManager) {
    super();
    this.#targetManager = targetManager;
    this.#cpuThrottlingRate = 1;  // No throttling
    targetManager.observeModels(EmulationModel, this);
  }

  static instance(opts: {
    forceNew?: boolean|null,
    settings?: Common.Settings.Settings,
    targetManager?: TargetManager,
  } = {forceNew: null}): CPUThrottlingManager {
    const {forceNew} = opts;
    if (!Root.DevToolsContext.globalInstance().has(CPUThrottlingManager) || forceNew) {
      Root.DevToolsContext.globalInstance().set(
          CPUThrottlingManager,
          new CPUThrottlingManager(opts.settings ?? Common.Settings.Settings.instance(),
                                   opts.targetManager ?? TargetManager.instance()));
    }

    return Root.DevToolsContext.globalInstance().get(CPUThrottlingManager);
  }

  static removeInstance(): void {
    Root.DevToolsContext.globalInstance().delete(CPUThrottlingManager);
  }

  cpuThrottlingRate(): number {
    return this.#cpuThrottlingRate;
  }

  setCPUThrottlingRate(rate: number): void {
    if (rate === this.#cpuThrottlingRate) {
      return;
    }

    this.#cpuThrottlingRate = rate;
    for (const emulationModel of this.#targetManager.models(EmulationModel)) {
      void emulationModel.setCPUThrottlingRate(this.#cpuThrottlingRate);
    }
    this.dispatchEventToListeners(Events.RATE_CHANGED, this.#cpuThrottlingRate);
  }

  setHardwareConcurrency(concurrency: number): void {
    this.#hardwareConcurrency = concurrency;
    for (const emulationModel of this.#targetManager.models(EmulationModel)) {
      void emulationModel.setHardwareConcurrency(concurrency);
    }
    this.dispatchEventToListeners(Events.HARDWARE_CONCURRENCY_CHANGED, this.#hardwareConcurrency);
  }

  hasPrimaryPageTargetSet(): boolean {
    // In some environments, such as Node, trying to check if we have a page
    // target may error. So if we get any errors here at all, assume that we do
    // not have a target.
    try {
      return this.#targetManager.primaryPageTarget() !== null;
    } catch {
      return false;
    }
  }

  async getHardwareConcurrency(): Promise<number> {
    const target = this.#targetManager.primaryPageTarget();
    const existingCallback = this.#pendingMainTargetPromise;

    // If the main target hasn't attached yet, block callers until it appears.
    if (!target) {
      if (existingCallback) {
        return await new Promise(r => {
          this.#pendingMainTargetPromise = (result: number) => {
            r(result);
            existingCallback(result);
          };
        });
      }
      return await new Promise(r => {
        this.#pendingMainTargetPromise = r;
      });
    }

    const evalResult = await target.runtimeAgent().invoke_evaluate(
        {expression: 'navigator.hardwareConcurrency', returnByValue: true, silent: true, throwOnSideEffect: true});
    const error = evalResult.getError();
    if (error) {
      throw new Error(error);
    }
    const {result, exceptionDetails} = evalResult;
    if (exceptionDetails) {
      throw new Error(exceptionDetails.text);
    }
    return result.value;
  }

  modelAdded(emulationModel: EmulationModel): void {
    if (this.#cpuThrottlingRate !== 1) {
      void emulationModel.setCPUThrottlingRate(this.#cpuThrottlingRate);
    }
    if (this.#hardwareConcurrency !== undefined) {
      void emulationModel.setHardwareConcurrency(this.#hardwareConcurrency);
    }

    // If there are any callers blocked on a getHardwareConcurrency call, let's wake them now.
    if (this.#pendingMainTargetPromise) {
      const existingCallback = this.#pendingMainTargetPromise;
      this.#pendingMainTargetPromise = undefined;
      void this.getHardwareConcurrency().then(existingCallback);
    }
  }

  modelRemoved(_emulationModel: EmulationModel): void {
    // Implemented as a requirement for being a SDKModelObserver.
  }
}

export const enum Events {
  RATE_CHANGED = 'RateChanged',
  HARDWARE_CONCURRENCY_CHANGED = 'HardwareConcurrencyChanged',
}

export interface EventTypes {
  [Events.RATE_CHANGED]: number;
  [Events.HARDWARE_CONCURRENCY_CHANGED]: number;
}
