/**
 * Transactional references for coordinating mutable state with Effect
 * transactions. A `TxRef` stores a current value, but reads and writes inside
 * `Effect.tx` are recorded in a transaction journal and committed together only
 * when the outermost transaction succeeds.
 *
 * This is the basic building block behind the other transactional collections
 * in Effect. The module provides effectful and unsafe constructors plus the
 * core operations for reading, setting, updating, and modifying a transactional
 * value while returning a separate result.
 *
 * @since 4.0.0
 */
import * as Effect from "./Effect.ts"
import { dual } from "./Function.ts"
import { pipeArguments } from "./Pipeable.ts"
import type { Pipeable } from "./Pipeable.ts"
import type { NoInfer } from "./Types.ts"

const TypeId = "~effect/transactions/TxRef"

/**
 * TxRef is a transactional value, it can be read and modified within the body of a transaction.
 *
 * **When to use**
 *
 * Use to store mutable state that must be read and modified inside Effect
 * transactions.
 *
 * **Details**
 *
 * Accessed values are tracked by the transaction in order to detect conflicts and in order to
 * track changes, a transaction will retry whenever a conflict is detected or whenever the
 * transaction explicitely calls to `Effect.txRetry` and any of the accessed TxRef values
 * change.
 *
 * **Example** (Using a transactional reference)
 *
 * ```ts
 * import { Effect, TxRef } from "effect"
 *
 * const program = Effect.gen(function*() {
 *   // Create a transactional reference
 *   const ref: TxRef.TxRef<number> = yield* TxRef.make(0)
 *
 *   // Use within a transaction
 *   yield* Effect.tx(Effect.gen(function*() {
 *     const current = yield* TxRef.get(ref)
 *     yield* TxRef.set(ref, current + 1)
 *   }))
 *
 *   const final = yield* TxRef.get(ref)
 *   console.log(final) // 1
 * })
 * ```
 *
 * @category models
 * @since 4.0.0
 */
export interface TxRef<in out A> extends Pipeable {
  readonly [TypeId]: typeof TypeId

  version: number
  pending: Map<unknown, () => void>
  value: A
}

/**
 * Creates a new `TxRef` with the specified initial value.
 *
 * **When to use**
 *
 * Use to create a `TxRef` inside an `Effect` workflow.
 *
 * **Example** (Creating transactional references)
 *
 * ```ts
 * import { Effect, TxRef } from "effect"
 *
 * const program = Effect.gen(function*() {
 *   // Create a transactional reference with initial value
 *   const counter = yield* TxRef.make(0)
 *   const name = yield* TxRef.make("Alice")
 *
 *   // Use in transactions
 *   yield* Effect.tx(Effect.gen(function*() {
 *     yield* TxRef.set(counter, 42)
 *     yield* TxRef.set(name, "Bob")
 *   }))
 *
 *   console.log(yield* TxRef.get(counter)) // 42
 *   console.log(yield* TxRef.get(name)) // "Bob"
 * })
 * ```
 *
 * @category constructors
 * @since 2.0.0
 */
export const make = <A>(initial: A) => Effect.sync(() => makeUnsafe(initial))

/**
 * Creates a new `TxRef` synchronously with the specified initial value.
 *
 * **When to use**
 *
 * Use to construct a `TxRef` synchronously when it must be created outside an
 * `Effect` workflow.
 *
 * **Example** (Creating transactional references unsafely)
 *
 * ```ts
 * import { TxRef } from "effect"
 *
 * // Create a TxRef synchronously (unsafe - use make instead in Effect contexts)
 * const counter = TxRef.makeUnsafe(0)
 * const config = TxRef.makeUnsafe({ timeout: 5000, retries: 3 })
 *
 * // These are now ready to use in transactions
 * console.log(counter.value) // 0
 * console.log(config.value) // { timeout: 5000, retries: 3 }
 * ```
 *
 * @category constructors
 * @since 4.0.0
 */
export const makeUnsafe = <A>(initial: A): TxRef<A> => ({
  [TypeId]: TypeId,
  pending: new Map(),
  pipe() {
    return pipeArguments(this, arguments)
  },
  version: 0,
  value: initial
})

/**
 * Modifies the value of the `TxRef` using the provided function.
 *
 * **When to use**
 *
 * Use to update a `TxRef` and return a computed result from the same
 * transaction step.
 *
 * **Example** (Modifying transactional references)
 *
 * ```ts
 * import { Effect, TxRef } from "effect"
 *
 * const program = Effect.gen(function*() {
 *   const counter = yield* TxRef.make(0)
 *
 *   // Modify and return both old and new value
 *   const result = yield* TxRef.modify(counter, (current) => [current * 2, current + 1])
 *
 *   console.log(result) // 0 (the return value: current * 2)
 *   console.log(yield* TxRef.get(counter)) // 1 (the new value: current + 1)
 * })
 * ```
 *
 * @category combinators
 * @since 2.0.0
 */
export const modify: {
  <A, R>(f: (current: NoInfer<A>) => [returnValue: R, newValue: A]): (self: TxRef<A>) => Effect.Effect<R>
  <A, R>(self: TxRef<A>, f: (current: A) => [returnValue: R, newValue: A]): Effect.Effect<R>
} = dual(2, <A, R>(
  self: TxRef<A>,
  f: (current: A) => [returnValue: R, newValue: A]
): Effect.Effect<R> =>
  Effect.Transaction.pipe(
    Effect.flatMap((state) =>
      Effect.sync(() => {
        if (!state.journal.has(self)) {
          state.journal.set(self, { version: self.version, value: self.value })
        }
        const current = state.journal.get(self)!
        const [returnValue, next] = f(current.value)
        current.value = next
        return returnValue
      })
    ),
    Effect.tx
  ))

/**
 * Updates the value of the `TxRef` using the provided function.
 *
 * **When to use**
 *
 * Use to transform a `TxRef` when no result value is needed.
 *
 * **Example** (Updating transactional references)
 *
 * ```ts
 * import { Effect, TxRef } from "effect"
 *
 * const program = Effect.gen(function*() {
 *   const counter = yield* TxRef.make(10)
 *
 *   // Update the value using a function
 *   yield* Effect.tx(
 *     TxRef.update(counter, (current) => current * 2)
 *   )
 *
 *   console.log(yield* TxRef.get(counter)) // 20
 * })
 * ```
 *
 * @category combinators
 * @since 2.0.0
 */
export const update: {
  <A>(f: (current: NoInfer<A>) => A): (self: TxRef<A>) => Effect.Effect<void>
  <A>(self: TxRef<A>, f: (current: A) => A): Effect.Effect<void>
} = dual(2, <A>(
  self: TxRef<A>,
  f: (current: A) => A
): Effect.Effect<void> => modify(self, (current) => [void 0, f(current)]))

/**
 * Reads the current value of the `TxRef`.
 *
 * **When to use**
 *
 * Use to read the current value of a `TxRef`.
 *
 * **Example** (Reading transactional references)
 *
 * ```ts
 * import { Effect, TxRef } from "effect"
 *
 * const program = Effect.gen(function*() {
 *   const counter = yield* TxRef.make(42)
 *
 *   // Read the value within a transaction
 *   const value = yield* Effect.tx(
 *     TxRef.get(counter)
 *   )
 *
 *   console.log(value) // 42
 * })
 * ```
 *
 * @category combinators
 * @since 2.0.0
 */
export const get = <A>(self: TxRef<A>): Effect.Effect<A> => modify(self, (current) => [current, current])

/**
 * Sets the value of the `TxRef`.
 *
 * **When to use**
 *
 * Use to replace the value of a `TxRef`.
 *
 * **Example** (Setting transactional references)
 *
 * ```ts
 * import { Effect, TxRef } from "effect"
 *
 * const program = Effect.gen(function*() {
 *   const counter = yield* TxRef.make(0)
 *
 *   // Set a new value within a transaction
 *   yield* Effect.tx(
 *     TxRef.set(counter, 100)
 *   )
 *
 *   console.log(yield* TxRef.get(counter)) // 100
 * })
 * ```
 *
 * @category combinators
 * @since 2.0.0
 */
export const set: {
  <A>(value: A): (self: TxRef<A>) => Effect.Effect<void>
  <A>(self: TxRef<A>, value: A): Effect.Effect<void>
} = dual(2, <A>(
  self: TxRef<A>,
  value: A
): Effect.Effect<void> => update(self, () => value))
