import { Context } from "./context.js";
import { NOOP } from "./internals/noop.js";

/**
 * Represents a stack frame that teardown hooks can be pushed into.
 *
 * Note that this may be an array.
 */
interface TeardownFrame {
	push(hook: TeardownHook): void;
}

/**
 * A function that is called by a signal or batch when updated.
 */
interface NotifyHook {
	(): void;
}

interface SignalCore {
	/**
	 * A counter to identify if a signal has notified its observers.
	 */
	c: number;

	/**
	 * A set of hooks that are called in iteration order by this signal to notify its observers.
	 *
	 * This is cleared before hooks are called.
	 */
	h: Set<NotifyHook>;
}

/**
 * A function that is called by a signal when accessed.
 */
interface AccessHook {
	/**
	 * @param hooks See `Signal.#hooks`.
	 */
	(hooks: SignalCore): void;
}

const THROW_ON_LEAK: TeardownFrame = {
	push(_hook) {
		// Teardown hooks can not be registered outside of a lifecycle context.
		throw new Error("G5");
	},
};

const LEAK: TeardownFrame = {
	push() {},
};

/**
 * The current frame for capturing teardown hooks.
 */
let TEARDOWN_FRAME: TeardownFrame = THROW_ON_LEAK;

/**
 * The current access tracking frame.
 */
let ACCESS_FRAME: AccessHook | undefined;

/**
 * A function that is called to dispose something.
 */
export type TeardownHook = () => void;

/**
 * Internal utility to dispose the specified hooks in reverse order.
 */
function dispose(hooks: TeardownHook[]) {
	for (let i = hooks.length - 1; i >= 0; i--) {
		hooks[i]();
	}
}

/**
 * Run a function while capturing teardown hooks.
 *
 * + If an error is thrown by the specified function, teardown hooks are called in reverse registration order and the error is re-thrown.
 * + If an error is thrown by a teardown hook, remaining ones are not called and the error is re-thrown.
 *
 * @param fn The function to run.
 * @returns A function to run all captured teardown hooks in reverse registration order.
 */
export function capture(fn: () => void): TeardownHook {
	const parent = TEARDOWN_FRAME;
	const hooks: TeardownHook[] = [];
	try {
		TEARDOWN_FRAME = hooks;
		fn();
	} catch (error) {
		isolate(dispose, hooks);
		throw error;
	} finally {
		TEARDOWN_FRAME = parent;
	}
	return hooks.length === 0 ? NOOP : () => isolate(dispose, hooks);
}

/**
 * Run a function while capturing teardown hooks.
 *
 * + When disposed before the specified function finishes, teardown hooks are called in reverse registration order immediately after the function finishes.
 * + If an error is thrown by the specified function, teardown hooks are called in reverse registration order and the error is re-thrown.
 * + If an error is thrown by a teardown hook, remaining ones are not called and the error is re-thrown.
 *
 * @param fn The function to run.
 * @returns The function's return value.
 */
export function captureSelf<T>(fn: (dispose: TeardownHook) => T): T {
	let disposed = false;
	let dispose: TeardownHook = NOOP;
	let value: T;
	dispose = capture(() => {
		value = fn(() => {
			disposed = true;
			dispose();
		});
	});
	if (disposed) {
		dispose();
	}
	return value!;
}

/**
 * Run a function while intentionally leaking teardown hooks.
 *
 * @param fn The function to run.
 * @returns The function's return value.
 */
export function leak<T>(fn: () => T): T {
	const parent = TEARDOWN_FRAME;
	try {
		TEARDOWN_FRAME = LEAK;
		return fn();
	} finally {
		TEARDOWN_FRAME = parent;
	}
}

/**
 * Run a function and immediately call teardown hooks if it throws an error.
 *
 * + If an error is thrown, teardown hooks are immediately called in reverse registration order and the error is re-thrown.
 * + If no error is thrown, teardown hooks are registered in the outer context.
 *
 * @param fn The function to run.
 * @returns The function's return value.
 */
export function teardownOnError<T>(fn: () => T): T {
	let value!: T;
	teardown(capture(() => {
		value = fn();
	}));
	return value;
}

/**
 * Register a teardown hook to be called when the current lifecycle is disposed.
 *
 * This has no effect if teardown hooks are not captured in the current context.
 *
 * @param hook The hook to register. This may be called multiple times.
 */
export function teardown(hook: TeardownHook): void {
	return TEARDOWN_FRAME.push(hook);
}

/**
 * Run a function in isolation from the following side effect causing APIs:
 * + Teardown hooks are leaked. To isolate only teardown hooks, use {@link leak} instead.
 * + Signal accesses are not tracked and the default tracking behavior is restored. To only isolate signal accesses, use {@link untrack} instead.
 *
 * Note, that batches and contexts are not isolated.
 *
 * @param fn The function to run.
 * @param args The function arguments.
 * @returns The function's return value.
 */
export function isolate<F extends (...args: any) => any>(fn: F, ...args: Parameters<F>): ReturnType<F> {
	const parentTeardownFrame = TEARDOWN_FRAME;
	const parentAccessFrame = ACCESS_FRAME;
	try {
		TEARDOWN_FRAME = THROW_ON_LEAK;
		ACCESS_FRAME = undefined;
		return fn(...args);
	} finally {
		TEARDOWN_FRAME = parentTeardownFrame;
		ACCESS_FRAME = parentAccessFrame;
	}
}

/**
 * Internal test utility to check if lifecycle & access tracking is isolated in the current context.
 */
export function isIsolated() {
	return TEARDOWN_FRAME === THROW_ON_LEAK && ACCESS_FRAME === undefined;
}

/**
 * During a {@link batch}, notify hooks are added to this set instead of being called.
 *
 * Outside of a batch, this is undefined.
 */
let BATCH: Set<NotifyHook> | undefined;

/**
 * Internal function to call the specified hook.
 */
const _notify = (fn: NotifyHook) => {
	try {
		fn();
	} catch (error) {
		Promise.reject(error);
	}
};

/**
 * Internal function to add the specified hook to the current batch.
 */
const _queueBatch = (fn: NotifyHook) => BATCH!.add(fn);

/**
 * Represents the source that a signal has been derived from.
 *
 * When deriving a signal, the source should be passed via the signal constructor or shorthand.
 * This has no impact on how a signal behaves, but allows other APIs to locate metadata about a signal's source.
 *
 * @example
 * ```js
 * function trim(source: Signal<string>) {
 *   const input = $(source.value, source);
 *   ...
 *   return input;
 * }
 * ```
 */
export type SignalSource = Signal<unknown> | undefined;

/**
 * Represents a value that changes over time.
 */
export class Signal<T> {
	/**
	 * The current value without access tracking or observer notifications when set.
	 */
	inert: T;

	/**
	 * The signal core referenced by other observers.
	 */
	#core: SignalCore = {
		c: 0,
		h: new Set(),
	};

	/**
	 * The {@link SignalSource source} this signal has been derived from.
	 */
	#source: SignalSource;

	/**
	 * The {@link SignalSource source root}.
	 */
	#root: Signal<unknown>;

	/**
	 * Create a new signal.
	 *
	 * @param value The initial value.
	 * @param source The {@link SignalSource source} this signal has been derived from.
	 */
	constructor(value: T, source?: SignalSource) {
		this.inert = value;
		this.#source = source;
		this.#root = source ? source.#root : this;
	}

	/**
	 * Reactively access the current value.
	 */
	get value(): T {
		this.access();
		return this.inert;
	}

	/**
	 * Set the current value.
	 *
	 * If the new value is the same as the previous one, no observers are notified.
	 *
	 * @example
	 * ```tsx
	 * import { $, watch } from "rvx";
	 *
	 * const count = $(0);
	 *
	 * watch(count, count => {
	 *   console.log("Count:", count);
	 * });
	 *
	 * count.value++;
	 * ```
	 */
	set value(value: T) {
		if (!Object.is(this.inert, value)) {
			this.inert = value;
			this.notify();
		}
	}

	/**
	 * The {@link SignalSource source}, this signal has been derived from.
	 */
	get source(): SignalSource {
		return this.#source;
	}

	/**
	 * The root {@link SignalSource source}, this signal has been derived from or this signal itself if it hasn't been derived.
	 */
	get root(): Signal<unknown> {
		return this.#root;
	}

	/**
	 * Check if this signal has any active observers.
	 */
	get active(): boolean {
		return this.#core.h.size > 0;
	}

	/**
	 * Manually access this signal.
	 */
	access(): void {
		ACCESS_FRAME?.(this.#core);
	}

	/**
	 * Manually notify observers.
	 *
	 * During batches, notifications are deferred.
	 */
	notify(): void {
		const core = this.#core;
		core.c++;
		if (core.h.size === 0) {
			return;
		}
		if (BATCH === undefined) {
			const record = Array.from(core.h);
			core.h.clear();
			record.forEach(_notify);
		} else {
			core.h.forEach(_queueBatch);
			/*
				Hooks are not cleared during batches to prevent breaking
				other observers if an error is thrown during the batch.

				Calls are deduplicated within the batch.
			*/
		}
	}

	/**
	 * Pass this signal to a function and get its result.
	 *
	 * @example
	 * ```tsx
	 * const value = $(42);
	 *
	 * <TextInput value={
	 *   value
	 *     .pipe(parseInt)
	 *     .pipe(trim)
	 * } />
	 * ```
	 */
	pipe<A extends any[], R>(fn: (self: this, ...args: A) => R, ...args: A): R {
		return fn(this, ...args);
	}
}

/**
 * Create a new signal.
 *
 * @param value The initial value.
 * @param source The {@link SignalSource source} this signal has been derived from.
 * @returns The signal.
 */
export function $(): Signal<void>;
export function $<T>(value: T, source?: SignalSource): Signal<T>;
export function $(value?: unknown, source?: SignalSource): Signal<unknown> {
	return new Signal(value, source);
}

/**
 * A value, signal or function to get a value.
 *
 * @example
 * ```tsx
 * import { $, watch } from "rvx";
 *
 * const message = $("Example");
 *
 * // Not reactive:
 * watch(message.value, message => {
 *   console.log("A:", message);
 * });
 *
 * // Reactive:
 * watch(message, message => {
 *   console.log("B:", message);
 * });
 *
 * // Reactive:
 * watch(() => message.value, message => {
 *   console.log("C:", message);
 * });
 *
 * message.value = "Hello World!";
 * ```
 */
export type Expression<T> = T | Signal<T> | (() => T);

/**
 * Utility to get the result type of an expression.
 */
export type ExpressionResult<T> = T extends Expression<infer R> ? R : never;

/**
 * Utility type for expressions that should never be static values.
 *
 * This can be used instead of the {@link Expression} type in places where accepting static values doesn't make sense.
 */
export type Reactive<T> = Signal<T> | (() => T);

/**
 * Utility type to require `T` to not be reactive.
 */
export type Static<T> = unknown extends T ? never : Exclude<T, Reactive<any>>;

/**
 * Internal utility to unfold potential recursion into a sequence.
 */
const _unfold = (hook: NotifyHook): NotifyHook => {
	let depth = 0;
	return () => {
		if (depth < 2) {
			depth++;
		}
		if (depth === 1) {
			try {
				while (depth > 0) {
					hook();
					depth--;
				}
			} finally {
				depth = 0;
			}
		}
	};
};

interface Observer {
	/**
	 * The access hook that can be pushed to the {@link ACCESS_STACK} to track signal accesses using this observer.
	 */
	a: AccessHook;

	/**
	 * Detach this observer from all currently accessed signals.
	 */
	c: () => void;
}

/**
 * Internal utility to create an observer for tracking signal accesses.
 *
 * @param hook The notify hook to add to all accessed signals.
 */
const _observer = (hook: NotifyHook): Observer => {
	/** Array of the hook sets of currently accessed signals. This can contain duplicates. */
	const signals: SignalCore[] = [];
	return {
		c: () => {
			for (let i = 0; i < signals.length; i++) {
				signals[i].h.delete(hook);
			}
			signals.length = 0;
		},
		a: (hooks: SignalCore): void => {
			signals.push(hooks);
			hooks.h.add(hook);
		},
	};
};

/**
 * Watch an expression until the current lifecycle is disposed.
 *
 * + Both the expression and effect are called at least once immediately.
 * + Lifecycle hooks from the expression or effect are called before a signal update is processed or when the current lifecycle is disposed.
 *
 * @param expr The expression to watch.
 * @param effect An optional function to call with each expression result without tracking signal accesses.
 *
 * @example
 * ```tsx
 * import { $, watch } from "rvx";
 *
 * const count = $(0);
 *
 * // Capture teardown hooks registered by "watch":
 * const dispose = capture(() => {
 *   // Start watching:
 *   watch(count, count => {
 *     console.log("Count:", count);
 *   });
 * });
 *
 * count.value = 1;
 *
 * // Stop watching:
 * dispose();
 *
 * count.value = 2;
 * ```
 */
export function watch<T>(expr: Reactive<T>, effect: (value: T) => void): void;

/**
 * @deprecated
 * This call can be removed because the expression is always static. You can call the effect directly.
 */
export function watch<T>(expr: Static<T>, effect: (value: T) => void): void;

/**
 * Watch an expression until the current lifecycle is disposed.
 *
 * + Both the expression and effect are called at least once immediately.
 * + Lifecycle hooks from the expression or effect are called before a signal update is processed or when the current lifecycle is disposed.
 *
 * @param expr The expression to watch.
 * @param effect An optional function to call with each expression result without tracking signal accesses.
 *
 * @example
 * ```tsx
 * import { $, watch } from "rvx";
 *
 * const count = $(0);
 *
 * // Capture teardown hooks registered by "watch":
 * const dispose = capture(() => {
 *   // Start watching:
 *   watch(count, count => {
 *     console.log("Count:", count);
 *   });
 * });
 *
 * count.value = 1;
 *
 * // Stop watching:
 * dispose();
 *
 * count.value = 2;
 * ```
 */
export function watch(expr: () => void): void;
export function watch<T>(expr: Expression<T>, effect: (value: T) => void): void;
export function watch<T>(expr: Expression<T>, effect?: (value: T) => void): void {
	const isSignal = expr instanceof Signal;
	if (isSignal || typeof expr === "function") {
		let value: T;
		let disposed = false;
		let dispose: TeardownHook = NOOP;
		const runExpr = isSignal ? () => (expr as Signal<T>).value : (expr as () => T);
		const entry = _unfold(Context.bind(() => {
			if (disposed) {
				// This covers an edge case where this observer is notified during a batch and then disposed immediately.
				return;
			}
			clear();
			isolate(dispose);
			dispose = capture(() => {
				const parent = ACCESS_FRAME;
				try {
					ACCESS_FRAME = access;
					value = runExpr();
					if (effect) {
						ACCESS_FRAME = undefined;
						effect(value);
					}
				} finally {
					ACCESS_FRAME = parent;
				}
			});
		}));
		const { c: clear, a: access } = _observer(entry);
		teardown(() => {
			disposed = true;
			clear();
			dispose();
		});
		entry();
	} else {
		effect!(expr);
	}
}

/**
 * Watch an expression until the current lifecycle is disposed.
 *
 * @param expr The expression to watch.
 * @param effect A function to call with each subsequent expression result without tracking signal accesses.
 * @returns The first expression result.
 */
export function watchUpdates<T>(expr: Reactive<T>, effect: (value: T) => void): T;

/**
 * @deprecated
 * This call can be removed because the expression is always static.
 */
export function watchUpdates<T>(expr: Static<T>, effect: (value: T) => void): T;

/**
 * Watch an expression until the current lifecycle is disposed.
 *
 * @param expr The expression to watch.
 * @param effect A function to call with each subsequent expression result without tracking signal accesses.
 * @returns The first expression result.
 */
export function watchUpdates<T>(expr: Expression<T>, effect: (value: T) => void): T;
export function watchUpdates<T>(expr: Expression<T>, effect: (value: T) => void): T {
	let first: T;
	let update = false;
	watch(expr, value => {
		if (update) {
			effect(value);
		} else {
			first = value;
			update = true;
		}
	});
	return first!;
}

interface Dependency {
	/**
	 * The accessed signal.
	 */
	s: SignalCore;

	/**
	 * The signal cycle at the time it was accessed.
	 */
	c: number;
}

function _isStale(dep: Dependency) {
	return dep.c !== dep.s.c;
}

/**
 * Wrap an expression to re-run only when any accessed signal has been updated.
 *
 * + Lifecycle hooks in the expression are not supported.
 * + The context from where `lazy` was called is available within the expression.
 *
 * @param expr The expression to wrap.
 */
export function lazy<T>(expr: () => T): () => T {
	let stale = true;
	let value!: T;

	const deps: Dependency[] = [];

	const access: AccessHook = signal => {
		deps.push({
			s: signal,
			c: signal.c,
		});
	};

	return Context.bind(() => {
		const observer = ACCESS_FRAME;
		if (stale || (stale = deps.some(_isStale))) {
			const parentTeardownFrame = TEARDOWN_FRAME;
			try {
				deps.length = 0;

				ACCESS_FRAME = access;
				TEARDOWN_FRAME = THROW_ON_LEAK;

				value = expr();
				stale = false;
			} finally {
				ACCESS_FRAME = observer;
				TEARDOWN_FRAME = parentTeardownFrame;

				if (observer) {
					deps.forEach(dep => observer(dep.s));
				}
			}
		} else {
			if (observer) {
				deps.forEach(dep => observer(dep.s));
			}
		}
		return value;
	});
}

function _dispatch(batch: Set<NotifyHook>): void {
	while (batch.size > 0) {
		batch.forEach(notify => {
			/*
				Notify hooks are deleted individually to ensure the correct behavior if calling
				the hooks adds itself to the batch again due to an immediate side effect.
			*/
			batch.delete(notify);
			_notify(notify);
		});
	}
}

/**
 * Defer signal updates until a function finishes.
 *
 * + When nesting batches, updates are processed after the most outer batch has completed.
 * + When updates cause immediate side effects, these side effects will run as part of the batch.
 *
 * @param fn The function to run.
 * @returns The function's return value.
 *
 * @example
 * The example below outputs `5` and `9` once. Without batching the output would be `5, 7, 9`.
 * ```tsx
 * import { batch, $, watch } from "rvx";
 *
 * const a = $(2);
 * const b = $(3);
 *
 * watch(() => a.value + b.value, value => {
 *   console.log("Sum:", value);
 * });
 *
 * batch(() => {
 *   a.value = 4;
 *   b.value = 5;
 * });
 * ```
 */
export function batch<T>(fn: () => T): T {
	if (BATCH === undefined) {
		const batch = new Set<NotifyHook>();
		let value: T;
		try {
			BATCH = batch;
			value = fn();
			_dispatch(batch);
		} finally {
			BATCH = undefined;
		}
		return value;
	}
	return fn();
}

/**
 * {@link watch Watch} an expression and get a function to reactively access its result.
 *
 * @param expr The expression to watch.
 * @returns A function to reactively access the latest result.
 *
 * @example
 * ```tsx
 * import { $, memo, watch } from "rvx";
 *
 * const count = $(42);
 *
 * const computed = memo(() => someExpensiveComputation(count.value));
 *
 * watch(computed, count => {
 *   console.log("Count:", count);
 * });
 * ```
 */
export function memo<T>(expr: Reactive<T>): () => T;

/**
 * @deprecated
 * This call can be removed because the expression is always static. You can use the value directly.
 */
export function memo<T>(expr: Static<T>): () => T;

/**
 * {@link watch Watch} an expression and get a function to reactively access its result.
 *
 * @param expr The expression to watch.
 * @returns A function to reactively access the latest result.
 *
 * @example
 * ```tsx
 * import { $, memo, watch } from "rvx";
 *
 * const count = $(42);
 *
 * const computed = memo(() => someExpensiveComputation(count.value));
 *
 * watch(computed, count => {
 *   console.log("Count:", count);
 * });
 * ```
 */
export function memo<T>(expr: Expression<T>): () => T;
export function memo<T>(expr: Expression<T>): () => T {
	const signal = $<T>(undefined!);
	watch(() => signal.value = get(expr));
	return () => signal.value;
}

/**
 * {@link get Evaluate an expression} without tracking signal accesses.
 *
 * @param expr The expression to evaluate.
 * @returns The function's return value.
 *
 * @example
 * ```tsx
 * import { $, untrack, watch } from "rvx";
 *
 * const a = $(2);
 * const b = $(3);
 *
 * watch(() => a.value + untrack(b), sum => {
 *   console.log("Sum:", sum);
 * });
 *
 * // This causes an update:
 * a.value = 4;
 *
 * // This has no effect:
 * b.value = 5;
 * ```
 */
export function untrack<T>(expr: Reactive<T>): T;

/**
 * @deprecated
 * This call can be removed because the expression is always static. You can use the value directly.
 */
export function untrack<T>(expr: Static<T>): T;

/**
 * {@link get Evaluate an expression} without tracking signal accesses.
 *
 * @param expr The expression to evaluate.
 * @returns The function's return value.
 *
 * @example
 * ```tsx
 * import { $, untrack, watch } from "rvx";
 *
 * const a = $(2);
 * const b = $(3);
 *
 * watch(() => a.value + untrack(b), sum => {
 *   console.log("Sum:", sum);
 * });
 *
 * // This causes an update:
 * a.value = 4;
 *
 * // This has no effect:
 * b.value = 5;
 * ```
 */
export function untrack<T>(expr: Expression<T>): T;
export function untrack<T>(expr: Expression<T>): T {
	const parent = ACCESS_FRAME;
	try {
		ACCESS_FRAME = undefined;
		return get(expr);
	} finally {
		ACCESS_FRAME = parent;
	}
}

/**
 * Check if signal accesses are currently tracked.
 */
export function isTracking(): boolean {
	return ACCESS_FRAME !== undefined;
}

/**
 * Run a function while tracking signal accesses to invoke the trigger callback when updated.
 *
 * See {@link trigger}.
 */
export interface TriggerPipe {
	<T>(expr: Reactive<T>): T;

	/**
	 * @deprecated
	 * This call can be removed because the expression is always static. You can use the value directly.
	 */
	<T>(expr: Static<T>): T;

	<T>(expr: Expression<T>): T;
}

/**
 * Create an expression evaluator pipe that calls a function once when any accessed signals from the latest evaluated expression are updated.
 *
 * + When the lifecycle at which the pipe was created is disposed, the callback function will not be called anymore.
 * + It is guaranteed that the function is called before any other observers like {@link watch} or {@link effect} are notified.
 * + If pipes are nested, the callback for the most inner one is called first.
 *
 * @param callback The callback to invoke when a signal is updated.
 * @returns The pipe to evaluate expressions.
 */
export function trigger(callback: () => void): TriggerPipe {
	const hookFn = Context.bind(() => {
		clear();
		isolate(_notify, callback);
	});
	const { c: clear, a: access } = _observer(hookFn);
	teardown(clear);
	return <T>(expr: Expression<T>) => {
		clear();
		const parent = ACCESS_FRAME;
		try {
			if (parent === undefined) {
				ACCESS_FRAME = access;
			} else {
				ACCESS_FRAME = hooks => {
					/*
						Tracking accesses using this observer before any outer ones also
						guarantees the order in which observers are notified because:
						+ Hooks in Signal.#hooks are called in iteration order.
						+ Set iteration order matches the order in which observers add their hooks.
					*/
					access(hooks);
					parent?.(hooks);
				};
			}
			return get(expr);
		} finally {
			ACCESS_FRAME = parent;
		}
	};
}

/**
 * Manually evaluate an expression in the current context.
 *
 * This can be used to access reactive and non reactive inputs.
 *
 * @param expr The expression to evaluate.
 * @returns The expression result.
 *
 * @example
 * ```tsx
 * import { $, get } from "rvx";
 *
 * const count = $(42);
 *
 * get(42) // 42
 * get(count) // 42
 * get(() => 42) // 42
 * ```
 */
export function get<T>(expr: Reactive<T>): T;

/**
 * @deprecated
 * This call can be removed because the expression is always static. You can use the value directly.
 */
export function get<T>(expr: Static<T>): T;

/**
 * Manually evaluate an expression in the current context.
 *
 * This can be used to access reactive and non reactive inputs.
 *
 * @param expr The expression to evaluate.
 * @returns The expression result.
 *
 * @example
 * ```tsx
 * import { $, get } from "rvx";
 *
 * const count = $(42);
 *
 * get(42) // 42
 * get(count) // 42
 * get(() => 42) // 42
 * ```
 */
export function get<T>(expr: Expression<T>): T;
export function get<T>(expr: Expression<T>): T {
	if (expr instanceof Signal) {
		return expr.value;
	}
	if (typeof expr === "function") {
		return (expr as () => T)();
	}
	return expr;
}

export type MapFn<I, O> = (input: I) => O;

/**
 * Map an expression value while preserving if the expression is static or not.
 *
 * @example
 * ```tsx
 * import { $, map, get } from "rvx";
 *
 * const count = $(42);
 * const doubleCount = map(count, value => value * 2);
 *
 * get(doubleCount) // 84
 * ```
 */
export function map<I, O>(input: Reactive<I>, mapFn: MapFn<I, O>): Expression<O>;

/**
 * @deprecated
 * This call can be removed because the input is always static. You can call the map function directly.
 */
export function map<I, O>(input: Static<I>, mapFn: MapFn<I, O>): Expression<O>;

/**
 * Map an expression value while preserving if the expression is static or not.
 *
 * @example
 * ```tsx
 * import { $, map, get } from "rvx";
 *
 * const count = $(42);
 * const doubleCount = map(count, value => value * 2);
 *
 * get(doubleCount) // 84
 * ```
 */
export function map<I, O>(input: Expression<I>, mapFn: MapFn<I, O>): Expression<O>;
export function map<I, O>(input: Expression<I>, mapFn: MapFn<I, O>): Expression<O> {
	if (input instanceof Signal) {
		return () => mapFn(input.value);
	}
	if (typeof input === "function") {
		return () => mapFn((input as () => I)());
	}
	return mapFn(input);
}
