//
//  ViewController.swift
//  MVMCoreUI
//
//  Created by Scott Pfeil on 11/5/19.
//  Copyright © 2019 Verizon Wireless. All rights reserved.
//

import UIKit

@objc open class ViewController: UIViewController, MVMCoreViewControllerProtocol, MVMCoreViewManagerViewControllerProtocol, MoleculeDelegateProtocol, FormHolderProtocol, MVMCoreActionDelegateProtocol, MVMCoreLoadDelegateProtocol, UITextFieldDelegate, UITextViewDelegate, ObservingTextFieldDelegate, MVMCoreUIDetailViewProtocol, PageProtocol, PageBehaviorHandlerProtocol {
    
    //--------------------------------------------------
    // MARK: - Properties
    //--------------------------------------------------
    
    @objc public var pageType: String?
    @objc public var loadObject: MVMCoreLoadObject?
    public var model: MVMControllerModelProtocol?
    public var pageModel: PageModelProtocol? {
        get { model }
        set { model = newValue as? MVMControllerModelProtocol }
    }
    
    /// Set if this page is containted in a manager.
    public weak var manager: (UIViewController & MVMCoreViewManagerProtocol)?
    
    /// A temporary iVar backer for delegateObject() until we change the protocol
    public lazy var delegateObjectIVar: MVMCoreUIDelegateObject = {
        MVMCoreUIDelegateObject.create(withDelegateForAll: self)
    }()
    
    public func delegateObject() -> DelegateObject? { delegateObjectIVar }
    
    public var formValidator: FormValidator?
    
    public var behaviors: [PageBehaviorProtocol]?
    
    public var needsUpdateUI = false
    private var observingForResponses = false
    private var initialLoadFinished = false
    public var previousScreenSize = CGSize.zero
    
    public var selectedField: UIView?
    
    // Stores the previous tab bar index.
    public var tabBarIndex: Int?
    
    /// Checks if the screen width has changed
    open func screenSizeChanged() -> Bool {
        !MVMCoreGetterUtility.cgfequalwiththreshold(previousScreenSize.width, view.bounds.size.width, 0.1)
    }
    
    //--------------------------------------------------
    // MARK: - Response handling
    //--------------------------------------------------
    
    open func observeForResponseJSONUpdates() {
        guard !observingForResponses,
              (pagesToListenFor()?.count ?? 0 > 0 || modulesToListenFor()?.count ?? 0 > 0)
        else { return }
        
        observingForResponses = true
        NotificationCenter.default.addObserver(self, selector: #selector(responseJSONUpdated(notification:)), name: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil)
    }
    
    open func stopObservingForResponseJSONUpdates() {
        guard observingForResponses else { return }
        NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil)
        observingForResponses = false
    }
    
    open func pagesToListenFor() -> [String]? {
        guard let pageType = loadObject?.pageType else { return nil }
        return [pageType]
    }
    
    open func modulesToListenFor() -> [String]? {
        loadObject?.requestParameters?.allModules()
    }
    
    @objc open func responseJSONUpdated(notification: Notification) {
        // Checks for a page we are listening for.
        var newData = false
        if let pagesLoaded = notification.userInfo?.optionalDictionaryForKey(KeyPageMap),
           let pageType = pagesToListenFor()?.first(where: { (pageTypeListened) -> Bool in
            guard let page = pagesLoaded.optionalDictionaryForKey(pageTypeListened),
                  let pageType = page.optionalStringForKey(KeyPageType),
                  pageType == pageTypeListened
            else { return false }
            
            return true
           }) {
            newData = true
            loadObject?.pageJSON = pagesLoaded.optionalDictionaryForKey(pageType)
        }
        
        // Checks for modules we are listening for.
        if let modulesLoaded = notification.userInfo?.optionalDictionaryForKey(KeyModuleMap),
           let modulesListened = modulesToListenFor() {
            for moduleName in modulesListened {
                if let module = modulesLoaded.optionalDictionaryForKey(moduleName) {
                    newData = true
                    var currentModules = loadObject?.modulesJSON ?? [:]
                    currentModules.updateValue(module, forKey: moduleName)
                    loadObject?.modulesJSON = currentModules
                }
            }
        }
        
        guard newData else { return }
        
        do {
            try parsePageJSON()
            MVMCoreDispatchUtility.performBlock(onMainThread: {
                self.handleNewDataAndUpdateUI()
            })
        } catch {
            if let coreError = MVMCoreErrorObject.createErrorObject(for: error, location: "updateJSON for pageType: \(String(describing: pageType))") {
                MVMCoreLoggingHandler.addError(toLog: coreError)
            }
        }
    }
    
    open func shouldFinishProcessingLoad(_ loadObject: MVMCoreLoadObject, error: AutoreleasingUnsafeMutablePointer<MVMCoreErrorObject?>) -> Bool {
        pageType = loadObject.pageType
        self.loadObject = loadObject
        
        // Verifies all modules needed are loaded.
        guard ViewController.verifyRequiredModulesLoaded(for: loadObject, error: error) else { return false }
        
        // Parse the model for the page.
        do {
            try parsePageJSON()
        } catch let parsingError {
            // Log all parsing errors and fail load.
            handleLoggingFor(parsingError: parsingError)
            if let errorObject = MVMCoreErrorObject.createErrorObject(for: parsingError, location: MVMCoreLoadHandler.sharedGlobal()?.errorLocation(forRequest: loadObject)) {
                errorObject.messageToDisplay = MVMCoreGetterUtility.hardcodedString(withKey: HardcodedErrorUnableToProcess)
                error.pointee = errorObject
            }
            return false
        }
        
        if let pageData = loadObject.dataForPage {
            executeBehaviors { (behavior: PageLocalDataShareBehavior) in
                behavior.receiveLocalPageData(pageData, delegateObjectIVar)
            }
        }
        
        return true
    }
    
    func handleLoggingFor(parsingError: Error) {
        if let registryError = parsingError as? ModelRegistry.Error {
            switch (registryError) {
            case .decoderErrorModelNotMapped(let identifier, let codingKey, let codingPath) where identifier != nil && codingKey != nil && codingPath != nil:
                MVMCoreLoggingHandler.shared()?.handleDebugMessage("Error parsing template: Model identifier \"\(identifier!)\" is not mapped for  \"\(codingKey!.stringValue)\" @ \(codingPath!.map { return $0.stringValue })")
                
            case .decoderErrorObjectNotPresent(let codingKey, codingPath: let codingPath):
                MVMCoreLoggingHandler.shared()?.handleDebugMessage("Error parsing template: Required model \"\(codingKey.stringValue)\" was not found @ \(codingPath.map { return $0.stringValue })")
                
            default:
                MVMCoreLoggingHandler.shared()?.handleDebugMessage("Error parsing template: Registry error: \(registryError)")
            }
        }
        if let decodingError = parsingError as? DecodingError {
            switch (decodingError) {
            case .keyNotFound(let codingKey, let context):
                MVMCoreLoggingHandler.shared()?.handleDebugMessage("Error parsing template: Key \(codingKey.stringValue) was not found @ \(context.codingPath.map { return $0.stringValue })")
                
            case .valueNotFound(_, let context):
                MVMCoreLoggingHandler.shared()?.handleDebugMessage("Error parsing template: Value not found @ \(context.codingPath.map { return $0.stringValue })")
                
            case .typeMismatch(_, let context):
                MVMCoreLoggingHandler.shared()?.handleDebugMessage("Error parsing template: Type mismatch @ \(context.codingPath.map { return $0.stringValue })")
                
            case .dataCorrupted(let context):
                MVMCoreLoggingHandler.shared()?.handleDebugMessage("Error parsing template: Data corrupted @ \(context.codingPath.map { return $0.stringValue })")
                
            @unknown default:
                MVMCoreLoggingHandler.shared()?.handleDebugMessage("Error parsing template: \(parsingError)")
            }
        }
    }
    
    open func parsePageJSON() throws { }
    
    open class func verifyRequiredModulesLoaded(for loadObject: MVMCoreLoadObject?, error: AutoreleasingUnsafeMutablePointer<MVMCoreErrorObject?>) -> Bool {
        guard let pageType = loadObject?.pageType,
              var modulesRequired = MVMCoreUIViewControllerMappingObject.shared()?.modulesRequired(forPageType: pageType),
              !modulesRequired.isEmpty
        else { return true }
        
        guard let loadedModules = loadObject?.modulesJSON else { return false }
        
        for case let key as String in Array(loadedModules.keys) {
            guard modulesRequired.count > 0 else { break }
            if let index = modulesRequired.firstIndex(where: {($0 as? String) == key}) {
                modulesRequired.remove(at: index)
            }
        }
        
        guard modulesRequired.count == 0 else {
            if let errorObject = MVMCoreErrorObject(title: nil, message: MVMCoreGetterUtility.hardcodedString(withKey: HardcodedErrorCritical), messageToLog: modulesRequired.description, code: ErrorCode.requiredModuleNotPresent.rawValue, domain: ErrorDomainNative, location: MVMCoreLoadHandler.sharedGlobal()?.errorLocation(forRequest: loadObject!)) { 
                error.pointee = errorObject
            }
            return false
        }
        return true
    }
    
    /// Calls processNewData and then sets the ui to update with updateView
    open func handleNewDataAndUpdateUI() {
        handleNewData()
        needsUpdateUI = true
        view.setNeedsLayout()
    }
    
    /// Creates a legacy navigation model.
    open func createDefaultLegacyNavigationModel() -> NavigationItemModel {
        let navigationModel = NavigationItemModel()
        navigationModel.title = model?.screenHeading
        return navigationModel
    }
    
    /// Processes any new data. Called after the page is loaded the first time and on response updates for this page,
    open func handleNewData() {
        if model?.navigationBar == nil {
            let navigationItem = createDefaultLegacyNavigationModel()
            model?.navigationBar = navigationItem
        }
        
        executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in
            behavior.onPageNew(rootMolecules: getRootMolecules(), delegateObjectIVar)
        }
        
        if formValidator == nil {
            let rules = model?.formRules
            formValidator = FormValidator(rules)
        }
        
        if let backgroundColor = model?.backgroundColor {
            view.backgroundColor = backgroundColor.uiColor
        }
        
        // Update splitview properties
        if self == MVMCoreUISplitViewController.main()?.getCurrentDetailViewController() {
            MVMCoreUISplitViewController.main()?.setBottomProgressBarProgress(bottomProgress() ?? 0)
            updateTabBar()
        }
        
        // Notify the manager of new data
        manager?.newDataReceived?(in: self)
    }
    
    //--------------------------------------------------
    // MARK: - Navigation Item
    //--------------------------------------------------
    
    open func getNavigationModel() -> NavigationItemModelProtocol? {
        // TODO: remove legacy. Temporary, convert legacy to navigation model.
        if model?.navigationBar == nil {
            let navigationItem = createDefaultLegacyNavigationModel()
            model?.navigationBar = navigationItem
        }
        return model?.navigationBar
    }
    
    //--------------------------------------------------
    // MARK: - TabBar
    //--------------------------------------------------
    
    open func updateTabBar() {
        guard MVMCoreUISplitViewController.main()?.getCurrentDetailViewController() == self else { return }
        MVMCoreUISplitViewController.main()?.tabBar?.delegateObject = delegateObjectIVar
        
        if let index = (model as? TabPageModelProtocol)?.tabBarIndex {
            MVMCoreUISplitViewController.main()?.tabBar?.highlightTab(at: index)
        } else if let index = loadObject?.requestParameters?.actionMap?["tabBarIndex"] as? Int {
            MVMCoreUISplitViewController.main()?.tabBar?.highlightTab(at: index)
        } else if let index = self.tabBarIndex {
            MVMCoreUISplitViewController.main()?.tabBar?.highlightTab(at: index)
        } else if let index = MVMCoreUISplitViewController.main()?.tabBar?.currentTabIndex() {
            // Store current tab index for cases like back button.
            self.tabBarIndex = index
        }
        
        if let hidden = (model as? TabPageModelProtocol)?.tabBarHidden {
            MVMCoreUISplitViewController.main()?.updateTabBarShowing(!hidden)
        } else if let hidden = loadObject?.requestParameters?.actionMap?["tabBarHidden"] as? Bool {
            MVMCoreUISplitViewController.main()?.updateTabBarShowing(!hidden)
        } else {
            MVMCoreUISplitViewController.main()?.updateTabBarShowing(true)
        }
    }
    
    //--------------------------------------------------
    // MARK: - View Lifecycle
    //--------------------------------------------------
    
    /// Called only once in viewDidLoad
    open func initialLoad() {
        observeForResponseJSONUpdates()
    }
    
    /// Called on screen size update.
    open func updateViews() {
        _ = formValidator?.validate()
    }
    
    override open func viewDidLoad() {
        super.viewDidLoad()
        
        // Do any additional setup after loading the view.
        MVMCoreLoggingHandler.logDebugMessage(withDelegate: "View Controller Loaded : \(self)")
        
        // We use our own margins.
        viewRespectsSystemMinimumLayoutMargins = false
        
        // Presents from the bottom.
        modalPresentationStyle = MVMCoreGetterUtility.isOnIPad() ? .formSheet : .overCurrentContext
        
        // Do some initial loading.
        if !initialLoadFinished {
            initialLoadFinished = true
            initialLoad()
        }
        
        handleNewDataAndUpdateUI()
    }
    
    open override func viewDidLayoutSubviews() {
        // Add to fix a constraint bug where the width is zero and things get messed up.
        guard isViewLoaded, view.bounds.width > 1 else {
            super.viewDidLayoutSubviews()
            return
        }
        
        // First update should be explicit (hence the zero check)
        if needsUpdateUI || (previousScreenSize != .zero && screenSizeChanged()) {
            needsUpdateUI = false
            updateViews()
        }
        
        previousScreenSize = view.bounds.size;
        
        super.viewDidLayoutSubviews()
    }
    
    open func pageShown() {
        // Update split view properties if this is the current detail controller.
        if self == MVMCoreUISplitViewController.main()?.getCurrentDetailViewController() {
            MVMCoreUISplitViewController.main()?.setBottomProgressBarProgress(bottomProgress() ?? 0)
            updateTabBar()
        }
        
        // Track.
        MVMCoreUISession.sharedGlobal()?.currentPageType = pageType
        MVMCoreUILoggingHandler.shared()?.defaultLogPageState(forController: self)
        
        executeBehaviors { [weak self] (behavior: PageVisibilityBehavior) in
            behavior.onPageShown(self?.delegateObjectIVar)
        }
    }
    
    open override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        if manager == nil {
            pageShown()
        }
    }
    
    open override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        
        executeBehaviors { [weak self] (behavior: PageVisibilityBehavior) in
            behavior.onPageHidden(self?.delegateObjectIVar)
        }
    }
    
    deinit {
        stopObservingForResponseJSONUpdates()
        MVMCoreLoggingHandler.logDebugMessage(withDelegate: "View Controller Deallocated : \(self)")
    }
    
    open override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        MVMCoreGetterUtility.isOnIPad() ? UIInterfaceOrientationMask.all : UIInterfaceOrientationMask.portrait
    }
    
    open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        
        // Updates the detail view width
        coordinator.animate(alongsideTransition: { UIViewControllerTransitionCoordinatorContext in
        }) { UIViewControllerTransitionCoordinatorContext in
            self.view.setNeedsLayout()
        }
    }
    //--------------------------------------------------
    // MARK: - MVMCoreViewManagerViewControllerProtocol
    //--------------------------------------------------
    
    open func viewControllerReady(inManager manager: UIViewController & MVMCoreViewManagerProtocol) {
        pageShown()
    }
    
    //--------------------------------------------------
    // MARK: - MVMCoreLoadDelegateProtocol
    //--------------------------------------------------
    
    // TODO: Move this function out of here after architecture cleanup.
    open func loadFinished(_ loadObject: MVMCoreLoadObject?, loadedViewController: (UIViewController & MVMCoreViewControllerProtocol)?, error: MVMCoreErrorObject?) {
        
        MVMCoreUILoggingHandler.log(withDelegateLoadFinished: loadObject, loadedViewController: loadedViewController, error: error)
        
        // Open the support panel
        if error == nil,
           loadObject?.requestParameters?.openSupportPanel ?? (loadObject?.systemParametersJSON?.boolForKey(KeyOpenSupport) ?? false) == true {
            MVMCoreUISession.sharedGlobal()?.splitViewController?.showRightPanel(animated: true)
        }
    }
    
    /// Override this method to avoid adding form params.
    open func addFormParams(requestParameters: MVMCoreRequestParameters, actionInformation: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?) {
        formValidator?.addFormParams(requestParameters: requestParameters, model: additionalData?[KeySourceModel] as? MoleculeModelProtocol & FormItemProtocol)
    }
    
    public func handleFieldErrors(_ fieldErrors: [Any]?, loadObject: MVMCoreLoadObject) {
        
        for case let fieldError as [AnyHashable: Any] in fieldErrors ?? [] {
            
            guard let fieldName = fieldError["fieldName"] as? String,
                  let userError = fieldError["userMessage"] as? String,
                  let entryFieldModel = formValidator?.fields[fieldName] as? EntryFieldModel
            else { continue }
            
            entryFieldModel.shouldClearText = fieldError["clearText"] as? Bool ?? true
            entryFieldModel.dynamicErrorMessage = userError
        }
    }
    
    //--------------------------------------------------
    // MARK: - MVMCoreActionDelegateProtocol
    //--------------------------------------------------
    
    open func handleOpenPage(for requestParameters: MVMCoreRequestParameters, actionInformation: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?) {
        addFormParams(requestParameters: requestParameters, actionInformation: actionInformation, additionalData: additionalData)
        requestParameters.parentPageType = loadObject?.pageJSON?.optionalStringForKey("parentPageType")
        var pageForwardedData = additionalData ?? [:]
        
        executeBehaviors { (behavior: PageLocalDataShareBehavior) in
            let dataMap = behavior.compileLocalPageDataForTransfer(delegateObjectIVar)
            pageForwardedData.merge(dataMap) { current, _ in current }
        }
        
        MVMCoreActionHandler.defaultHandleOpenPage(for: requestParameters, actionInformation: actionInformation, additionalData: pageForwardedData, delegateObject: delegateObject())
    }
    
    open func logAction(withActionInformation actionInformation: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?) {
        MVMCoreUILoggingHandler.shared()?.defaultLogAction(forController: self, actionInformation: actionInformation, additionalData: additionalData)
    }
    
    open func handleUnknownActionType(_ actionType: String?, actionInformation: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?) {
        var handled = false
        
        executeBehaviors { (behavior: PageCustomActionHandlerBehavior) in
            if !handled {
                handled = behavior.handleAction(type: actionType, information: actionInformation, additionalData: additionalData)
            }
        }
        
        if !handled {
            MVMCoreUIActionHandler.defaultHandleUnknownActionType(actionType, actionInformation: actionInformation, additionalData: additionalData, delegateObject: delegateObjectIVar)
        }
    }
    
    //--------------------------------------------------
    // MARK: - MoleculeDelegateProtocol
    //--------------------------------------------------
    
    open func getRootMolecules() -> [MoleculeModelProtocol] {
        model?.rootMolecules ?? []
    }
    
    open func getModuleWithName(_ name: String?) -> [AnyHashable: Any]? {
        guard let name = name else { return nil }
        return loadObject?.modulesJSON?.optionalDictionaryForKey(name)
    }
    
    open func getModuleWithName(_ moleculeName: String) -> MoleculeModelProtocol? {
        guard let moduleJSON = loadObject?.modulesJSON?.optionalDictionaryForKey(moleculeName),
              let moleculeName = moduleJSON.optionalStringForKey("moleculeName"),
              let modelType = ModelRegistry.getType(for: moleculeName, with: MoleculeModelProtocol.self)
        else { return nil }
        
        do {
            return try modelType.decode(jsonDict: moduleJSON) as? MoleculeModelProtocol
        } catch {
            MVMCoreUILoggingHandler.logDebugMessage(withDelegate: "error: \(error)")
        }
        
        return nil
    }
    
    // Needed otherwise when subclassed, the extension gets called.
    open func moleculeLayoutUpdated(_ molecule: MoleculeViewProtocol) { }
    
    //--------------------------------------------------
    // MARK: - MVMCoreUIDetailViewProtocol
    //--------------------------------------------------
    
    public func isLeftPanelAccessible() -> Bool {
        // TODO: Remove when hamburger menu is fully phased out.
        if loadObject?.pageJSON?.boolForKey(KeyHideMainMenu) ?? false {
            return false
        }
        return MVMCoreUISession.sharedGlobal()?.launchAppLoadedSuccessfully ?? false
    }
    
    public func isRightPanelAccessible() -> Bool {
        // TODO: Remove when FAB is 100%.
        if loadObject?.pageJSON?.boolForKey(KeyHideMainMenu) ?? false {
            return false
        }
        return (MVMCoreUISession.sharedGlobal()?.launchAppLoadedSuccessfully ?? false) || showRightPanelForScreenBeforeLaunchApp()
    }
    
    open func showRightPanelForScreenBeforeLaunchApp() -> Bool {
        loadObject?.pageJSON?.lenientBoolForKey("showRightPanel") ?? false
    }
    
    // TODO: make molecular
    open func isOverridingRightButton() -> Bool {
        guard let rightPanelLink = loadObject?.pageJSON?.optionalDictionaryForKey("rightPanelButtonLink")
        else { return false }
        MVMCoreActionHandler.shared()?.handleAction(with: rightPanelLink, additionalData: nil, delegateObject: delegateObject())
        return true
    }
    
    // TODO: make molecular
    open func isOverridingLeftButton() -> Bool {
        guard let leftPanelLink = loadObject?.pageJSON?.optionalDictionaryForKey("leftPanelButtonLink")
        else { return false }
        MVMCoreActionHandler.shared()?.handleAction(with: leftPanelLink, additionalData: nil, delegateObject: delegateObject())
        return true
    }
    
    // Eventually will be moved to Model
    open func bottomProgress() -> Float? {
        guard let progressString = loadObject?.pageJSON?.optionalStringForKey(KeyProgressPercent),
              let progress = Float(progressString)
        else { return nil }
        
        return progress / Float(100)
    }
    
    //--------------------------------------------------
    // MARK: - UITextFieldDelegate
    //--------------------------------------------------
    
    // To Remove TextFields Bug: Keyboard is not dismissing after reaching textfield max length limit
    open func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        return true
    }
    
    open func textFieldDidBeginEditing(_ textField: UITextField) {
        selectedField = textField
        
        // TODO: Make this into a protocol
        if UIAccessibility.isVoiceOverRunning {
            if let toolBar = textField.inputAccessoryView as? UIToolbar,
               let _ = toolBar.items?.last,
               let pickerView = textField.inputView as? UIPickerView {
                
                view.accessibilityElements = [pickerView, toolBar]
            }
            
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                UIAccessibility.post(notification: .layoutChanged, argument: textField.inputView)
            }
        }
    }
    
    open func textFieldDidEndEditing(_ textField: UITextField, reason: UITextField.DidEndEditingReason) {
        
        if textField === selectedField {
            if UIAccessibility.isVoiceOverRunning {
                view.accessibilityElements = nil
                UIAccessibility.post(notification: .layoutChanged, argument: textField)
            }
            
            selectedField = nil
        }
    }
    
    @objc open func dismissFieldInput(_ sender: Any?) {
        selectedField?.resignFirstResponder()
    }
    
    //--------------------------------------------------
    // MARK: - UITextViewDelegate
    //--------------------------------------------------
    
    public func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
        selectedField = textView
        return true
    }
    
    open func textViewDidBeginEditing(_ textView: UITextView) {
        selectedField = textView
    }
    
    open func textViewDidEndEditing(_ textView: UITextView) {
        if textView === selectedField {
            selectedField = nil
        }
    }
    
    //--------------------------------------------------
    // MARK: - Behavior Execution
    //--------------------------------------------------
    
    public func executeBehaviors<T>(_ behaviorBlock: (_ behavior: T) -> Void) {
        behaviors?.compactMap { $0 as? T }.forEach { behaviorBlock($0) }
    }
}
