// Copyright (C) 2024 The Android Open Source Project
//
// 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 {defer} from '../base/deferred';
import {raf} from '../core/raf_scheduler';
import {AppImpl} from '../core/app_impl';
import {taskTracker as frontendTaskTracker} from './task_tracker';

/**
 * This class is exposed by index.ts as window.waitForPerfettoIdle() and is used
 * by tests, to detect when we reach quiescence.
 */

const IDLE_HYSTERESIS_MS = 100;
const TIMEOUT_MS = 60_000;

export class IdleDetector {
  private promise = defer<void>();
  private deadline = performance.now() + TIMEOUT_MS;
  private idleSince?: number;
  private idleHysteresisMs = IDLE_HYSTERESIS_MS;

  waitForPerfettoIdle(idleHysteresisMs = IDLE_HYSTERESIS_MS): Promise<void> {
    this.idleSince = undefined;
    this.idleHysteresisMs = idleHysteresisMs;
    this.scheduleNextTask();
    return this.promise;
  }

  private onIdleCallback() {
    const now = performance.now();
    if (now > this.deadline) {
      this.promise.reject(
        `Didn't reach idle within ${TIMEOUT_MS} ms, giving up` +
          ` ${this.idleIndicators()}`,
      );
      return;
    }
    if (this.idleIndicators().every((x) => x)) {
      this.idleSince = this.idleSince ?? now;
      const idleDur = now - this.idleSince;
      if (idleDur >= this.idleHysteresisMs) {
        // We have been idle for more than the threshold, success.
        this.promise.resolve();
        return;
      }
      // We are idle, but not for long enough. keep waiting
      this.scheduleNextTask();
      return;
    }
    // Not idle, reset and repeat.
    this.idleSince = undefined;
    this.scheduleNextTask();
  }

  private scheduleNextTask() {
    requestIdleCallback(() => this.onIdleCallback());
  }

  private idleIndicators() {
    const reqsPending = AppImpl.instance.trace?.engine.numRequestsPending ?? 0;
    return [
      !AppImpl.instance.isLoadingTrace,
      reqsPending === 0,
      !raf.hasPendingRedraws,
      !AppImpl.instance.taskTracker.hasPendingTasks(),
      !frontendTaskTracker.hasPendingTasks(),
      !document.getAnimations().some((a) => {
        if (a.playState !== 'running') return false;
        // Ignore infinite animations (e.g., cursor blinks) - they're decorative
        const iterations = a.effect?.getTiming().iterations;
        return iterations !== Infinity;
      }),
      document.querySelector('.progress.progress-anim') == null,
      document.querySelector('.pf-omnibox--message-mode') == null,
    ];
  }
}
