// Copyright 2019 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 Host from '../../../../core/host/host.js';
import * as SDK from '../../../../core/sdk/sdk.js';
import type * as Protocol from '../../../../generated/protocol.js';

import {Memory} from './LineLevelProfile.js';

let liveHeapProfileInstance: LiveHeapProfile;
export class LiveHeapProfile implements Common.Runnable.Runnable,
                                        SDK.TargetManager.SDKModelObserver<SDK.HeapProfilerModel.HeapProfilerModel> {
  private running: boolean;
  private sessionId: number;
  private loadEventCallback: (arg0?: () => void|null) => void;
  private readonly setting: Common.Settings.Setting<boolean>;

  private constructor() {
    this.running = false;
    this.sessionId = 0;
    this.loadEventCallback = () => {};
    this.setting = Common.Settings.Settings.instance().moduleSetting('memory-live-heap-profile');
    this.setting.addChangeListener(event => event.data ? this.startProfiling() : this.stopProfiling());
    if (this.setting.get()) {
      void this.startProfiling();
    }
  }

  static instance(opts: {forceNew: boolean|null} = {forceNew: null}): LiveHeapProfile {
    const {forceNew} = opts;
    if (!liveHeapProfileInstance || forceNew) {
      liveHeapProfileInstance = new LiveHeapProfile();
    }

    return liveHeapProfileInstance;
  }

  async run(): Promise<void> {
    return;
  }

  modelAdded(model: SDK.HeapProfilerModel.HeapProfilerModel): void {
    void model.startSampling(1e4);
  }

  modelRemoved(_model: SDK.HeapProfilerModel.HeapProfilerModel): void {
    // Cannot do much when the model has already been removed.
  }

  private async startProfiling(): Promise<void> {
    if (this.running) {
      return;
    }
    this.running = true;
    const sessionId = this.sessionId;
    SDK.TargetManager.TargetManager.instance().observeModels(SDK.HeapProfilerModel.HeapProfilerModel, this);
    SDK.TargetManager.TargetManager.instance().addModelListener(
        SDK.ResourceTreeModel.ResourceTreeModel, SDK.ResourceTreeModel.Events.Load, this.loadEventFired, this);

    do {
      const models = SDK.TargetManager.TargetManager.instance().models(SDK.HeapProfilerModel.HeapProfilerModel);
      const profiles = await Promise.all(models.map(model => model.getSamplingProfile()));
      if (sessionId !== this.sessionId) {
        break;
      }

      const profilesAndTargets: Array<{
        profile: Protocol.HeapProfiler.SamplingHeapProfile,
        target: SDK.Target.Target,
      }> = [];
      for (let i = 0; i < profiles.length; ++i) {
        const profile = profiles[i];
        if (!profile) {
          continue;
        }

        const target = models[i].target();
        profilesAndTargets.push({profile, target});
      }

      Memory.instance().initialize(profilesAndTargets);

      await Promise.race([
        new Promise(r => window.setTimeout(r, Host.InspectorFrontendHost.isUnderTest() ? 10 : 5000)),
        new Promise(r => {
          this.loadEventCallback = r;
        }),
      ]);
    } while (sessionId === this.sessionId);

    SDK.TargetManager.TargetManager.instance().unobserveModels(SDK.HeapProfilerModel.HeapProfilerModel, this);
    SDK.TargetManager.TargetManager.instance().removeModelListener(
        SDK.ResourceTreeModel.ResourceTreeModel, SDK.ResourceTreeModel.Events.Load, this.loadEventFired, this);
    for (const model of SDK.TargetManager.TargetManager.instance().models(SDK.HeapProfilerModel.HeapProfilerModel)) {
      void model.stopSampling();
    }
    Memory.instance().reset();
  }

  private stopProfiling(): void {
    if (!this.running) {
      return;
    }
    this.running = false;
    this.sessionId++;
  }

  private loadEventFired(): void {
    this.loadEventCallback();
  }
}
