import ConcurrencyExtras
import Foundation
import Perception

/// Configuration for `ObservedBox`.
public enum ObservedBoxConfig {
    /// Overridable feature flag resolver.
    public static var overrideObserveChanges: @Sendable () -> Bool = { false }
}

/// A copy-on-write wrapper used to move large value types off the stack and on to the heap.
/// This can be useful for properties on large state structs to keep the stack size under control.
///
/// This implementation requires `@ObservationStateIgnored`.
/// To observe changes to the wrapped value, set `observe` to `true`.
@propertyWrapper
public struct ObservedBox<T: Equatable & Sendable>: Equatable, Observable {
    private var ref: LockIsolated<T>
    private let observe: Bool
    
    private let _observationRegistrar: ObservationRegistrar?

    public init(wrappedValue: T, observe: Bool = false) {
        self.ref = LockIsolated(wrappedValue)
        self.observe = observe || ObservedBoxConfig.overrideObserveChanges()
        
        if self.observe {
            self._observationRegistrar = ObservationRegistrar()
        } else {
            self._observationRegistrar = nil
        }
    }

    public static func == (lhs: ObservedBox<T>, rhs: ObservedBox<T>) -> Bool {
        if lhs.ref === rhs.ref {
            return true
        } else {
            return lhs.wrappedValue == rhs.wrappedValue
        }
    }

    public var wrappedValue: T {
        get {
            access(keyPath: \.ref)
            return ref.value
        }
        set {
            withMutation(keyPath: \.ref) {
                if !isKnownUniquelyReferenced(&ref) {
                    ref = LockIsolated(newValue)
                    return
                }
                ref.setValue(newValue)
            }
        }
    }

    nonisolated func access<Member>(keyPath: KeyPath<Self, Member>) {
        if observe, let _observationRegistrar {
            _observationRegistrar.access(self, keyPath: keyPath)
        }
    }

    nonisolated func withMutation<Member, MutationResult>(
        keyPath: KeyPath<Self, Member>,
        mutation: () throws -> MutationResult
    ) rethrows -> MutationResult {
        if observe, let _observationRegistrar {
            return try _observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation)
        } else {
            return try mutation()
        }
    }
}

extension ObservedBox: Sendable where T: Sendable {}
