//
//  MessageBus.swift
//  Astro
//
//  Created by Mark Sandstrom on 4/23/15.
//  Copyright (c) 2015 Mobify Research & Development Inc. All rights reserved.
//

import Foundation

/// A helper function for calling a block on the main thread. If this function is called
/// from the main thread `block` is called immediately. Otherwise the block is scheduled
/// to run on the main GCD queue which is executed on the main thread the next time the
/// `NSRunLoop.mainRunLoop()` is run.
private func runOnMainThread(_ block: @escaping ()->Void) {
    if Thread.isMainThread {
        block()
    } else {
        DispatchQueue.main.async {
            block()
        }
    }
}

/// The core Astro components communicate with one another via a `MessageBus`.
/// These components include `AstroWorker`, `PluginManager` and `Plugin`. A
/// `MessageBus` instance routes instances of `Message` to `MessageReceiver`s
/// listening to `MessageAddress`es.
///
/// A message bus simply routes messages. It doesn't know or care about
/// specialized message types such as `RPCMessage`. `MessageReceiver`s are
/// responsible for the handling of messages, and most will find it convenient
/// to inherit from `BaseMessageReceiver` which contains logic for acting on
/// specialized message types, like calling an RPC method shim when an
/// `RPCRequest` message is received.
public class MessageBus {
    private var receivers = [MessageAddress: [MessageReceiver]]()

    /// A convenience method for setting up `receiver` so that it is listening
    /// on its own address. This requires that the `MessageReceiver` is also an
    /// `Addressable`. Receivers that expect to receive `RPCMessage`s should
    /// register themselves using this method.
    ///
    /// - parameter receiver: A `protocol<Addressable, MessageReceiver>` that should be set up to 
    ///                  listen on its own address.
    func register(_ receiver: Addressable & MessageReceiver) {
        listen(on: receiver.address, receiver: receiver)
    }

    /// Set up `receiver` so that it's `MessageReceiver` protocol methods are
    /// called when a message is sent to the `to` `MessageAddress`. Multiple
    /// receivers may listen to messages sent to the same address.
    ///
    /// **Thread safety** You may call listen from any thread.
    ///
    /// - parameter on:       The `MessageAddress` to listen to.
    /// - parameter receiver: The `MessageReceiver` to notify when a message is sent to the `to` address.
    func listen(on address: MessageAddress, receiver: MessageReceiver) {
        runOnMainThread {
            assert(!self.isListening(toAddress: address, receiver: receiver), "Receiver already listening to \"\(address)\"")

            if self.receivers[address] == nil {
                self.receivers[address] = [MessageReceiver]()
            }

            self.receivers[address]!.append(receiver)
        }
    }

    // Remove all references to a given MessageReceiver that is no longer active
    func deregister(_ receiver: MessageReceiver) {
        var receiversToRemove = [MessageAddress]()
        for item in receivers {
            while let index = receivers[item.0]?.firstIndex(where: { $0 === receiver}) {
                receivers[item.0]?.remove(at: index)
                if receivers[item.0]?.count == 0 {
                    receiversToRemove.append(item.0)
                }
            }
        }

        // Remove keys that are now empty
        for item in receiversToRemove {
            receivers.removeValue(forKey: item)
        }
    }

    /// Send a `Message` to receivers listening to the message `to` address on
    /// the message bus.
    ///
    /// Note that the `to` address is a property of `Message`. If you wish to
    /// send a message to multiple addresses you need to construct multiple
    /// messages.
    ///
    /// **Thread safety** You may send a message from any thead, but messages
    /// will always be received on the main thread.
    ///
    /// - parameter message: The `Message` to send.
    func send(_ message: Message) {
        message.messageBus = self

        AstroLog.logger(AstroLog.Messaging).info("<--> Sending message: \(message)")

        runOnMainThread {
            if let receiverList = self.receivers[message.to] {
                for receiver in receiverList {
                    receiver.receive(message)
                }
            }
        }
    }

    /// An internal helper function used to assert that `receiver` isn't set up to listen to the same
    /// `address` multiple times. The assertion cannot be directly tested, so we test this function instead.
    func isListening(toAddress address: MessageAddress, receiver: MessageReceiver) -> Bool {
        if let receiverList = receivers[address] {
            return receiverList.filter { $0 === receiver }.count > 0
        }
        return false
    }
}
