EventDispatcher.swift 9.99 KB
//
//  EventDispatcher.swift
//  SwiftWarplyFramework
//
//  Created by Warply on 10/06/2025.
//  Copyright © 2025 Warply. All rights reserved.
//

import Foundation

// MARK: - Event Protocol

/// Base protocol for all Warply events
public protocol WarplyEvent {
    /// Unique identifier for the event type
    var name: String { get }
    /// Timestamp when the event was created
    var timestamp: Date { get }
    /// Optional data payload for the event
    var data: Any? { get }
}

// MARK: - Event Subscription

/// Represents a subscription to an event
public final class EventSubscription {
    let id: UUID
    let eventName: String
    weak var dispatcher: EventDispatcher?
    
    init(id: UUID, eventName: String, dispatcher: EventDispatcher) {
        self.id = id
        self.eventName = eventName
        self.dispatcher = dispatcher
    }
    
    /// Unsubscribe from the event
    public func unsubscribe() {
        dispatcher?.unsubscribe(self)
    }
    
    deinit {
        unsubscribe()
    }
}

// MARK: - Event Dispatcher

/// Modern Swift event dispatcher to replace SwiftEventBus
public final class EventDispatcher {
    
    // MARK: - Singleton
    public static let shared = EventDispatcher()
    
    // MARK: - Private Properties
    private var subscribers: [String: [UUID: (Any) -> Void]] = [:]
    private let queue = DispatchQueue(label: "com.warply.eventdispatcher", attributes: .concurrent)
    
    // MARK: - Initialization
    private init() {}
    
    // MARK: - Public Methods
    
    /// Post an event to all subscribers
    /// - Parameter event: The event to post
    public func post<T: WarplyEvent>(_ event: T) {
        queue.async(flags: .barrier) {
            let eventName = event.name
            
            if let eventSubscribers = self.subscribers[eventName] {
                DispatchQueue.main.async {
                    for handler in eventSubscribers.values {
                        handler(event)
                    }
                }
            }
        }
    }
    
    /// Post an event with string name and optional sender (for backward compatibility)
    /// - Parameters:
    ///   - eventName: Name of the event
    ///   - sender: Optional sender object
    public func post(_ eventName: String, sender: Any? = nil) {
        let event = GenericWarplyEvent(name: eventName, data: sender)
        post(event)
    }
    
    /// Subscribe to events of a specific type
    /// - Parameters:
    ///   - eventType: The type of event to subscribe to
    ///   - handler: The handler to call when the event is posted
    /// - Returns: An EventSubscription that can be used to unsubscribe
    @discardableResult
    public func subscribe<T: WarplyEvent>(_ eventType: T.Type, handler: @escaping (T) -> Void) -> EventSubscription {
        let eventName = String(describing: eventType)
        return subscribe(eventName) { event in
            if let typedEvent = event as? T {
                handler(typedEvent)
            }
        }
    }
    
    /// Subscribe to events by name (for backward compatibility)
    /// - Parameters:
    ///   - eventName: Name of the event to subscribe to
    ///   - handler: The handler to call when the event is posted
    /// - Returns: An EventSubscription that can be used to unsubscribe
    @discardableResult
    public func subscribe(_ eventName: String, handler: @escaping (Any) -> Void) -> EventSubscription {
        let subscriptionId = UUID()
        
        queue.async(flags: .barrier) {
            if self.subscribers[eventName] == nil {
                self.subscribers[eventName] = [:]
            }
            self.subscribers[eventName]?[subscriptionId] = handler
        }
        
        return EventSubscription(id: subscriptionId, eventName: eventName, dispatcher: self)
    }
    
    /// Unsubscribe from an event
    /// - Parameter subscription: The subscription to remove
    public func unsubscribe(_ subscription: EventSubscription) {
        queue.async(flags: .barrier) {
            self.subscribers[subscription.eventName]?.removeValue(forKey: subscription.id)
            
            // Clean up empty event arrays
            if self.subscribers[subscription.eventName]?.isEmpty == true {
                self.subscribers.removeValue(forKey: subscription.eventName)
            }
        }
    }
    
    /// Remove all subscribers for a specific event name
    /// - Parameter eventName: The event name to clear
    public func removeAllSubscribers(for eventName: String) {
        queue.async(flags: .barrier) {
            self.subscribers.removeValue(forKey: eventName)
        }
    }
    
    /// Remove all subscribers
    public func removeAllSubscribers() {
        queue.async(flags: .barrier) {
            self.subscribers.removeAll()
        }
    }
}

// MARK: - Specific Event Types

/// Generic event for backward compatibility with string-based events
public struct GenericWarplyEvent: WarplyEvent {
    public let name: String
    public let timestamp: Date
    public let data: Any?
    
    public init(name: String, data: Any? = nil) {
        self.name = name
        self.timestamp = Date()
        self.data = data
    }
}

/// Dynatrace analytics event
public struct DynatraceEvent: WarplyEvent {
    public let name: String = "dynatrace"
    public let timestamp: Date
    public let data: Any?
    
    public init(data: Any? = nil) {
        self.timestamp = Date()
        self.data = data
    }
}

/// Campaigns retrieved event
public struct CampaignsRetrievedEvent: WarplyEvent {
    public let name: String = "campaigns_retrieved"
    public let timestamp: Date
    public let data: Any?
    
    public init(data: Any? = nil) {
        self.timestamp = Date()
        self.data = data
    }
}

/// Market pass details fetched event
public struct MarketPassDetailsEvent: WarplyEvent {
    public let name: String = "market_pass_details_fetched"
    public let timestamp: Date
    public let data: Any?
    
    public init(data: Any? = nil) {
        self.timestamp = Date()
        self.data = data
    }
}

/// CCMS campaigns retrieved event
public struct CCMSRetrievedEvent: WarplyEvent {
    public let name: String = "ccms_retrieved"
    public let timestamp: Date
    public let data: Any?
    
    public init(data: Any? = nil) {
        self.timestamp = Date()
        self.data = data
    }
}

/// Seasonal campaigns retrieved event
public struct SeasonalsRetrievedEvent: WarplyEvent {
    public let name: String = "seasonals_retrieved"
    public let timestamp: Date
    public let data: Any?
    
    public init(data: Any? = nil) {
        self.timestamp = Date()
        self.data = data
    }
}

/// Coupons fetched event
public struct CouponsRetrievedEvent: WarplyEvent {
public let name: String = "coupons_fetched"
    public let timestamp: Date
    public let data: Any?

    public init(data: Any? = nil) {
        self.timestamp = Date()
        self.data = data
    }
}

/// Event posted when a Warply push notification is received
/// Equivalent of old ObjC `NB_PushReceived` analytics event
public struct PushNotificationReceivedEvent: WarplyEvent {
    public let name: String = "push_notification_received"
    public let timestamp: Date
    public let data: Any?
    
    /// The session UUID from the push notification payload
    public let sessionUuid: String?
    
    public init(data: Any? = nil, sessionUuid: String? = nil) {
        self.timestamp = Date()
        self.data = data
        self.sessionUuid = sessionUuid
    }
}

/// Event posted when a user interacts with (engages) a Warply push notification
/// Equivalent of old ObjC `NB_PushAck` analytics event
public struct PushNotificationEngagedEvent: WarplyEvent {
    public let name: String = "push_notification_engaged"
    public let timestamp: Date
    public let data: Any?
    
    /// The session UUID from the push notification payload
    public let sessionUuid: String?
    
    public init(data: Any? = nil, sessionUuid: String? = nil) {
        self.timestamp = Date()
        self.data = data
        self.sessionUuid = sessionUuid
    }
}

// MARK: - Convenience Extensions

extension EventDispatcher {
    
    /// Post a dynatrace event
    /// - Parameter sender: The event data
    public func postDynatraceEvent(sender: Any? = nil) {
        post(DynatraceEvent(data: sender))
    }
    
    /// Post a campaigns retrieved event
    /// - Parameter sender: The event data
    public func postCampaignsRetrievedEvent(sender: Any? = nil) {
        post(CampaignsRetrievedEvent(data: sender))
    }
    
    /// Post a market pass details event
    /// - Parameter sender: The event data
    public func postMarketPassDetailsEvent(sender: Any? = nil) {
        post(MarketPassDetailsEvent(data: sender))
    }
    
    /// Post a CCMS retrieved event
    /// - Parameter sender: The event data
    public func postCCMSRetrievedEvent(sender: Any? = nil) {
        post(CCMSRetrievedEvent(data: sender))
    }
    
    /// Post a seasonals retrieved event
    /// - Parameter sender: The event data
    public func postSeasonalsRetrievedEvent(sender: Any? = nil) {
        post(SeasonalsRetrievedEvent(data: sender))
    }
    
    /// Post a coupons retrieved event
    /// - Parameter sender: The event data
    public func postCouponsRetrievedEvent(sender: Any? = nil) {
        post(CouponsRetrievedEvent(data: sender))
    }
    
    /// Post a push notification received event
    /// - Parameters:
    ///   - sender: The event data (typically the raw push payload)
    ///   - sessionUuid: The session UUID from the push notification
    public func postPushNotificationReceivedEvent(sender: Any? = nil, sessionUuid: String? = nil) {
        post(PushNotificationReceivedEvent(data: sender, sessionUuid: sessionUuid))
    }
    
    /// Post a push notification engaged event
    /// - Parameters:
    ///   - sender: The event data (typically the raw push payload)
    ///   - sessionUuid: The session UUID from the push notification
    public func postPushNotificationEngagedEvent(sender: Any? = nil, sessionUuid: String? = nil) {
        post(PushNotificationEngagedEvent(data: sender, sessionUuid: sessionUuid))
    }
}