import { ReadableStreamsChunks } from '@johngw/stream-common/Stream'
import { empty } from '@johngw/stream-common/Symbol'
import { ControllableSource } from '@johngw/stream/sources/ControllableSource'
import { SourceComposite } from '@johngw/stream/sources/SourceComposite'

/**
 * Combines the source Observable with other Observables to create an Observable
 * whose values are calculated from the latest values of each, only when the source emits.
 *
 * @group Transformers
 * @example
 * ```
 * --a-----b-------c---d---e--
 *
 * withLatestFrom(
 * -----1----2-3-4------------
 * )
 *
 * --------b1------c4--d4--e4-
 * ```
 */
export function withLatestFrom<T, RSs extends ReadableStream<unknown>[]>(
  ...inputs: RSs
): ReadableWritablePair<[T, ...ReadableStreamsChunks<RSs>], T> {
  const abortController = new AbortController()

  const isFilled = (arr: unknown[]): arr is ReadableStreamsChunks<RSs> =>
    arr.every((value) => value !== empty)

  const inputValues: unknown[] = inputs.map(() => empty)

  inputs.forEach((input, index) =>
    input
      .pipeTo(
        new WritableStream({
          abort(reason) {
            abortController.abort(reason)
          },
          write(chunk) {
            inputValues[index] = chunk
          },
        }),
        { signal: abortController.signal }
      )
      .catch(() => {
        // errors are handled in the stream object
      })
  )

  const controller = new ControllableSource<
    [T, ...ReadableStreamsChunks<RSs>]
  >()

  const readable = new ReadableStream<[T, ...ReadableStreamsChunks<RSs>]>(
    new SourceComposite([
      controller,
      {
        start,

        cancel(reason) {
          abortController.abort(reason)
        },
      },
    ])
  )

  const writable = new WritableStream<T>({
    start,

    write(chunk) {
      if (isFilled(inputValues)) controller.enqueue([chunk, ...inputValues])
    },

    abort(reason) {
      abortController.abort(reason)
    },

    close() {
      controller.close()
    },
  })

  return { readable, writable }

  function start(
    controller:
      | ReadableStreamDefaultController<[T, ...ReadableStreamsChunks<RSs>]>
      | WritableStreamDefaultController
  ) {
    const onAbort = () => controller.error(abortController.signal.reason)
    abortController.signal.addEventListener('abort', onAbort)
  }
}
