import queueMicrotask from '@alloc/queue-microtask'
import { batchedUpdates } from 'react-batched-updates'

type Effect = () => void

/** The FIFO queue of async `Auto` updates */
let runQueue: Effect[] = []

/** The queue of `withAuto` updates. Sorted by deepest child last */
const renderQueue: RenderConfig[] = []
type RenderConfig = { depth: number; effect: Effect }

export interface Batch {
  /**
   * Run the given effect in the next microtask, immediately before
   * rendering any components with stale observations.
   *
   * To avoid infinite loops, the "run" step only runs the first 100,000
   * effects in any given microtask. Any remaining effects are scheduled
   * for the next microtask.
   */
  run(effect: Effect): void
  /**
   * The "render" step is sorted by `withAuto` depth.
   * After the "run" step, all `Auto` objects are updated, and `withAuto`
   * components can render now.
   */
  render(depth: number, effect: Effect): void
}

export const batch: Batch = {
  run(effect) {
    runQueue.push(effect)
    flushAsync()
  },
  render(depth, effect) {
    let i = renderQueue.findIndex(other => other.depth > depth)
    if (i < 0) i = renderQueue.length
    renderQueue.splice(i, 0, { depth, effect })
    flushAsync()
  },
}

let isQueued = false
function flushAsync() {
  if (!isQueued) {
    isQueued = true
    queueMicrotask(() => {
      isQueued = false
      if (flushSync()) {
        flushAsync()
      }
    })
  }
}

/**
 * Flush the queue of delayed reactions.
 *
 * Returns `true` if the queue wasn't flushed entirely.
 *
 * Useful when testing `wana`-integrated components/hooks.
 */
export function flushSync() {
  batchedUpdates(() => {
    // Run any pending reactions.
    let runs = 0
    for (const effect of runQueue) {
      if (++runs > 1e5) break
      effect()
    }

    // Postpone remaining runs until next flush.
    runQueue = runQueue.slice(runs)

    // Stale components always rerender after reactions.
    renderQueue.forEach(({ effect }) => effect())
    renderQueue.length = 0
  })

  return !!(runQueue.length || renderQueue.length)
}
