// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.

import Foundation

// MARK: - Atomic<Value>

/// The `Atomic<Value>` wrapper is a generic wrapper providing a thread-safe way to get and set a value
///
/// A write-up on the need for this class and it's approaches can be found at these links:
/// https://www.vadimbulavin.com/atomic-properties/
/// https://www.vadimbulavin.com/swift-atomic-properties-with-property-wrappers/
/// there is also another approach which can be taken but it requires separate types for collections and results in
/// a somewhat inconsistent interface between different `Atomic` wrappers
///
/// We use a Read-write lock approach because the `DispatchQueue` approach means mutating the property
/// occurs on a different thread, and GRDB requires it's changes to be executed on specific threads so using a lock
/// is more compatible (and Read-write locks allow for concurrent reads which shouldn't be a huge issue but could
/// help reduce cases of blocking)
@propertyWrapper
public class Atomic<Value> {
    private var value: Value
    private let lock: ReadWriteLock = ReadWriteLock()

    /// In order to change the value you **must** use the `mutate` function
    public var wrappedValue: Value {
        lock.readLock()
        let result: Value = value
        lock.unlock()

        return result
    }

    /// For more information see https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md#projections
    public var projectedValue: Atomic<Value> {
        return self
    }
    
    // MARK: - Initialization

    public init(_ initialValue: Value) {
        self.value = initialValue
    }
    
    public init(wrappedValue: Value) {
        self.value = wrappedValue
    }
    
    // MARK: - Functions

    @discardableResult public func mutate<T>(_ mutation: (inout Value) -> T) -> T {
        lock.writeLock()
        let result: T = mutation(&value)
        lock.unlock()
        
        return result
    }
    
    @discardableResult public func mutate<T>(_ mutation: (inout Value) throws -> T) throws -> T {
        let result: T
        
        do {
            lock.writeLock()
            result = try mutation(&value)
            lock.unlock()
        }
        catch {
            lock.unlock()
            throw error
        }
        
        return result
    }
}

extension Atomic where Value: CustomDebugStringConvertible {
    var debugDescription: String {
        return value.debugDescription
    }
}

// MARK: - ReadWriteLock

private class ReadWriteLock {
    private var rwlock: pthread_rwlock_t
    
    // Need to do this in a proper init function instead of a lazy variable or it can indefinitely
    // hang on XCode 15 when trying to retrieve a lock (potentially due to optimisations?)
    init() {
        rwlock = pthread_rwlock_t()
        pthread_rwlock_init(&rwlock, nil)
    }
    
    func writeLock() {
        pthread_rwlock_wrlock(&rwlock)
    }
    
    func readLock() {
        pthread_rwlock_rdlock(&rwlock)
    }
    
    func unlock() {
        pthread_rwlock_unlock(&rwlock)
    }
}
