#if Tracing

/// Hooks for tracing Swift <-> JavaScript bridge calls.
public struct JSTracing: Sendable {
    public static let `default` = JSTracing()

    public enum JSCallInfo {
        case function(function: JSObject, arguments: [JSValue])
        case method(receiver: JSObject, methodName: String?, arguments: [JSValue])
        case propertyGet(receiver: JSObject, propertyName: String)
        case propertySet(receiver: JSObject, propertyName: String, value: JSValue)
    }

    /// Register a hook for Swift to JavaScript calls.
    ///
    /// The hook is invoked at the start of the call. Return a closure to run when
    /// the call finishes, or `nil` to skip the end hook.
    ///
    /// - Returns: A cleanup closure that unregisters the hook.
    @discardableResult
    public func addJSCallHook(
        _ hook: @escaping (_ info: JSCallInfo) -> (() -> Void)?
    ) -> () -> Void {
        JSTracingHooks.addJSCallHook(hook)
    }

    public struct JSClosureCallInfo {
        /// The file identifier where the called `JSClosure` was created.
        public let fileID: String
        /// The line number where the called `JSClosure` was created.
        public let line: UInt
    }

    /// Register a hook for JavaScript to Swift calls via `JSClosure`.
    ///
    /// The hook is invoked at the start of the call. Return a closure to run when
    /// the call finishes, or `nil` to skip the end hook.
    ///
    /// - Returns: A cleanup closure that unregisters the hook.
    @discardableResult
    public func addJSClosureCallHook(
        _ hook: @escaping (_ info: JSClosureCallInfo) -> (() -> Void)?
    ) -> () -> Void {
        JSTracingHooks.addJSClosureCallHook(hook)
    }
}

enum JSTracingHooks {
    typealias HookEnd = () -> Void
    typealias JSCallHook = (JSTracing.JSCallInfo) -> HookEnd?
    typealias JSClosureCallHook = (JSTracing.JSClosureCallInfo) -> HookEnd?

    private final class HookList<Hook> {
        private var hooks: [(id: UInt, hook: Hook)] = []
        private var nextID: UInt = 0

        var isEmpty: Bool { hooks.isEmpty }

        func add(_ hook: Hook) -> UInt {
            let id = nextID
            nextID &+= 1
            hooks.append((id, hook))
            return id
        }

        func remove(id: UInt) {
            hooks.removeAll { $0.id == id }
        }

        func forEach(_ body: (Hook) -> Void) {
            for entry in hooks {
                body(entry.hook)
            }
        }
    }

    private final class Storage {
        let jsCallHooks = HookList<JSCallHook>()
        let jsClosureCallHooks = HookList<JSClosureCallHook>()
    }

    private static let storage = LazyThreadLocal(initialize: Storage.init)

    static func addJSCallHook(_ hook: @escaping JSCallHook) -> () -> Void {
        let storage = storage.wrappedValue
        let id = storage.jsCallHooks.add(hook)
        return { storage.jsCallHooks.remove(id: id) }
    }

    static func addJSClosureCallHook(_ hook: @escaping JSClosureCallHook) -> () -> Void {
        let storage = storage.wrappedValue
        let id = storage.jsClosureCallHooks.add(hook)
        return { storage.jsClosureCallHooks.remove(id: id) }
    }

    static func beginJSCall(_ info: JSTracing.JSCallInfo) -> HookEnd? {
        let storage = storage.wrappedValue
        guard !storage.jsCallHooks.isEmpty else { return nil }

        var callbacks: [HookEnd] = []
        storage.jsCallHooks.forEach { hook in
            if let callback = hook(info) {
                callbacks.append(callback)
            }
        }

        guard !callbacks.isEmpty else { return nil }
        return {
            for callback in callbacks.reversed() {
                callback()
            }
        }
    }

    static func beginJSClosureCall(_ info: JSTracing.JSClosureCallInfo) -> HookEnd? {
        let storage = storage.wrappedValue
        guard !storage.jsClosureCallHooks.isEmpty else { return nil }

        var callbacks: [HookEnd] = []
        storage.jsClosureCallHooks.forEach { hook in
            if let callback = hook(info) {
                callbacks.append(callback)
            }
        }

        guard !callbacks.isEmpty else { return nil }
        return {
            for callback in callbacks.reversed() {
                callback()
            }
        }
    }
}

#endif
