//
//  TopNotificationHandler.swift
//  MVMCoreUI
//
//  Created by Scott Pfeil on 4/11/23.
//  Copyright © 2023 Verizon Wireless. All rights reserved.
//

import MVMCore
import Dispatch
import Combine

/// Handles the UI tasks for the notification.
public protocol NotificationTransitionDelegateProtocol {
    @MainActor
    func show(notification: UIView) async
    
    @MainActor
    func hide(notification: UIView) async
    
    @MainActor
    func update(with model: NotificationModel, delegateObject: MVMCoreUIDelegateObject?)
}

/// An operation for managing the life cycle of the notification.
public class NotificationOperation: MVMCoreOperation {
    
    public let notification: UIView
    
    public var notificationModel: NotificationModel
    
    /// The delegate that manages transitioning the notification.
    private let transitionDelegate: NotificationTransitionDelegateProtocol
    
    /// The showing animation transition operation.
    private weak var showTransitionOperation: Operation?

    /// The stop timer for non-persistent notifications.
    private var timerSource: DispatchSourceTimer?
    
    /// Determines if the operation is ready. For example, certain notifications are only meant to be displayed on certain pages and this can be set accordingly.
    public var isDisplayable: Bool {
        get {
            var isDisplayable: Bool = true
            // TODO: Causes crash. Check thoughts of reviewer.
            //displayableQueue.sync {
                isDisplayable = _isDisplayable
            //}
            return isDisplayable
        }
        set {
            guard newValue != isDisplayable else { return }
            //displayableQueue.async(flags: .barrier) { [weak self] in
                self._isDisplayable = newValue
            //}
        }
    }
    /// Thread safety.
    private var displayableQueue = DispatchQueue(label: "displayable", attributes: .concurrent)
    /// Updates the operation readiness accordingly.
    private var _isDisplayable: Bool = true {
        willSet {
            guard super.isReady else { return }
            willChangeValue(for: \.isReady)
        }
        didSet {
            guard super.isReady else { return }
            didChangeValue(for: \.isReady)
        }
    }
    
    /// This operation is ready only if this notification is allowed to show.
    public override var isReady: Bool {
        get {
            guard !isCancelled else { return super.isReady }
            return super.isReady && isDisplayable
        }
    }
    
    public actor Properties {
        /// If the notification is currently displayed.
        private var isDisplayed: Bool = false
        
        /// If the notification is currently animating (showing/hiding).
        private var isAnimating: Bool = false

        fileprivate func set(displayed: Bool) {
            isDisplayed = displayed
        }
        
        public func getIsDisplayed() -> Bool {
            return isDisplayed
        }
        
        fileprivate func set(animating: Bool) {
            isAnimating = animating
        }
        
        public func getIsAnimating() -> Bool {
            return isAnimating
        }
    }
    /// Actor isolated properties for the operation.
    public var properties = Properties()
    
    /// A flag for tracking if the operation needs to be re-added because it was cancelled for a higher priority notification.
    public var reAddAfterCancel = false
    
    public init(with notification: UIView, notificationModel: NotificationModel, transitionDelegate: NotificationTransitionDelegateProtocol) {
        self.notification = notification
        self.notificationModel = notificationModel
        self.transitionDelegate = transitionDelegate
        super.init()
        queuePriority = notificationModel.priority
    }
    
    public override func main() {
        log(message: "Operation Started")
        guard !checkAndHandleForCancellation() else { return }
        Task {
            await withCheckedContinuation { continuation in
                // Show the notification.
                showTransitionOperation = add(transition: { [weak self] in
                    guard let self = self else { return }
                    NotificationHandler.shared()?.onNotificationWillShow.send((self.notification, self.notificationModel))
                    self.log(message: "Operation Will Show")
                    await self.showNotification()
                }, completionBlock: {
                    continuation.resume()
                })
            }
            guard await properties.getIsDisplayed() else {
                // If the animation did not complete...
                log(message: "Operation Never Shown")
                markAsFinished()
                return
            }
                
            // Publish that the notification has been shown.
            NotificationHandler.shared()?.onNotificationShown.send((notification, notificationModel))
            log(message: "Operation Did Show")

            guard !isCancelled else {
                // If cancelled during the animation, dismiss immediately.
                stop()
                return
            }
            updateStopTimer()
        }
    }
    
    public func stop() {
        if let timerSource = timerSource {
            timerSource.cancel()
        }
        Task {
            guard await properties.getIsDisplayed(),
                  await !properties.getIsAnimating() else { return }
            // Hide the notification
            await withCheckedContinuation({ continuation in
                _ = add(transition: { [weak self] in
                    guard let self = self else { return }
                    self.log(message: "Operation Will Dismiss")
                    NotificationHandler.shared()?.onNotificationWillDismiss.send((notification, notificationModel))
                    await self.hideNotification()
                }, completionBlock: {
                    continuation.resume()
                })
            })
            // The animation must complete...
            guard await !self.properties.getIsDisplayed() else { return }
                
            // Publish that the notification has been hidden.
            NotificationHandler.shared()?.onNotificationDismissed.send((notification, notificationModel))
            log(message: "Operation Did Dismiss")

            guard isCancelled || !notificationModel.persistent else { return }
            markAsFinished()
        }
    }
    
    public override func markAsFinished() {
        log(message: "Operation Finished")
        super.markAsFinished()
    }
    
    public override func cancel() {
        super.cancel()
        log(message: "Operation Cancelled")
        Task {
            if await !properties.getIsDisplayed() {
                // Cancel any pending show transitions.
                showTransitionOperation?.cancel()
            }
            
            // Do nothing if animating.
            guard await !properties.getIsAnimating() else { return }
            if await properties.getIsDisplayed() {
                stop()
            } else if isExecuting {
                markAsFinished()
            }
        }
    }
    
    public func log(message: String) {
        MVMCoreUILoggingHandler.logDebugMessage(withDelegate: "------Notification message: \(message) type: \(notificationModel.type) title: \(String(describing: (notificationModel.molecule as? NotificationMoleculeModel)?.headline.text)) id: \(notificationModel.id) priority: e\(notificationModel.priority.rawValue) a\(queuePriority.rawValue) operation: \(String(describing: self)) ------")
    }
    
    // MARK: - Automatic
    
    /// Sets up a timer to hide the notification.
    private func updateStopTimer() {
        if let timerSource = timerSource {
            timerSource.cancel()
        }
        guard !notificationModel.persistent else { return }
        timerSource = DispatchSource.makeTimerSource()
        timerSource?.setEventHandler(handler: { [weak self] in
            self?.log(message: "Operation Time Out")
            guard let self = self,
            !self.isFinished,
            !self.checkAndHandleForCancellation() else { return }

            // If voice over is on and the notification is focused, do not collapse until unfocused.
            guard !MVMCoreUIUtility.viewContainsAccessiblityFocus(notification) else {
                NotificationCenter.default.addObserver(self, selector: #selector(accessibilityFocusChanged), name: UIAccessibility.elementFocusedNotification, object: nil)
                NotificationCenter.default.addObserver(self, selector: #selector(accessibilityFocusChanged), name: UIAccessibility.voiceOverStatusDidChangeNotification, object: nil)
                return
            }
            self.stop()
        })
        timerSource?.setCancelHandler(handler: { [weak self] in
            self?.log(message: "Operation Time Out Cancel")
        })
        timerSource?.schedule(deadline: .now() + .seconds(notificationModel.dismissTime))
        timerSource?.activate()
    }
    
    /// If the voice over user leaves top alert focus, hide.
    @objc func accessibilityFocusChanged(_ notification: NSNotification) {
        guard UIAccessibility.isVoiceOverRunning else {
            accessibilityFocusFinished()
            return
        }
        guard let _ = notification.userInfo?[UIAccessibility.focusedElementUserInfoKey],
                !MVMCoreUIUtility.viewContainsAccessiblityFocus(self.notification) else { return }
        accessibilityFocusFinished()
    }
    
    /// Dismisses the nofitication when we lose focus.
    private func accessibilityFocusFinished() {
        log(message: "Operation Accessibility Focus Removed")
        NotificationCenter.default.removeObserver(self, name: UIAccessibility.elementFocusedNotification, object: nil)
        NotificationCenter.default.removeObserver(self, name: UIAccessibility.voiceOverStatusDidChangeNotification, object: nil)
        stop()
    }
    
    // MARK: - Transitions
    
    /// Adds the transition of the notification to the navigation queue to avoid animation collisions.
    private func add(transition: @escaping () async -> Void, completionBlock: (() -> Void)?) -> Operation {
        let transitionOperation = MVMCoreBlockOperation(block: { blockOperation in
            guard !blockOperation.checkAndHandleForCancellation() else { return }
            Task {
                await transition()
                blockOperation.markAsFinished()
            }
        })!
        transitionOperation.completionBlock = completionBlock
        NavigationHandler.shared().navigationQueue.addOperation(transitionOperation)
        return transitionOperation
    }
    
    @MainActor
    private func showNotification() async {
        await properties.set(animating: true)
        await transitionDelegate.show(notification: notification)
        await properties.set(displayed: true)
        await properties.set(animating: false)
    }
    
    @MainActor
    private func hideNotification() async {
        await properties.set(animating: true)
        await transitionDelegate.hide(notification: notification)
        await properties.set(displayed: false)
        await properties.set(animating: false)
    }
}

extension NotificationOperation: NSCopying {
    public func copy(with zone: NSZone? = nil) -> Any {
         let operation = NotificationOperation(with: notification, notificationModel: notificationModel, transitionDelegate: transitionDelegate)
        operation.reAddAfterCancel = reAddAfterCancel
        operation.isDisplayable = isDisplayable
        for dependency in dependencies {
            operation.addDependency(dependency)
        }
        operation.name = name
        operation.qualityOfService = qualityOfService
        return operation
    }
}

/// Manages notifications.
open class NotificationHandler {
    
    private var queue: OperationQueue = {
        let queue = OperationQueue()
        queue.maxConcurrentOperationCount = 1
        return queue
    }()
    
    private var transitionDelegate: NotificationTransitionDelegateProtocol
    
    private var cancellable: Cancellable?
    
    // MARK: - Publishers
        
    /// Publishes when a notification will show.
    public let onNotificationWillShow = PassthroughSubject<(UIView, NotificationModel), Never>()

    /// Publishes when a notification is shown.
    public let onNotificationShown = PassthroughSubject<(UIView, NotificationModel), Never>()
    
    /// Publishes when a notification will dismiss.
    public let onNotificationWillDismiss = PassthroughSubject<(UIView, NotificationModel), Never>()
    
    /// Publishes when a notification is dismissed.
    public let onNotificationDismissed = PassthroughSubject<(UIView, NotificationModel), Never>()
    
    /// Publishes when a notification is updated.
    public let onNotificationUpdated = PassthroughSubject<(UIView, NotificationModel), Never>()
    
    // MARK: -

    /// Returns the handler stored in the CoreUIObject
    public static func shared() -> Self? {
        guard let shared = CoreUIObject.sharedInstance()?.topNotificationHandler else { return nil }
        return MVMCoreActionUtility.fatalClassCheck(object: shared)
    }
    
    public init(with transitionDelegate: NotificationTransitionDelegateProtocol) {
        self.transitionDelegate = transitionDelegate
        registerWithNotificationCenter()
        registerForPageChanges()
    }
    
    // MARK: - JSON Handling
    
    /// Registers with the notification center to know when json is updated.
    private func registerWithNotificationCenter() {
        NotificationCenter.default.addObserver(self, selector: #selector(responseJSONUpdated(notification:)), name: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil)
    }
    
    /// Registers to know when pages change.
    private func registerForPageChanges() {
        cancellable = NavigationHandler.shared().onNavigation
            .filter({ $0.0 == .didNavigate })
            .sink { [weak self] (event, operation) in
            guard let self = self else { return }
            Task {
                // Update displayable for each top alert operation when page type changes, in top queue priority order.
                guard self.queue.operations.count > 0,
                      let navigationController = await operation.navigationType.getNavigationController(),
                      await navigationController == MVMCoreUISplitViewController.main()?.navigationController,
                      let viewController = await navigationController.getViewController() else { return }
                let pageType = (viewController as? MVMCoreViewControllerProtocol)?.pageType
                self.queue.operations.compactMap {
                    $0 as? NotificationOperation
                }.sorted {
                    $0.notificationModel.priority.rawValue > $1.notificationModel.priority.rawValue
                }.forEach {
                    $0.updateDisplayable(by: pageType)
                }
                self.reevaluteQueue()
            }
        }
    }
    
    /// Checks for new top alert json
    @objc private func responseJSONUpdated(notification: Notification) {
        guard let loadObject = (notification.userInfo?[String(describing: MVMCoreLoadObject.self)] as? MVMCoreLoadObject) else { return }
        let delegateObject = loadObject.delegateObject as? MVMCoreUIDelegateObject
        
        // Dismiss any top alerts that server wants us to dismiss.
        if let disableType = loadObject.responseInfoMap?.optionalStringForKey("disableType") {
            NotificationHandler.shared()?.cancelNotification(using: { view, model in
                return model.type == disableType
            })
        }
        
        // Show any new top alert.
        guard let responseJSON = loadObject.responseJSON,
                let json = responseJSON.optionalDictionaryForKey(KeyTopAlert) else { return }
        Task {
            do {
                try await showNotification(for: json, delegateObject: delegateObject)
            } catch {
                if let errorObject = MVMCoreErrorObject.createErrorObject(for: error, location: "\(self)") {
                    MVMCoreUILoggingHandler.shared()?.addError(toLog: errorObject)
                }
            }
        }
    }
    
    /// Converts the json to a model and creates the view and queues up the notification.
    open func showNotification(for json: [AnyHashable: Any], delegateObject: MVMCoreUIDelegateObject?) async throws {
        let model = try NotificationModel.decode(json: json, delegateObject: delegateObject)
        try await showNotification(for: model, delegateObject: delegateObject)
    }
    
    // MARK: - Operation Handling
    
    /// Adds the operation to the queue.
    private func add(operation: NotificationOperation) {
        operation.completionBlock = { [weak self] in
            // If the alert was cancelled to show another with higher priority, re-add to the operation when cancelled to the queue.
            if operation.reAddAfterCancel {
                let newOperation: NotificationOperation = operation.copy() as! NotificationOperation
                newOperation.reAddAfterCancel = false
                self?.add(operation: newOperation)
            }
            operation.completionBlock = nil
        }

        let currentPageType = (MVMCoreUISplitViewController.main()?.getCurrentDetailViewController() as? MVMCoreViewControllerProtocol)?.pageType
        operation.updateDisplayable(by: currentPageType)
        
        queue.addOperation(operation)
        operation.log(message: "Operation Added")
        reevaluteQueue()
    }
    
    /// Checks for existing top alert object of same type and updates it. Returns true if we updated.
    open func checkAndUpdateExisting(with model: NotificationModel, delegateObject: MVMCoreUIDelegateObject?) -> Bool {
        for case let operation as NotificationOperation in queue.operations {
            guard operation.notificationModel.type == model.type else { continue }
            operation.update(with: model, delegateObject: delegateObject)
            let pageType = (MVMCoreUISplitViewController.main()?.getCurrentDetailViewController() as? MVMCoreViewControllerProtocol)?.pageType
            operation.updateDisplayable(by: pageType)
            reevaluteQueue()
            return true
        }
        return false
    }
    
    /// Re-evaluates the queue operations
    private func reevaluteQueue() {
        var highestReadyOperation: NotificationOperation?
        var executingOperation: NotificationOperation?
        for case let operation as NotificationOperation in queue.operations {
            guard !operation.isCancelled,
                  !operation.isFinished else { continue }
            if operation.isReady,
               highestReadyOperation == nil || operation.notificationModel.priority.rawValue > highestReadyOperation!.notificationModel.priority.rawValue {
                highestReadyOperation = operation
            }
            if operation.isExecuting {
                executingOperation = operation
            }
        }
        guard let currentOperation = executingOperation else { return }
        
        // Cancel the executing operation if it is no longer ready to run. Re-add for later if it is persistent.
        guard currentOperation.isReady else {
            currentOperation.reAddAfterCancel = currentOperation.notificationModel.persistent
            currentOperation.cancel()
            return
        }
            
        // If the highest priority operation is not executing, and the executing operation is persistent, cancel it.
        if let newOperation = highestReadyOperation,
           currentOperation != newOperation,
           currentOperation.notificationModel.persistent {
            currentOperation.reAddAfterCancel = true
            currentOperation.cancel()
        }
    }
    
    // MARK: - Verify

    /// Returns if any notification is executing
    open func isNotificationShowing() -> Bool {
        return queue.operations.first(where: { operation in
            return operation.isExecuting
        }) != nil
    }
    
    /** Returns if the first executing operation matches the provided predicate.
    *  @param predicate       The predicate block to decide if it is the notification.
    */
    open func hasNotification(using predicate: ((UIView, NotificationModel) -> Bool)) -> Bool {
        return queue.operations.first(where: { operation in
            guard operation.isExecuting,
                  let operation = operation as? NotificationOperation else { return false }
            return predicate(operation.notification, operation.notificationModel)
        }) as? NotificationOperation != nil
    }
    
    /// Returns the current executing notification view and model
    open func getCurrentNotification() async -> (UIView, NotificationModel)? {
        for operation in queue.operations {
            guard operation.isExecuting,
                  let operation = operation as? NotificationOperation,
                  await operation.properties.getIsDisplayed() else { continue }
            return (operation.notification, operation.notificationModel)
        }
        return nil
    }
    
    // MARK: - Show and hide
    
    /// Creates the view and queues up the notification.
    open func showNotification(for model: NotificationModel, delegateObject: MVMCoreUIDelegateObject? = nil) async throws {
        guard !checkAndUpdateExisting(with: model, delegateObject: delegateObject) else { return }
        let view = try await createNotification(with: model, delegateObject: delegateObject)
        let operation = NotificationOperation(with: view, notificationModel: model, transitionDelegate: transitionDelegate)
        add(operation: operation)
    }
    
    @MainActor
    private func createNotification(with model: NotificationModel, delegateObject: MVMCoreUIDelegateObject?) async throws -> UIView & MoleculeViewProtocol {
        guard let view = ModelRegistry.createMolecule(model.molecule, delegateObject: delegateObject, additionalData: nil) else {
            throw ModelRegistry.Error.decoderOther(message: "Molecule not mapped")
        }
        return view
    }

    /// Cancel the current top alert view.
    open func hideNotification() {
        guard let currentOperation = queue.operations.first(where: { operation in
            return operation.isExecuting && !operation.isCancelled
        }) as? NotificationOperation else { return }
        currentOperation.notificationModel.persistent = false
        currentOperation.reAddAfterCancel = false
        currentOperation.cancel()
    }
    
    /** Iterates through all scheduled notifications and cancels any that match the provided predicate.
    *  @param predicate       The predicate block to decide whether to cancel an notification.
    */
    open func cancelNotification(using predicate: ((UIView, NotificationModel) -> Bool)) {
        for case let operation as NotificationOperation in queue.operations {
            if predicate(operation.notification, operation.notificationModel) {
                operation.reAddAfterCancel = false
                operation.cancel()
            }
        }
    }
    
    /// Cancel all notifications, current or pending.
    open func removeAllNotifications() {
        queue.cancelAllOperations()
    }
}

extension NotificationOperation {
    /// Updates the operation and notification with the new model.
    public func update(with model: NotificationModel, delegateObject: MVMCoreUIDelegateObject?) {
        self.notificationModel = model
        queuePriority = model.priority
        guard isExecuting,
              !isCancelled else { return }
        self.log(message: "Operation Updated")
        displayableQueue.async(flags: .barrier) { [self] in
            updateStopTimer()
        }
        Task {
            await transitionDelegate.update(with: notificationModel, delegateObject: delegateObject)
            NotificationHandler.shared()?.onNotificationUpdated.send((notification, notificationModel))
        }
    }
    
    /// Updates if the operation is displayable based on the page type.
    func updateDisplayable(by pageType: String?) {
        guard let pages = notificationModel.pages else {
            isDisplayable = true
            return
        }
        guard let pageType = pageType else {
            isDisplayable = false
            return
        }
        isDisplayable = pages.contains(pageType)
    }
}
