/* eslint-disable */
// Adapted from Alien Signals
// https://github.com/stackblitz/alien-signals/

import { ReactiveFlags, createReactiveSystem } from './alien'

import type { ReactiveNode } from './alien'

export { ReactiveFlags, createReactiveSystem } from './alien'
export type { Link, ReactiveNode } from './alien'

interface EffectNode extends ReactiveNode {
  fn(): void
}

interface ComputedNode<T = any> extends ReactiveNode {
  value: T | undefined
  getter: (previousValue?: T) => T
}

interface SignalNode<T = any> extends ReactiveNode {
  currentValue: T
  pendingValue: T
}

let cycle = 0
let batchDepth = 0
let notifyIndex = 0
let queuedLength = 0
let activeSub: ReactiveNode | undefined

const queued: (EffectNode | undefined)[] = []
const { link, unlink, propagate, checkDirty, shallowPropagate } =
  /* @__PURE__ */ createReactiveSystem({
    update(node: SignalNode | ComputedNode): boolean {
      if (node.depsTail !== undefined) {
        return updateComputed(node as ComputedNode)
      } else {
        return updateSignal(node as SignalNode)
      }
    },
    notify(effect: EffectNode) {
      let insertIndex = queuedLength
      let firstInsertedIndex = insertIndex

      do {
        queued[insertIndex++] = effect
        effect.flags &= ~ReactiveFlags.Watching
        effect = effect.subs?.sub as EffectNode
        if (effect === undefined || !(effect.flags & ReactiveFlags.Watching)) {
          break
        }
      } while (true)

      queuedLength = insertIndex

      while (firstInsertedIndex < --insertIndex) {
        const left = queued[firstInsertedIndex]
        queued[firstInsertedIndex++] = queued[insertIndex]
        queued[insertIndex] = left
      }
    },
    unwatched(node) {
      if (!(node.flags & ReactiveFlags.Mutable)) {
        effectScopeOper.call(node)
      } else if (node.depsTail !== undefined) {
        node.depsTail = undefined
        node.flags = ReactiveFlags.Mutable | ReactiveFlags.Dirty
        purgeDeps(node)
      }
    },
  })

export function getActiveSub(): ReactiveNode | undefined {
  return activeSub
}

export function setActiveSub(sub?: ReactiveNode) {
  const prevSub = activeSub
  activeSub = sub
  return prevSub
}

export function getBatchDepth(): number {
  return batchDepth
}

export function startBatch() {
  ++batchDepth
}

export function endBatch() {
  if (!--batchDepth) {
    flush()
  }
}

export function isSignal(fn: () => void): boolean {
  return fn.name === 'bound ' + signalOper.name
}

export function isComputed(fn: () => void): boolean {
  return fn.name === 'bound ' + computedOper.name
}

export function isEffect(fn: () => void): boolean {
  return fn.name === 'bound ' + effectOper.name
}

export function isEffectScope(fn: () => void): boolean {
  return fn.name === 'bound ' + effectScopeOper.name
}

export function signal<T>(): {
  (): T | undefined
  (value: T | undefined): void
}
export function signal<T>(initialValue: T): {
  (): T
  (value: T): void
}
export function signal<T>(initialValue?: T): {
  (): T | undefined
  (value: T | undefined): void
} {
  return signalOper.bind({
    currentValue: initialValue,
    pendingValue: initialValue,
    subs: undefined,
    subsTail: undefined,
    flags: ReactiveFlags.Mutable,
  }) as () => T | undefined
}

export function computed<T>(getter: (previousValue?: T) => T): () => T {
  return computedOper.bind({
    value: undefined,
    subs: undefined,
    subsTail: undefined,
    deps: undefined,
    depsTail: undefined,
    flags: ReactiveFlags.None,
    getter: getter as (previousValue?: unknown) => unknown,
  }) as () => T
}

export function effect(fn: () => void): () => void {
  const e: EffectNode = {
    fn,
    subs: undefined,
    subsTail: undefined,
    deps: undefined,
    depsTail: undefined,
    flags: ReactiveFlags.Watching | ReactiveFlags.RecursedCheck,
  }
  const prevSub = setActiveSub(e)
  if (prevSub !== undefined) {
    link(e, prevSub, 0)
  }
  try {
    e.fn()
  } finally {
    activeSub = prevSub
    e.flags &= ~ReactiveFlags.RecursedCheck
  }
  return effectOper.bind(e)
}

export function effectScope(fn: () => void): () => void {
  const e: ReactiveNode = {
    deps: undefined,
    depsTail: undefined,
    subs: undefined,
    subsTail: undefined,
    flags: ReactiveFlags.None,
  }
  const prevSub = setActiveSub(e)
  if (prevSub !== undefined) {
    link(e, prevSub, 0)
  }
  try {
    fn()
  } finally {
    activeSub = prevSub
  }
  return effectScopeOper.bind(e)
}

export function trigger(fn: () => void) {
  const sub: ReactiveNode = {
    deps: undefined,
    depsTail: undefined,
    flags: ReactiveFlags.Watching,
  }
  const prevSub = setActiveSub(sub)
  try {
    fn()
  } finally {
    activeSub = prevSub
    let link = sub.deps
    while (link !== undefined) {
      const dep = link.dep
      link = unlink(link, sub)
      const subs = dep.subs
      if (subs !== undefined) {
        sub.flags = ReactiveFlags.None
        propagate(subs)
        shallowPropagate(subs)
      }
    }
    if (!batchDepth) {
      flush()
    }
  }
}

function updateComputed(c: ComputedNode): boolean {
  ++cycle
  c.depsTail = undefined
  c.flags = ReactiveFlags.Mutable | ReactiveFlags.RecursedCheck
  const prevSub = setActiveSub(c)
  try {
    const oldValue = c.value
    return oldValue !== (c.value = c.getter(oldValue))
  } finally {
    activeSub = prevSub
    c.flags &= ~ReactiveFlags.RecursedCheck
    purgeDeps(c)
  }
}

function updateSignal(s: SignalNode): boolean {
  s.flags = ReactiveFlags.Mutable
  return s.currentValue !== (s.currentValue = s.pendingValue)
}

function run(e: EffectNode): void {
  const flags = e.flags
  if (
    flags & ReactiveFlags.Dirty ||
    (flags & ReactiveFlags.Pending && checkDirty(e.deps!, e))
  ) {
    ++cycle
    e.depsTail = undefined
    e.flags = ReactiveFlags.Watching | ReactiveFlags.RecursedCheck
    const prevSub = setActiveSub(e)
    try {
      ;(e as EffectNode).fn()
    } finally {
      activeSub = prevSub
      e.flags &= ~ReactiveFlags.RecursedCheck
      purgeDeps(e)
    }
  } else {
    e.flags = ReactiveFlags.Watching
  }
}

function flush(): void {
  try {
    while (notifyIndex < queuedLength) {
      const effect = queued[notifyIndex]!
      queued[notifyIndex++] = undefined
      run(effect)
    }
  } finally {
    while (notifyIndex < queuedLength) {
      const effect = queued[notifyIndex]!
      queued[notifyIndex++] = undefined
      effect.flags |= ReactiveFlags.Watching | ReactiveFlags.Recursed
    }
    notifyIndex = 0
    queuedLength = 0
  }
}

function computedOper<T>(this: ComputedNode<T>): T {
  const flags = this.flags
  if (
    flags & ReactiveFlags.Dirty ||
    (flags & ReactiveFlags.Pending &&
      (checkDirty(this.deps!, this) ||
        ((this.flags = flags & ~ReactiveFlags.Pending), false)))
  ) {
    if (updateComputed(this)) {
      const subs = this.subs
      if (subs !== undefined) {
        shallowPropagate(subs)
      }
    }
  } else if (!flags) {
    this.flags = ReactiveFlags.Mutable | ReactiveFlags.RecursedCheck
    const prevSub = setActiveSub(this)
    try {
      this.value = this.getter()
    } finally {
      activeSub = prevSub
      this.flags &= ~ReactiveFlags.RecursedCheck
    }
  }
  const sub = activeSub
  if (sub !== undefined) {
    link(this, sub, cycle)
  }
  return this.value!
}

function signalOper<T>(this: SignalNode<T>, ...value: [T]): T | void {
  if (value.length) {
    if (this.pendingValue !== (this.pendingValue = value[0])) {
      this.flags = ReactiveFlags.Mutable | ReactiveFlags.Dirty
      const subs = this.subs
      if (subs !== undefined) {
        propagate(subs)
        if (!batchDepth) {
          flush()
        }
      }
    }
  } else {
    if (this.flags & ReactiveFlags.Dirty) {
      if (updateSignal(this)) {
        const subs = this.subs
        if (subs !== undefined) {
          shallowPropagate(subs)
        }
      }
    }
    let sub = activeSub
    while (sub !== undefined) {
      if (sub.flags & (ReactiveFlags.Mutable | ReactiveFlags.Watching)) {
        link(this, sub, cycle)
        break
      }
      sub = sub.subs?.sub
    }
    return this.currentValue
  }
}

function effectOper(this: EffectNode): void {
  effectScopeOper.call(this)
}

function effectScopeOper(this: ReactiveNode): void {
  this.depsTail = undefined
  this.flags = ReactiveFlags.None
  purgeDeps(this)
  const sub = this.subs
  if (sub !== undefined) {
    unlink(sub)
  }
}

function purgeDeps(sub: ReactiveNode) {
  const depsTail = sub.depsTail
  let dep = depsTail !== undefined ? depsTail.nextDep : sub.deps
  while (dep !== undefined) {
    dep = unlink(dep, sub)
  }
}
