//
//  Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//

import Foundation

@objc(OWSBlockListCacheDelegate)
public protocol BlockListCacheDelegate: class {
    func blockListCacheDidUpdate(_ blocklistCache: BlockListCache)
}

/// A performant cache for which contacts/groups are blocked.
///
/// The source of truth for which contacts and groups are blocked is the `blockingManager`, but because
/// those accessors are made to be thread safe, they can be slow in tight loops, e.g. when rendering table
/// view cells.
///
/// Typically you'll want to create a Cache, update it to the latest state while simultaneously being informed
/// of any future changes to block list state.
///
///     class SomeViewController: BlockListCacheDelegate {
///         let blockListCache = BlockListCache()
///         func viewDidLoad() {
///            super.viewDidLoad()
///            blockListCache.startObservingAndSyncState(delegate: self)
///            self.updateAnyViewsWhichDepondOnBlockListCache()
///         }
///
///         func blockListCacheDidUpdate(_ blocklistCache: BlockListCache) {
///             self.updateAnyViewsWhichDepondOnBlockListCache()
///         }
///
///         ...
///      }
///
@objc(OWSBlockListCache)
public class BlockListCache: NSObject {

    private var blockedRecipientIds: Set<String> = Set()
    private var blockedGroupIds: Set<Data> = Set()
    private let serialQueue: DispatchQueue = DispatchQueue(label: "BlockListCache")
    weak var delegate: BlockListCacheDelegate?

    private var blockingManager: OWSBlockingManager {
        return OWSBlockingManager.shared()
    }

    /// Generally something which wants to use this cache wants to do 3 things
    ///   1. get the cache on the latest state
    ///   2. update the cache whenever the blockingManager's state changes
    ///   3. be notified when the cache updates
    /// This method does all three.
    @objc
    public func startObservingAndSyncState(delegate: BlockListCacheDelegate) {
        self.delegate = delegate
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(blockListDidChange),
                                               name: NSNotification.Name(rawValue: kNSNotificationName_BlockListDidChange),
                                               object: nil)
        updateWithoutNotifyingDelegate()
    }

    // MARK: -

    @objc
    func blockListDidChange() {
        self.update()
    }

    @objc(isRecipientIdBlocked:)
    public func isBlocked(recipientId: String) -> Bool {
        return serialQueue.sync {
            blockedRecipientIds.contains(recipientId)
        }
    }

    @objc(isGroupIdBlocked:)
    public func isBlocked(groupId: Data) -> Bool {
        return serialQueue.sync {
            blockedGroupIds.contains(groupId)
        }
    }

    @objc(isThreadBlocked:)
    public func isBlocked(thread: TSThread) -> Bool {
        switch thread {
        case let contactThread as TSContactThread:
            return serialQueue.sync {
                blockedRecipientIds.contains(contactThread.contactIdentifier())
            }
        case let groupThread as TSGroupThread:
            return serialQueue.sync {
                blockedGroupIds.contains(groupThread.groupModel.groupId)
            }
        default:
            owsFailDebug("\(self.logTag) in \(#function) unexpected thread type: \(type(of: thread))")
            return false
        }
    }

    // MARK: -

    public func update() {
        updateWithoutNotifyingDelegate()
        DispatchQueue.main.async {
            self.delegate?.blockListCacheDidUpdate(self)
        }
    }

    private func updateWithoutNotifyingDelegate() {
        let blockedRecipientIds = Set(blockingManager.blockedPhoneNumbers())
        let blockedGroupIds = Set(blockingManager.blockedGroupIds)
        update(blockedRecipientIds: blockedRecipientIds, blockedGroupIds: blockedGroupIds)
    }

    private func update(blockedRecipientIds: Set<String>, blockedGroupIds: Set<Data>) {
        serialQueue.sync {
            self.blockedRecipientIds = blockedRecipientIds
            self.blockedGroupIds = blockedGroupIds
        }
    }
}
