/**
 * The `UndefinedOr` module works with plain TypeScript values of type
 * `A | undefined` where `undefined` is the only absence marker. It is a small
 * alternative to wrapping values in `Option` when your domain already uses
 * `undefined` to mean "no value".
 *
 * **Mental model**
 *
 * - A defined value is the present branch.
 * - `undefined` is reserved for absence; the payload type `A` should not itself
 *   include `undefined`.
 * - Helpers such as {@link map} and {@link match} keep values unwrapped and use
 *   `undefined` directly instead of allocating tagged values.
 * - The fail-fast combiner and reducer helpers propagate `undefined` when a
 *   required value is missing.
 *
 * **Common tasks**
 *
 * - Transform a present value: {@link map}
 * - Branch on present versus absent: {@link match}
 * - Unwrap at a boundary: {@link getOrThrow}, {@link getOrThrowWith}
 * - Adapt throwing code to an optional result: {@link liftThrowable}
 * - Lift combination and reduction logic: {@link makeReducer},
 *   {@link makeCombinerFailFast}, {@link makeReducerFailFast}
 *
 * **Gotchas**
 *
 * - `A | undefined` cannot distinguish "missing" from "present and
 *   undefined". Use `Option` when that distinction matters.
 * - {@link liftThrowable} discards the thrown value and also returns
 *   `undefined` when the wrapped function throws.
 *
 * **Example** (Parsing optional input)
 *
 * ```ts
 * import { UndefinedOr } from "effect"
 *
 * const parseInteger = UndefinedOr.liftThrowable((input: string) => {
 *   const value = Number.parseInt(input, 10)
 *   if (Number.isNaN(value)) {
 *     throw new Error("not an integer")
 *   }
 *   return value
 * })
 *
 * console.log(UndefinedOr.map(parseInteger("42"), (n) => n + 1))
 * // 43
 *
 * console.log(
 *   UndefinedOr.match(parseInteger("abc"), {
 *     onUndefined: () => "missing",
 *     onDefined: (n) => `parsed ${n}`
 *   })
 * )
 * // "missing"
 * ```
 *
 * @since 4.0.0
 */
import * as Combiner from "./Combiner.ts"
import type { LazyArg } from "./Function.ts"
import { dual } from "./Function.ts"
import * as Reducer from "./Reducer.ts"

/**
 * Maps a defined value with `f`, or returns `undefined` unchanged.
 *
 * **When to use**
 *
 * Use to apply a pure transformation to an `A | undefined` value while
 * preserving `undefined` as absence.
 *
 * @see {@link match} when you need to handle the `undefined` case explicitly
 *
 * @category mapping
 * @since 4.0.0
 */
export const map: {
  /**
   * Maps a defined value with `f`, or returns `undefined` unchanged.
   *
   * **When to use**
   *
   * Use to apply a pure transformation to an `A | undefined` value while
   * preserving `undefined` as absence.
   *
   * @see {@link match} when you need to handle the `undefined` case explicitly
   *
   * @category mapping
   * @since 4.0.0
   */
  <A, B>(f: (a: A) => B): (self: A | undefined) => B | undefined
  /**
   * Maps a defined value with `f`, or returns `undefined` unchanged.
   *
   * **When to use**
   *
   * Use to apply a pure transformation to an `A | undefined` value while
   * preserving `undefined` as absence.
   *
   * @see {@link match} when you need to handle the `undefined` case explicitly
   *
   * @category mapping
   * @since 4.0.0
   */
  <A, B>(self: A | undefined, f: (a: A) => B): B | undefined
} = dual(2, (self, f) => (self === undefined ? undefined : f(self)))

/**
 * Pattern matches on an `A | undefined` value, running `onDefined` when the
 * value is present or evaluating `onUndefined` when the value is `undefined`.
 *
 * **When to use**
 *
 * Use when you need to turn an `A | undefined` into a non-optional result by
 * handling both the defined and undefined branches in one expression.
 *
 * @see {@link map} for transforming defined values while preserving `undefined`
 * @see {@link getOrThrowWith} for throwing when the value is `undefined` instead of returning a fallback branch
 *
 * @category pattern matching
 * @since 4.0.0
 */
export const match: {
  /**
   * Pattern matches on an `A | undefined` value, running `onDefined` when the
   * value is present or evaluating `onUndefined` when the value is `undefined`.
   *
   * **When to use**
   *
   * Use when you need to turn an `A | undefined` into a non-optional result by
   * handling both the defined and undefined branches in one expression.
   *
   * @see {@link map} for transforming defined values while preserving `undefined`
   * @see {@link getOrThrowWith} for throwing when the value is `undefined` instead of returning a fallback branch
   *
   * @category pattern matching
   * @since 4.0.0
   */
  <B, A, C = B>(
    options: {
      readonly onUndefined: LazyArg<B>
      readonly onDefined: (a: A) => C
    }
  ): (self: A | undefined) => B | C
  /**
   * Pattern matches on an `A | undefined` value, running `onDefined` when the
   * value is present or evaluating `onUndefined` when the value is `undefined`.
   *
   * **When to use**
   *
   * Use when you need to turn an `A | undefined` into a non-optional result by
   * handling both the defined and undefined branches in one expression.
   *
   * @see {@link map} for transforming defined values while preserving `undefined`
   * @see {@link getOrThrowWith} for throwing when the value is `undefined` instead of returning a fallback branch
   *
   * @category pattern matching
   * @since 4.0.0
   */
  <A, B, C = B>(
    self: A | undefined,
    options: {
      readonly onUndefined: LazyArg<B>
      readonly onDefined: (a: A) => C
    }
  ): B | C
} = dual(
  2,
  <A, B, C = B>(self: A | undefined, { onDefined, onUndefined }: {
    readonly onUndefined: LazyArg<B>
    readonly onDefined: (a: A) => C
  }): B | C => self === undefined ? onUndefined() : onDefined(self)
)

/**
 * Returns the defined value, or throws the value produced by `onUndefined`
 * when the input is `undefined`.
 *
 * **When to use**
 *
 * Use when fail-fast unwrapping of an `A | undefined` value is appropriate and
 * callers need to provide the thrown error for the undefined case.
 *
 * **Details**
 *
 * Defined values are returned unchanged. When the input is `undefined`,
 * `onUndefined` is called and its result is thrown.
 *
 * @see {@link getOrThrow} for the default-error sibling
 * @see {@link match} for handling defined and undefined cases without throwing
 *
 * @category getters
 * @since 4.0.0
 */
export const getOrThrowWith: {
  /**
   * Returns the defined value, or throws the value produced by `onUndefined`
   * when the input is `undefined`.
   *
   * **When to use**
   *
   * Use when fail-fast unwrapping of an `A | undefined` value is appropriate and
   * callers need to provide the thrown error for the undefined case.
   *
   * **Details**
   *
   * Defined values are returned unchanged. When the input is `undefined`,
   * `onUndefined` is called and its result is thrown.
   *
   * @see {@link getOrThrow} for the default-error sibling
   * @see {@link match} for handling defined and undefined cases without throwing
   *
   * @category getters
   * @since 4.0.0
   */
  (onUndefined: () => unknown): <A>(self: A | undefined) => A
  /**
   * Returns the defined value, or throws the value produced by `onUndefined`
   * when the input is `undefined`.
   *
   * **When to use**
   *
   * Use when fail-fast unwrapping of an `A | undefined` value is appropriate and
   * callers need to provide the thrown error for the undefined case.
   *
   * **Details**
   *
   * Defined values are returned unchanged. When the input is `undefined`,
   * `onUndefined` is called and its result is thrown.
   *
   * @see {@link getOrThrow} for the default-error sibling
   * @see {@link match} for handling defined and undefined cases without throwing
   *
   * @category getters
   * @since 4.0.0
   */
  <A>(self: A | undefined, onUndefined: () => unknown): A
} = dual(2, <A>(self: A | undefined, onUndefined: () => unknown): A => {
  if (self !== undefined) {
    return self
  }
  throw onUndefined()
})

/**
 * Returns the defined value, or throws a default `Error` when the input is
 * `undefined`.
 *
 * **When to use**
 *
 * Use when a value should already be defined at this point and throwing a
 * generic missing-value `Error` is acceptable.
 *
 * **Details**
 *
 * Defined inputs are returned unchanged. `undefined` throws
 * `new Error("getOrThrow called on a undefined")`.
 *
 * @see {@link getOrThrowWith} for the sibling that lets callers choose the thrown value
 * @see {@link match} for handling defined and undefined cases without throwing
 *
 * @category getters
 * @since 4.0.0
 */
export const getOrThrow: <A>(self: A | undefined) => A = getOrThrowWith(() =>
  new Error("getOrThrow called on a undefined")
)

/**
 * Converts a throwing function into one that returns successful results
 * unchanged and returns `undefined` when the function throws.
 *
 * **When to use**
 *
 * Use to adapt exception-throwing functions when `undefined` is the absence
 * value you want to return for failures.
 *
 * **Gotchas**
 *
 * Thrown values are discarded. If the wrapped function can successfully return
 * `undefined`, that success is indistinguishable from a thrown failure.
 *
 * @category converting
 * @since 4.0.0
 */
export const liftThrowable = <A extends ReadonlyArray<unknown>, B>(
  f: (...a: A) => B
): (...a: A) => B | undefined =>
(...a) => {
  try {
    return f(...a)
  } catch {
    return undefined
  }
}

/**
 * Creates a `Reducer` for `UndefinedOr<A>` that prioritizes the first non-`undefined`
 * value and combines values when both operands are present.
 *
 * **When to use**
 *
 * Use to take the first available value like a fallback chain, combining values
 * only when both operands are present.
 *
 * **Details**
 *
 * - `undefined` + `undefined` -> `undefined`
 * - `a` + `undefined` -> `a` (first value wins)
 * - `undefined` + `b` -> `b` (second value wins)
 * - `a` + `b` -> `combiner.combine(a, b)`
 * - Initial value is `undefined`
 *
 * @category constructors
 * @since 4.0.0
 */
export function makeReducer<A>(combiner: Combiner.Combiner<A>): Reducer.Reducer<A | undefined> {
  return Reducer.make((self, that) => {
    if (self === undefined) return that
    if (that === undefined) return self
    return combiner.combine(self, that)
  }, undefined as A | undefined)
}

/**
 * Creates a `Combiner` for `A | undefined` that combines values only when both
 * operands are defined.
 *
 * **When to use**
 *
 * Use to lift a `Combiner` so any `undefined` operand makes the combined result
 * `undefined`.
 *
 * **Details**
 *
 * - `undefined` combined with any value returns `undefined`
 * - Any value combined with `undefined` returns `undefined`
 * - `a` combined with `b` returns `combiner.combine(a, b)`
 *
 * @see {@link makeReducerFailFast} if you have a `Reducer` and want to lift it
 * to `UndefinedOr` values.
 *
 * @category constructors
 * @since 4.0.0
 */
export function makeCombinerFailFast<A>(combiner: Combiner.Combiner<A>): Combiner.Combiner<A | undefined> {
  return Combiner.make((self, that) => {
    if (self === undefined || that === undefined) return undefined
    return combiner.combine(self, that)
  })
}

/**
 * Creates a `Reducer` for `A | undefined` by wrapping an existing reducer with
 * fail-fast semantics.
 *
 * **When to use**
 *
 * Use to wrap an existing `Reducer` so any `undefined` value aborts the entire
 * reduction result.
 *
 * **Details**
 *
 * - Initial value is the wrapped reducer's `initialValue`
 * - Combining two defined values delegates to the wrapped reducer
 * - If the accumulator or next value is `undefined`, the reduction returns `undefined`
 *
 * @see {@link makeCombinerFailFast} if you only have a `Combiner` and want to
 * lift it to `UndefinedOr` values.
 *
 * @category constructors
 * @since 4.0.0
 */
export function makeReducerFailFast<A>(reducer: Reducer.Reducer<A>): Reducer.Reducer<A | undefined> {
  const combine = makeCombinerFailFast(reducer).combine
  const initialValue = reducer.initialValue as A | undefined
  return Reducer.make(combine, initialValue, (collection) => {
    let out = initialValue
    for (const value of collection) {
      out = combine(out, value)
      if (out === undefined) return out
    }
    return out
  })
}
