/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import type { FrameMetricProps } from '../VirtualizedList/VirtualizedListProps'

export type FillRateInfo = Info

class Info {
  any_blank_count = 0
  any_blank_ms = 0
  any_blank_speed_sum = 0
  mostly_blank_count = 0
  mostly_blank_ms = 0
  pixels_blank = 0
  pixels_sampled = 0
  pixels_scrolled = 0
  total_time_spent = 0
  sample_count = 0
}

type FrameMetrics = {
  inLayout?: boolean
  length: number
  offset: number
}

const DEBUG = false

let _listeners: Array<(info: Info) => void> = []
let _minSampleCount = 10
let _sampleRate: number | null = DEBUG ? 1 : null

/**
 * A helper class for detecting when the maximum fill rate of `VirtualizedList` is exceeded.
 * By default the sampling rate is set to zero and this will do nothing. If you want to collect
 * samples (e.g., to log them), make sure to call `FillRateHelper.setSampleRate(0.0-1.0)`.
 *
 * Listeners and sample rate are global for all `VirtualizedList`s - typical usage will combine with
 * `SceneTracker.getActiveScene` to determine the context of the events.
 */
class FillRateHelper {
  private _anyBlankStartTime: number | null = null
  private _enabled = false
  private _getFrameMetrics: (
    index: number,
    props: FrameMetricProps
  ) => FrameMetrics | null
  private _info: Info = new Info()
  private _mostlyBlankStartTime: number | null = null
  private _samplesStartTime: number | null = null

  static addListener(callback: (info: FillRateInfo) => void): { remove: () => void } {
    if (_sampleRate === null) {
      console.warn('Call `FillRateHelper.setSampleRate` before `addListener`.')
    }
    _listeners.push(callback)
    return {
      remove: () => {
        _listeners = _listeners.filter((listener) => callback !== listener)
      },
    }
  }

  static setSampleRate(sampleRate: number) {
    _sampleRate = sampleRate
  }

  static setMinSampleCount(minSampleCount: number) {
    _minSampleCount = minSampleCount
  }

  constructor(
    getFrameMetrics: (index: number, props: FrameMetricProps) => FrameMetrics | null
  ) {
    this._getFrameMetrics = getFrameMetrics
    this._enabled = (_sampleRate || 0) > Math.random()
    this._resetData()
  }

  activate() {
    if (this._enabled && this._samplesStartTime == null) {
      DEBUG && console.debug('FillRateHelper: activate')
      this._samplesStartTime = globalThis.performance.now()
    }
  }

  deactivateAndFlush() {
    if (!this._enabled) {
      return
    }
    const start = this._samplesStartTime
    if (start == null) {
      DEBUG && console.debug('FillRateHelper: bail on deactivate with no start time')
      return
    }
    if (this._info.sample_count < _minSampleCount) {
      this._resetData()
      return
    }
    const total_time_spent = globalThis.performance.now() - start
    const info: Partial<Info> = {
      ...this._info,
      total_time_spent,
    }
    if (DEBUG) {
      const derived = {
        avg_blankness: this._info.pixels_blank / this._info.pixels_sampled,
        avg_speed: this._info.pixels_scrolled / (total_time_spent / 1000),
        avg_speed_when_any_blank:
          this._info.any_blank_speed_sum / this._info.any_blank_count,
        any_blank_per_min: this._info.any_blank_count / (total_time_spent / 1000 / 60),
        any_blank_time_frac: this._info.any_blank_ms / total_time_spent,
        mostly_blank_per_min:
          this._info.mostly_blank_count / (total_time_spent / 1000 / 60),
        mostly_blank_time_frac: this._info.mostly_blank_ms / total_time_spent,
      }
      for (const key in derived) {
        ;(derived as any)[key] = Math.round(1000 * (derived as any)[key]) / 1000
      }
      console.debug('FillRateHelper deactivateAndFlush: ', { derived, info })
    }
    _listeners.forEach((listener) => listener(info as Info))
    this._resetData()
  }

  computeBlankness(
    props: FrameMetricProps & { initialNumToRender?: number | null },
    cellsAroundViewport: { first: number; last: number },
    scrollMetrics: {
      dOffset: number
      offset: number
      velocity: number
      visibleLength: number
    }
  ): number {
    if (
      !this._enabled ||
      props.getItemCount(props.data) === 0 ||
      cellsAroundViewport.last < cellsAroundViewport.first ||
      this._samplesStartTime == null
    ) {
      return 0
    }
    const { dOffset, offset, velocity, visibleLength } = scrollMetrics

    this._info.sample_count++
    this._info.pixels_sampled += Math.round(visibleLength)
    this._info.pixels_scrolled += Math.round(Math.abs(dOffset))
    const scrollSpeed = Math.round(Math.abs(velocity) * 1000) // px / sec

    const now = globalThis.performance.now()
    if (this._anyBlankStartTime != null) {
      this._info.any_blank_ms += now - this._anyBlankStartTime
    }
    this._anyBlankStartTime = null
    if (this._mostlyBlankStartTime != null) {
      this._info.mostly_blank_ms += now - this._mostlyBlankStartTime
    }
    this._mostlyBlankStartTime = null

    let blankTop = 0
    let first = cellsAroundViewport.first
    let firstFrame = this._getFrameMetrics(first, props)
    while (first <= cellsAroundViewport.last && (!firstFrame || !firstFrame.inLayout)) {
      firstFrame = this._getFrameMetrics(first, props)
      first++
    }
    if (firstFrame && first > 0) {
      blankTop = Math.min(visibleLength, Math.max(0, firstFrame.offset - offset))
    }
    let blankBottom = 0
    let last = cellsAroundViewport.last
    let lastFrame = this._getFrameMetrics(last, props)
    while (last >= cellsAroundViewport.first && (!lastFrame || !lastFrame.inLayout)) {
      lastFrame = this._getFrameMetrics(last, props)
      last--
    }
    if (lastFrame && last < props.getItemCount(props.data) - 1) {
      const bottomEdge = lastFrame.offset + lastFrame.length
      blankBottom = Math.min(
        visibleLength,
        Math.max(0, offset + visibleLength - bottomEdge)
      )
    }
    const pixels_blank = Math.round(blankTop + blankBottom)
    const blankness = pixels_blank / visibleLength
    if (blankness > 0) {
      this._anyBlankStartTime = now
      this._info.any_blank_speed_sum += scrollSpeed
      this._info.any_blank_count++
      this._info.pixels_blank += pixels_blank
      if (blankness > 0.5) {
        this._mostlyBlankStartTime = now
        this._info.mostly_blank_count++
      }
    } else if (scrollSpeed < 0.01 || Math.abs(dOffset) < 1) {
      this.deactivateAndFlush()
    }
    return blankness
  }

  enabled(): boolean {
    return this._enabled
  }

  private _resetData() {
    this._anyBlankStartTime = null
    this._info = new Info()
    this._mostlyBlankStartTime = null
    this._samplesStartTime = null
  }
}

export default FillRateHelper
