import type * as Cause from "../../Cause.js"
import type * as Deferred from "../../Deferred.js"
import * as Duration from "../../Duration.js"
import type * as Effect from "../../Effect.js"
import * as Effectable from "../../Effectable.js"
import * as Equal from "../../Equal.js"
import type { Equivalence } from "../../Equivalence.js"
import * as Exit from "../../Exit.js"
import type * as Fiber from "../../Fiber.js"
import * as FiberId from "../../FiberId.js"
import type * as FiberRefsPatch from "../../FiberRefsPatch.js"
import type { LazyArg } from "../../Function.js"
import { dual, pipe } from "../../Function.js"
import * as Hash from "../../Hash.js"
import * as MutableHashMap from "../../MutableHashMap.js"
import * as Option from "../../Option.js"
import { pipeArguments } from "../../Pipeable.js"
import * as Predicate from "../../Predicate.js"
import * as Readable from "../../Readable.js"
import type * as Ref from "../../Ref.js"
import type * as Schedule from "../../Schedule.js"
import { currentScheduler } from "../../Scheduler.js"
import type * as Scope from "../../Scope.js"
import type * as Supervisor from "../../Supervisor.js"
import type * as Synchronized from "../../SynchronizedRef.js"
import type * as Types from "../../Types.js"
import * as internalCause from "../cause.js"
import * as effect from "../core-effect.js"
import * as core from "../core.js"
import * as internalFiber from "../fiber.js"
import * as fiberRuntime from "../fiberRuntime.js"
import { globalScope } from "../fiberScope.js"
import * as internalRef from "../ref.js"
import * as schedule_ from "../schedule.js"
import * as supervisor from "../supervisor.js"

/** @internal */
class Semaphore {
  public waiters = new Set<() => void>()
  public taken = 0

  constructor(readonly permits: number) {}

  get free() {
    return this.permits - this.taken
  }

  readonly take = (n: number): Effect.Effect<number> =>
    core.asyncInterrupt<number>((resume) => {
      if (this.free < n) {
        const observer = () => {
          if (this.free < n) {
            return
          }
          this.waiters.delete(observer)
          this.taken += n
          resume(core.succeed(n))
        }
        this.waiters.add(observer)
        return core.sync(() => {
          this.waiters.delete(observer)
        })
      }
      this.taken += n
      return resume(core.succeed(n))
    })

  readonly updateTaken = (f: (n: number) => number): Effect.Effect<number> =>
    core.withFiberRuntime((fiber) => {
      this.taken = f(this.taken)
      if (this.waiters.size > 0) {
        fiber.getFiberRef(currentScheduler).scheduleTask(() => {
          const iter = this.waiters.values()
          let item = iter.next()
          while (item.done === false && this.free > 0) {
            item.value()
            item = iter.next()
          }
        }, fiber.getFiberRef(core.currentSchedulingPriority))
      }
      return core.succeed(this.free)
    })

  readonly release = (n: number): Effect.Effect<number> => this.updateTaken((taken) => taken - n)

  readonly releaseAll: Effect.Effect<number> = this.updateTaken((_) => 0)

  readonly withPermits = (n: number) => <A, E, R>(self: Effect.Effect<A, E, R>) =>
    core.uninterruptibleMask((restore) =>
      core.flatMap(
        restore(this.take(n)),
        (permits) => fiberRuntime.ensuring(restore(self), this.release(permits))
      )
    )

  readonly withPermitsIfAvailable = (n: number) => <A, E, R>(self: Effect.Effect<A, E, R>) =>
    core.uninterruptibleMask((restore) =>
      core.suspend(() => {
        if (this.free < n) {
          return effect.succeedNone
        }
        this.taken += n
        return fiberRuntime.ensuring(restore(effect.asSome(self)), this.release(n))
      })
    )
}

/** @internal */
export const unsafeMakeSemaphore = (permits: number): Semaphore => new Semaphore(permits)

/** @internal */
export const makeSemaphore = (permits: number) => core.sync(() => unsafeMakeSemaphore(permits))

class Latch extends Effectable.Class<void> implements Effect.Latch {
  waiters: Array<(_: Effect.Effect<void>) => void> = []
  scheduled = false
  constructor(private isOpen: boolean) {
    super()
  }

  commit() {
    return this.await
  }

  private unsafeSchedule(fiber: Fiber.RuntimeFiber<void>) {
    if (this.scheduled || this.waiters.length === 0) {
      return core.void
    }
    this.scheduled = true
    fiber.currentScheduler.scheduleTask(this.flushWaiters, fiber.getFiberRef(core.currentSchedulingPriority))
    return core.void
  }
  private flushWaiters = () => {
    this.scheduled = false
    const waiters = this.waiters
    this.waiters = []
    for (let i = 0; i < waiters.length; i++) {
      waiters[i](core.exitVoid)
    }
  }

  open = core.withFiberRuntime<void>((fiber) => {
    if (this.isOpen) {
      return core.void
    }
    this.isOpen = true
    return this.unsafeSchedule(fiber)
  })
  release = core.withFiberRuntime<void>((fiber) => {
    if (this.isOpen) {
      return core.void
    }
    return this.unsafeSchedule(fiber)
  })
  await = core.asyncInterrupt<void>((resume) => {
    if (this.isOpen) {
      return resume(core.void)
    }
    this.waiters.push(resume)
    return core.sync(() => {
      const index = this.waiters.indexOf(resume)
      if (index !== -1) {
        this.waiters.splice(index, 1)
      }
    })
  })
  unsafeClose() {
    this.isOpen = false
  }
  close = core.sync(() => {
    this.isOpen = false
  })
  whenOpen = <A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> => {
    return core.zipRight(this.await, self)
  }
}

/** @internal */
export const unsafeMakeLatch = (open?: boolean | undefined): Effect.Latch => new Latch(open ?? false)

/** @internal */
export const makeLatch = (open?: boolean | undefined) => core.sync(() => unsafeMakeLatch(open))

/** @internal */
export const awaitAllChildren = <A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
  ensuringChildren(self, fiberRuntime.fiberAwaitAll)

/** @internal */
export const cached: {
  (
    timeToLive: Duration.DurationInput
  ): <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<Effect.Effect<A, E>, never, R>
  <A, E, R>(
    self: Effect.Effect<A, E, R>,
    timeToLive: Duration.DurationInput
  ): Effect.Effect<Effect.Effect<A, E>, never, R>
} = dual(
  2,
  <A, E, R>(
    self: Effect.Effect<A, E, R>,
    timeToLive: Duration.DurationInput
  ): Effect.Effect<Effect.Effect<A, E>, never, R> =>
    core.map(cachedInvalidateWithTTL(self, timeToLive), (tuple) => tuple[0])
)

/** @internal */
export const cachedInvalidateWithTTL: {
  (timeToLive: Duration.DurationInput): <A, E, R>(
    self: Effect.Effect<A, E, R>
  ) => Effect.Effect<[Effect.Effect<A, E>, Effect.Effect<void>], never, R>
  <A, E, R>(
    self: Effect.Effect<A, E, R>,
    timeToLive: Duration.DurationInput
  ): Effect.Effect<[Effect.Effect<A, E>, Effect.Effect<void>], never, R>
} = dual(
  2,
  <A, E, R>(
    self: Effect.Effect<A, E, R>,
    timeToLive: Duration.DurationInput
  ): Effect.Effect<[Effect.Effect<A, E>, Effect.Effect<void>], never, R> => {
    const duration = Duration.decode(timeToLive)
    return core.flatMap(
      core.context<R>(),
      (env) =>
        core.map(
          makeSynchronized<Option.Option<readonly [number, Deferred.Deferred<A, E>]>>(Option.none()),
          (cache) =>
            [
              core.provideContext(getCachedValue(self, duration, cache), env),
              invalidateCache(cache)
            ] as [Effect.Effect<A, E>, Effect.Effect<void>]
        )
    )
  }
)

/** @internal */
const computeCachedValue = <A, E, R>(
  self: Effect.Effect<A, E, R>,
  timeToLive: Duration.DurationInput,
  start: number
): Effect.Effect<Option.Option<[number, Deferred.Deferred<A, E>]>, never, R> => {
  const timeToLiveMillis = Duration.toMillis(Duration.decode(timeToLive))
  return pipe(
    core.deferredMake<A, E>(),
    core.tap((deferred) => core.intoDeferred(self, deferred)),
    core.map((deferred) => Option.some([start + timeToLiveMillis, deferred]))
  )
}

/** @internal */
const getCachedValue = <A, E, R>(
  self: Effect.Effect<A, E, R>,
  timeToLive: Duration.DurationInput,
  cache: Synchronized.SynchronizedRef<Option.Option<readonly [number, Deferred.Deferred<A, E>]>>
): Effect.Effect<A, E, R> =>
  core.uninterruptibleMask((restore) =>
    pipe(
      effect.clockWith((clock) => clock.currentTimeMillis),
      core.flatMap((time) =>
        updateSomeAndGetEffectSynchronized(cache, (option) => {
          switch (option._tag) {
            case "None": {
              return Option.some(computeCachedValue(self, timeToLive, time))
            }
            case "Some": {
              const [end] = option.value
              return end - time <= 0
                ? Option.some(computeCachedValue(self, timeToLive, time))
                : Option.none()
            }
          }
        })
      ),
      core.flatMap((option) =>
        Option.isNone(option) ?
          core.dieMessage(
            "BUG: Effect.cachedInvalidate - please report an issue at https://github.com/Effect-TS/effect/issues"
          ) :
          restore(core.deferredAwait(option.value[1]))
      )
    )
  )

/** @internal */
const invalidateCache = <A, E>(
  cache: Synchronized.SynchronizedRef<Option.Option<readonly [number, Deferred.Deferred<A, E>]>>
): Effect.Effect<void> => internalRef.set(cache, Option.none())

/** @internal */
export const ensuringChild = dual<
  <X, R2>(
    f: (fiber: Fiber.Fiber<ReadonlyArray<unknown>, any>) => Effect.Effect<X, never, R2>
  ) => <A, E, R>(
    self: Effect.Effect<A, E, R>
  ) => Effect.Effect<A, E, R | R2>,
  <A, E, R, X, R2>(
    self: Effect.Effect<A, E, R>,
    f: (fiber: Fiber.Fiber<ReadonlyArray<unknown>, any>) => Effect.Effect<X, never, R2>
  ) => Effect.Effect<A, E, R | R2>
>(2, (self, f) => ensuringChildren(self, (children) => f(fiberRuntime.fiberAll(children))))

/** @internal */
export const ensuringChildren = dual<
  <X, R2>(
    children: (fibers: ReadonlyArray<Fiber.RuntimeFiber<any, any>>) => Effect.Effect<X, never, R2>
  ) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R | R2>,
  <A, E, R, X, R2>(
    self: Effect.Effect<A, E, R>,
    children: (fibers: ReadonlyArray<Fiber.RuntimeFiber<any, any>>) => Effect.Effect<X, never, R2>
  ) => Effect.Effect<A, E, R | R2>
>(2, (self, children) =>
  core.flatMap(supervisor.track, (supervisor) =>
    pipe(
      supervised(self, supervisor),
      fiberRuntime.ensuring(core.flatMap(supervisor.value, children))
    )))

/** @internal */
export const forkAll: {
  (
    options?: {
      readonly discard?: false | undefined
    }
  ): <Eff extends Effect.Effect<any, any, any>>(
    effects: Iterable<Eff>
  ) => Effect.Effect<
    Fiber.Fiber<Array<Effect.Effect.Success<Eff>>, Effect.Effect.Error<Eff>>,
    never,
    Effect.Effect.Context<Eff>
  >
  (options: {
    readonly discard: true
  }): <Eff extends Effect.Effect<any, any, any>>(
    effects: Iterable<Eff>
  ) => Effect.Effect<void, never, Effect.Effect.Context<Eff>>
  <Eff extends Effect.Effect<any, any, any>>(
    effects: Iterable<Eff>,
    options?: {
      readonly discard?: false | undefined
    }
  ): Effect.Effect<
    Fiber.Fiber<Array<Effect.Effect.Success<Eff>>, Effect.Effect.Error<Eff>>,
    never,
    Effect.Effect.Context<Eff>
  >
  <Eff extends Effect.Effect<any, any, any>>(effects: Iterable<Eff>, options: {
    readonly discard: true
  }): Effect.Effect<void, never, Effect.Effect.Context<Eff>>
} = dual((args) => Predicate.isIterable(args[0]), <A, E, R>(effects: Iterable<Effect.Effect<A, E, R>>, options: {
  readonly discard: true
}): Effect.Effect<void, never, R> =>
  options?.discard ?
    core.forEachSequentialDiscard(effects, fiberRuntime.fork) :
    core.map(core.forEachSequential(effects, fiberRuntime.fork), fiberRuntime.fiberAll))

/** @internal */
export const forkIn = dual<
  (scope: Scope.Scope) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<Fiber.RuntimeFiber<A, E>, never, R>,
  <A, E, R>(self: Effect.Effect<A, E, R>, scope: Scope.Scope) => Effect.Effect<Fiber.RuntimeFiber<A, E>, never, R>
>(
  2,
  (self, scope) =>
    core.withFiberRuntime((parent, parentStatus) => {
      const scopeImpl = scope as fiberRuntime.ScopeImpl
      const fiber = fiberRuntime.unsafeFork(self, parent, parentStatus.runtimeFlags, globalScope)
      if (scopeImpl.state._tag === "Open") {
        const finalizer = () =>
          core.fiberIdWith((fiberId) =>
            Equal.equals(fiberId, fiber.id()) ?
              core.void :
              core.asVoid(core.interruptFiber(fiber))
          )
        const key = {}
        scopeImpl.state.finalizers.set(key, finalizer)
        fiber.addObserver(() => {
          if (scopeImpl.state._tag === "Closed") return
          scopeImpl.state.finalizers.delete(key)
        })
      } else {
        fiber.unsafeInterruptAsFork(parent.id())
      }
      return core.succeed(fiber)
    })
)

/** @internal */
export const forkScoped = <A, E, R>(
  self: Effect.Effect<A, E, R>
): Effect.Effect<Fiber.RuntimeFiber<A, E>, never, R | Scope.Scope> =>
  fiberRuntime.scopeWith((scope) => forkIn(self, scope))

/** @internal */
export const fromFiber = <A, E>(fiber: Fiber.Fiber<A, E>): Effect.Effect<A, E> => internalFiber.join(fiber)

/** @internal */
export const fromFiberEffect = <A, E, R>(fiber: Effect.Effect<Fiber.Fiber<A, E>, E, R>): Effect.Effect<A, E, R> =>
  core.suspend(() => core.flatMap(fiber, internalFiber.join))

const memoKeySymbol = Symbol.for("effect/Effect/memoizeFunction.key")

class Key<in out A> implements Equal.Equal {
  [memoKeySymbol] = memoKeySymbol
  constructor(readonly a: A, readonly eq?: Equivalence<A>) {}
  [Equal.symbol](that: Equal.Equal) {
    if (Predicate.hasProperty(that, memoKeySymbol)) {
      if (this.eq) {
        return this.eq(this.a, (that as unknown as Key<A>).a)
      } else {
        return Equal.equals(this.a, (that as unknown as Key<A>).a)
      }
    }
    return false
  }
  [Hash.symbol]() {
    return this.eq ? 0 : Hash.cached(this, Hash.hash(this.a))
  }
}

/** @internal */
export const cachedFunction = <A, B, E, R>(
  f: (a: A) => Effect.Effect<B, E, R>,
  eq?: Equivalence<A>
): Effect.Effect<(a: A) => Effect.Effect<B, E, R>> => {
  return pipe(
    core.sync(() => MutableHashMap.empty<Key<A>, Deferred.Deferred<readonly [FiberRefsPatch.FiberRefsPatch, B], E>>()),
    core.flatMap(makeSynchronized),
    core.map((ref) => (a: A) =>
      pipe(
        ref.modifyEffect((map) => {
          const result = pipe(map, MutableHashMap.get(new Key(a, eq)))
          if (Option.isNone(result)) {
            return pipe(
              core.deferredMake<readonly [FiberRefsPatch.FiberRefsPatch, B], E>(),
              core.tap((deferred) =>
                pipe(
                  effect.diffFiberRefs(f(a)),
                  core.intoDeferred(deferred),
                  fiberRuntime.fork
                )
              ),
              core.map((deferred) => [deferred, pipe(map, MutableHashMap.set(new Key(a, eq), deferred))] as const)
            )
          }
          return core.succeed([result.value, map] as const)
        }),
        core.flatMap(core.deferredAwait),
        core.flatMap(([patch, b]) => pipe(effect.patchFiberRefs(patch), core.as(b)))
      )
    )
  )
}

/** @internal */
export const raceFirst = dual<
  <A2, E2, R2>(
    that: Effect.Effect<A2, E2, R2>
  ) => <A, E, R>(
    self: Effect.Effect<A, E, R>
  ) => Effect.Effect<A2 | A, E2 | E, R | R2>,
  <A, E, R, A2, E2, R2>(
    self: Effect.Effect<A, E, R>,
    that: Effect.Effect<A2, E2, R2>
  ) => Effect.Effect<A2 | A, E2 | E, R | R2>
>(2, <A, E, R, A2, E2, R2>(
  self: Effect.Effect<A, E, R>,
  that: Effect.Effect<A2, E2, R2>
) =>
  pipe(
    core.exit(self),
    fiberRuntime.race(core.exit(that)),
    (effect: Effect.Effect<Exit.Exit<A | A2, E | E2>, never, R | R2>) => core.flatten(effect)
  ))

/** @internal */
export const scheduleForked = dual<
  <Out, R2>(
    schedule: Schedule.Schedule<Out, unknown, R2>
  ) => <A, E, R>(
    self: Effect.Effect<A, E, R>
  ) => Effect.Effect<Fiber.RuntimeFiber<Out, E>, never, R | R2 | Scope.Scope>,
  <A, E, R, Out, R2>(
    self: Effect.Effect<A, E, R>,
    schedule: Schedule.Schedule<Out, unknown, R2>
  ) => Effect.Effect<Fiber.RuntimeFiber<Out, E>, never, R | R2 | Scope.Scope>
>(2, (self, schedule) => pipe(self, schedule_.schedule_Effect(schedule), forkScoped))

/** @internal */
export const supervised = dual<
  <X>(supervisor: Supervisor.Supervisor<X>) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>,
  <A, E, R, X>(self: Effect.Effect<A, E, R>, supervisor: Supervisor.Supervisor<X>) => Effect.Effect<A, E, R>
>(2, (self, supervisor) => {
  const supervise = core.fiberRefLocallyWith(fiberRuntime.currentSupervisor, (s) => s.zip(supervisor))
  return supervise(self)
})

/** @internal */
export const timeout = dual<
  (
    duration: Duration.DurationInput
  ) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E | Cause.TimeoutException, R>,
  <A, E, R>(
    self: Effect.Effect<A, E, R>,
    duration: Duration.DurationInput
  ) => Effect.Effect<A, E | Cause.TimeoutException, R>
>(2, (self, duration) =>
  timeoutFail(self, {
    onTimeout: () => core.timeoutExceptionFromDuration(duration),
    duration
  }))

/** @internal */
export const timeoutFail = dual<
  <E1>(
    options: {
      readonly onTimeout: LazyArg<E1>
      readonly duration: Duration.DurationInput
    }
  ) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E | E1, R>,
  <A, E, R, E1>(
    self: Effect.Effect<A, E, R>,
    options: {
      readonly onTimeout: LazyArg<E1>
      readonly duration: Duration.DurationInput
    }
  ) => Effect.Effect<A, E | E1, R>
>(2, (self, { duration, onTimeout }) =>
  core.flatten(timeoutTo(self, {
    onTimeout: () => core.failSync(onTimeout),
    onSuccess: core.succeed,
    duration
  })))

/** @internal */
export const timeoutFailCause = dual<
  <E1>(
    options: {
      readonly onTimeout: LazyArg<Cause.Cause<E1>>
      readonly duration: Duration.DurationInput
    }
  ) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E | E1, R>,
  <A, E, R, E1>(
    self: Effect.Effect<A, E, R>,
    options: {
      readonly onTimeout: LazyArg<Cause.Cause<E1>>
      readonly duration: Duration.DurationInput
    }
  ) => Effect.Effect<A, E | E1, R>
>(2, (self, { duration, onTimeout }) =>
  core.flatten(timeoutTo(self, {
    onTimeout: () => core.failCauseSync(onTimeout),
    onSuccess: core.succeed,
    duration
  })))

/** @internal */
export const timeoutOption = dual<
  (
    duration: Duration.DurationInput
  ) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<Option.Option<A>, E, R>,
  <A, E, R>(
    self: Effect.Effect<A, E, R>,
    duration: Duration.DurationInput
  ) => Effect.Effect<Option.Option<A>, E, R>
>(2, (self, duration) =>
  timeoutTo(self, {
    duration,
    onSuccess: Option.some,
    onTimeout: Option.none
  }))

/** @internal */
export const timeoutTo = dual<
  <A, B, B1>(
    options: {
      readonly onTimeout: LazyArg<B1>
      readonly onSuccess: (a: A) => B
      readonly duration: Duration.DurationInput
    }
  ) => <E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<B | B1, E, R>,
  <A, E, R, B1, B>(
    self: Effect.Effect<A, E, R>,
    options: {
      readonly onTimeout: LazyArg<B1>
      readonly onSuccess: (a: A) => B
      readonly duration: Duration.DurationInput
    }
  ) => Effect.Effect<B | B1, E, R>
>(
  2,
  (self, { duration, onSuccess, onTimeout }) =>
    core.fiberIdWith((parentFiberId) =>
      core.uninterruptibleMask((restore) =>
        fiberRuntime.raceFibersWith(
          restore(self),
          core.interruptible(effect.sleep(duration)),
          {
            onSelfWin: (winner, loser) =>
              core.flatMap(
                winner.await,
                (exit) => {
                  if (exit._tag === "Success") {
                    return core.flatMap(
                      winner.inheritAll,
                      () =>
                        core.as(
                          core.interruptAsFiber(loser, parentFiberId),
                          onSuccess(exit.value)
                        )
                    )
                  } else {
                    return core.flatMap(
                      core.interruptAsFiber(loser, parentFiberId),
                      () => core.exitFailCause(exit.cause)
                    )
                  }
                }
              ),
            onOtherWin: (winner, loser) =>
              core.flatMap(
                winner.await,
                (exit) => {
                  if (exit._tag === "Success") {
                    return core.flatMap(
                      winner.inheritAll,
                      () =>
                        core.as(
                          core.interruptAsFiber(loser, parentFiberId),
                          onTimeout()
                        )
                    )
                  } else {
                    return core.flatMap(
                      core.interruptAsFiber(loser, parentFiberId),
                      () => core.exitFailCause(exit.cause)
                    )
                  }
                }
              ),
            otherScope: globalScope
          }
        )
      )
    )
)

// circular with Synchronized

/** @internal */
const SynchronizedSymbolKey = "effect/Ref/SynchronizedRef"

/** @internal */
export const SynchronizedTypeId: Synchronized.SynchronizedRefTypeId = Symbol.for(
  SynchronizedSymbolKey
) as Synchronized.SynchronizedRefTypeId

/** @internal */
export const synchronizedVariance = {
  /* c8 ignore next */
  _A: (_: any) => _
}

/** @internal */
class SynchronizedImpl<in out A> extends Effectable.Class<A> implements Synchronized.SynchronizedRef<A> {
  readonly [SynchronizedTypeId] = synchronizedVariance
  readonly [internalRef.RefTypeId] = internalRef.refVariance
  readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId
  constructor(
    readonly ref: Ref.Ref<A>,
    readonly withLock: <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>
  ) {
    super()
    this.get = internalRef.get(this.ref)
  }
  readonly get: Effect.Effect<A>
  commit() {
    return this.get
  }
  modify<B>(f: (a: A) => readonly [B, A]): Effect.Effect<B> {
    return this.modifyEffect((a) => core.succeed(f(a)))
  }
  modifyEffect<B, E, R>(f: (a: A) => Effect.Effect<readonly [B, A], E, R>): Effect.Effect<B, E, R> {
    return this.withLock(
      pipe(
        core.flatMap(internalRef.get(this.ref), f),
        core.flatMap(([b, a]) => core.as(internalRef.set(this.ref, a), b))
      )
    )
  }
}

/** @internal */
export const makeSynchronized = <A>(value: A): Effect.Effect<Synchronized.SynchronizedRef<A>> =>
  core.sync(() => unsafeMakeSynchronized(value))

/** @internal */
export const unsafeMakeSynchronized = <A>(value: A): Synchronized.SynchronizedRef<A> => {
  const ref = internalRef.unsafeMake(value)
  const sem = unsafeMakeSemaphore(1)
  return new SynchronizedImpl(ref, sem.withPermits(1))
}

/** @internal */
export const updateSomeAndGetEffectSynchronized = dual<
  <A, R, E>(
    pf: (a: A) => Option.Option<Effect.Effect<A, E, R>>
  ) => (self: Synchronized.SynchronizedRef<A>) => Effect.Effect<A, E, R>,
  <A, R, E>(
    self: Synchronized.SynchronizedRef<A>,
    pf: (a: A) => Option.Option<Effect.Effect<A, E, R>>
  ) => Effect.Effect<A, E, R>
>(2, (self, pf) =>
  self.modifyEffect((value) => {
    const result = pf(value)
    switch (result._tag) {
      case "None": {
        return core.succeed([value, value] as const)
      }
      case "Some": {
        return core.map(result.value, (a) => [a, a] as const)
      }
    }
  }))

// circular with Fiber

/** @internal */
export const zipFiber = dual<
  <A2, E2>(that: Fiber.Fiber<A2, E2>) => <A, E>(self: Fiber.Fiber<A, E>) => Fiber.Fiber<[A, A2], E | E2>,
  <A, E, A2, E2>(self: Fiber.Fiber<A, E>, that: Fiber.Fiber<A2, E2>) => Fiber.Fiber<[A, A2], E | E2>
>(2, (self, that) => zipWithFiber(self, that, (a, b) => [a, b]))

/** @internal */
export const zipLeftFiber = dual<
  <A2, E2>(that: Fiber.Fiber<A2, E2>) => <A, E>(self: Fiber.Fiber<A, E>) => Fiber.Fiber<A, E | E2>,
  <A, E, A2, E2>(self: Fiber.Fiber<A, E>, that: Fiber.Fiber<A2, E2>) => Fiber.Fiber<A, E | E2>
>(2, (self, that) => zipWithFiber(self, that, (a, _) => a))

/** @internal */
export const zipRightFiber = dual<
  <A2, E2>(that: Fiber.Fiber<A2, E2>) => <A, E>(self: Fiber.Fiber<A, E>) => Fiber.Fiber<A2, E | E2>,
  <A, E, A2, E2>(self: Fiber.Fiber<A, E>, that: Fiber.Fiber<A2, E2>) => Fiber.Fiber<A2, E | E2>
>(2, (self, that) => zipWithFiber(self, that, (_, b) => b))

/** @internal */
export const zipWithFiber = dual<
  <B, E2, A, C>(
    that: Fiber.Fiber<B, E2>,
    f: (a: A, b: B) => C
  ) => <E>(self: Fiber.Fiber<A, E>) => Fiber.Fiber<C, E | E2>,
  <A, E, B, E2, C>(
    self: Fiber.Fiber<A, E>,
    that: Fiber.Fiber<B, E2>,
    f: (a: A, b: B) => C
  ) => Fiber.Fiber<C, E | E2>
>(3, (self, that, f) => ({
  ...Effectable.CommitPrototype,
  commit() {
    return internalFiber.join(this)
  },
  [internalFiber.FiberTypeId]: internalFiber.fiberVariance,
  id: () => pipe(self.id(), FiberId.getOrElse(that.id())),
  await: pipe(
    self.await,
    core.flatten,
    fiberRuntime.zipWithOptions(core.flatten(that.await), f, { concurrent: true }),
    core.exit
  ),
  children: self.children,
  inheritAll: core.zipRight(
    that.inheritAll,
    self.inheritAll
  ),
  poll: core.zipWith(
    self.poll,
    that.poll,
    (optionA, optionB) =>
      pipe(
        optionA,
        Option.flatMap((exitA) =>
          pipe(
            optionB,
            Option.map((exitB) =>
              Exit.zipWith(exitA, exitB, {
                onSuccess: f,
                onFailure: internalCause.parallel
              })
            )
          )
        )
      )
  ),
  interruptAsFork: (id) =>
    core.zipRight(
      self.interruptAsFork(id),
      that.interruptAsFork(id)
    ),
  pipe() {
    return pipeArguments(this, arguments)
  }
}))

/* @internal */
export const bindAll: {
  <
    A extends object,
    X extends Record<string, Effect.Effect<any, any, any>>,
    O extends {
      readonly concurrency?: Types.Concurrency | undefined
      readonly batching?: boolean | "inherit" | undefined
      readonly mode?: "default" | "validate" | "either" | undefined
      readonly concurrentFinalizers?: boolean | undefined
    }
  >(
    f: (a: A) => [Extract<keyof X, keyof A>] extends [never] ? X : `Duplicate keys`,
    options?: undefined | O
  ): <E1, R1>(
    self: Effect.Effect<A, E1, R1>
  ) => [Effect.All.ReturnObject<X, false, Effect.All.ExtractMode<O>>] extends
    [Effect.Effect<infer Success, infer Error, infer Context>] ? Effect.Effect<
      {
        [K in keyof A | keyof Success]: K extends keyof A ? A[K]
          : K extends keyof Success ? Success[K]
          : never
      },
      | E1
      | Error,
      R1 | Context
    >
    : never

  <
    A extends object,
    X extends Record<string, Effect.Effect<any, any, any>>,
    O extends {
      readonly concurrency?: Types.Concurrency | undefined
      readonly batching?: boolean | "inherit" | undefined
      readonly mode?: "default" | "validate" | "either" | undefined
      readonly concurrentFinalizers?: boolean | undefined
    },
    E1,
    R1
  >(
    self: Effect.Effect<A, E1, R1>,
    f: (a: A) => [Extract<keyof X, keyof A>] extends [never] ? X : `Duplicate keys`,
    options?: undefined | {
      readonly concurrency?: Types.Concurrency | undefined
      readonly batching?: boolean | "inherit" | undefined
      readonly mode?: "default" | "validate" | "either" | undefined
      readonly concurrentFinalizers?: boolean | undefined
    }
  ): [Effect.All.ReturnObject<X, false, Effect.All.ExtractMode<O>>] extends
    [Effect.Effect<infer Success, infer Error, infer Context>] ? Effect.Effect<
      {
        [K in keyof A | keyof Success]: K extends keyof A ? A[K]
          : K extends keyof Success ? Success[K]
          : never
      },
      | E1
      | Error,
      R1 | Context
    >
    : never
} = dual((args) => core.isEffect(args[0]), <
  A extends object,
  X extends Record<string, Effect.Effect<any, any, any>>,
  O extends {
    readonly concurrency?: Types.Concurrency | undefined
    readonly batching?: boolean | "inherit" | undefined
    readonly mode?: "default" | "validate" | "either" | undefined
    readonly concurrentFinalizers?: boolean | undefined
  },
  E1,
  R1
>(
  self: Effect.Effect<A, E1, R1>,
  f: (a: A) => X,
  options?: undefined | O
) =>
  core.flatMap(
    self,
    (a) =>
      (fiberRuntime.all(f(a), options) as Effect.All.ReturnObject<
        X,
        Effect.All.IsDiscard<O>,
        Effect.All.ExtractMode<O>
      >)
        .pipe(
          core.map((record) => Object.assign({}, a, record))
        )
  ))
