import Carbon
import Cocoa
import OSLog

// Manages the secure keyboard input state. Secure keyboard input is an old Carbon
// API still in use by applications such as Webkit. From the old Carbon docs:
// "When secure event input mode is enabled, keyboard input goes only to the
// application with keyboard focus and is not echoed to other applications that
// might be using the event monitor target to watch keyboard input."
//
// Secure input is global and stateful so you need a singleton class to manage
// it. You have to yield secure input on application deactivation (because
// it'll affect other apps) and reacquire on reactivation, and every enable
// needs to be balanced with a disable.
class SecureInput: ObservableObject {
    static let shared = SecureInput()

    private static let logger = Logger(
        subsystem: Bundle.main.bundleIdentifier!,
        category: String(describing: SecureInput.self)
    )

    // True if you want to enable secure input globally.
    var global: Bool = false {
        didSet {
            apply()
        }
    }

    // The scoped objects and whether they're currently in focus.
    private var scoped: [ObjectIdentifier: Bool] = [:]

    // This is set to true when we've successfully called EnableSecureInput.
    @Published private(set) var enabled: Bool = false

    // This is true if we want to enable secure input. We want to enable
    // secure input if its enabled globally or any of the scoped objects are
    // in focus.
    private var desired: Bool {
        global || scoped.contains(where: { $0.value })
    }

    private init() {
        // Add notifications for application active/resign so we can disable
        // secure input. This is only useful for global enabling of secure
        // input.
        let center = NotificationCenter.default
        center.addObserver(
            self,
            selector: #selector(onDidResignActive(notification:)),
            name: NSApplication.didResignActiveNotification,
            object: nil)
        center.addObserver(
            self,
            selector: #selector(onDidBecomeActive(notification:)),
            name: NSApplication.didBecomeActiveNotification,
            object: nil)
    }

    deinit {
        NotificationCenter.default.removeObserver(self)

        // Reset our state so that we can ensure we set the proper secure input
        // system state
        scoped.removeAll()
        global = false
        apply()
    }

    // Add a scoped object that has secure input enabled. The focused value will
    // determine if the object currently has focus. This is used so that secure
    // input is only enabled while the object is focused.
    func setScoped(_ object: ObjectIdentifier, focused: Bool) {
        scoped[object] = focused
        apply()
    }

    // Remove a scoped object completely.
    func removeScoped(_ object: ObjectIdentifier) {
        scoped[object] = nil
        apply()
    }

    private func apply() {
        // If we aren't active then we don't do anything. The become/resign
        // active notifications will handle applying for us.
        guard NSApp.isActive else { return }

        // We only need to apply if we're not in our desired state
        guard enabled != desired else { return }

        let err: OSStatus
        if enabled {
            err = DisableSecureEventInput()
        } else {
            err = EnableSecureEventInput()
        }
        if err == noErr {
            enabled = desired
            Self.logger.debug("secure input state=\(self.enabled, privacy: .public)")
            return
        }

        Self.logger.warning("secure input apply failed err=\(err, privacy: .public)")
    }

    // MARK: Notifications

    @objc private func onDidBecomeActive(notification: NSNotification) {
        // We only want to re-enable if we're not already enabled and we
        // desire to be enabled.
        guard !enabled && desired else { return }
        let err = EnableSecureEventInput()
        if err == noErr {
            enabled = true
            Self.logger.debug("secure input enabled on activation")
            return
        }

        Self.logger.warning("secure input apply failed err=\(err, privacy: .public)")
    }

    @objc private func onDidResignActive(notification: NSNotification) {
        // We only want to disable if we're enabled.
        guard enabled else { return }
        let err = DisableSecureEventInput()
        if err == noErr {
            enabled = false
            Self.logger.debug("secure input disabled on deactivation")
            return
        }

        Self.logger.warning("secure input apply failed err=\(err, privacy: .public)")
    }
}
