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

import Foundation

/// Used to return a result or error from an RPC method.
public typealias RPCMethodCallback = (RPCMethodResult) -> Void

/// An `RPCMethodShim` is responsible for calling a method with the `//
/// @RpcMethod` pseudo annotation. This includes type checking `receiver` and
/// unpacking and type checking method parameters from `params`.
///
/// RPC methods return results via the `respond` callback. RPC methods are
/// therefore inherently asynchronous, although synchronous semantics can be
/// achieved by calling `respond` immediately at the end of the method.
public typealias RPCMethodShim = (_ params: JSONObject, _ respond: @escaping RPCMethodCallback) -> Void

public class MethodShimInstaller: NSObject {
    override public required init() {}
    @objc open func installShims(_ receiver: AnyObject) {}
}

/// A "Failable" type representing either a result from an RPC method or a
/// error explaining why the method failed.
public enum RPCMethodResult {
    case result(Any)
    case error(String)
}

/// `BaseMessageReceiver` implements specialized `receiveMessage(_:)` methods
/// for the various `Message` types. You will almost always want to inherit
/// from this type instead of conforming to the `MessageReceiver` protocol
/// directly.
///
/// `BaseMessageReceiver` implements the logic for connecting `RPCRequest`
/// messages to methods annotated with the `// @RpcMethod` pseudo annotation
/// (via RPC method shims). The class also handles the creation and dispatch of
/// `RPCResponse` messages.
///
/// If you cannot inherit from `BaseMessageReceiver` because of functionality
/// required from another base class you should consider hosting a
/// `BaseMessageReceiver` and delegating to your class as needed.
open class BaseMessageReceiver: NSObject, MessageReceiver {
    private var rpcMethodShims = [String: RPCMethodShim]()
    private var asyncRPCMethodShims = [String: RPCMethodShim]()

    override init() {
        super.init()
        let module = type(of: self).description().components(separatedBy: ".")[0]

        if let installerClass: AnyClass = NSClassFromString("\(module).AstroMethodShimInstaller") {
            if let installerType = installerClass as? MethodShimInstaller.Type {
                installerType.init().installShims(self)
            }
        }
    }

    /// Conformance to the `MessageReceiver` protocol.
    ///
    /// This method handles the dispatch of RpcMethods. Consider overriding one
    /// of the specialized `receiveMessage(_:)` implementations instead of
    /// overriding this method.
    ///
    /// You must call the super implementation of this method when overriding.
    func receive(_ message: Message) {
        if let rpcMessage = message as? RPCMessage {
            self.receive(rpcMessage)
        } else if let eventMessage = message as? EventMessage {
            self.receive(eventMessage)
        }
    }

    /// This method handles the dispatch of `RPCRequest` and `RPCResponse`
    /// messages.
    ///
    /// Consider overriding one of the specialized `receiveMessage(_:)`
    /// implementations instead of overriding this method.
    ///
    /// You must call the super implementation of this method when overriding.
    func receive(_ message: RPCMessage) {
        if let addressableSelf = self as? Addressable {
            if addressableSelf.address == message.to {
                if let rpcRequest = message as? RPCRequest {
                    receive(rpcRequest)
                } else if let rpcResponse = message as? RPCResponse {
                    receive(rpcResponse)
                }
            } else {
                AstroLog.logger(AstroLog.Messaging).info("*** Warning: \(self) rejecting RPC message for \(message.to)")
            }
        }
    }

    /// This method handles `RPCRequest` messages.
    ///
    /// It implements the logic for calling methods annotated with the `//
    /// @RpcMethod` pseudo annotation (via `RPCMethodShim`s), and creates
    /// `RPCResponse` messages from `RPCMethodResult`s.
    func receive(_ request: RPCRequest) {
        callRPCMethod(request.method, params: request.params) { rpcMethodResult in
#if DEBUG
            if case .error(let message) = rpcMethodResult {
                fatalError("Fatal error processing RPC message: \(message)")
            }
#endif
            let response = request.createResponse(rpcMethodResult)
            request.messageBus?.send(response)
        }
    }

    /// This method handles `RPCResponse` messages.
    ///
    /// Subclasses should override this method to process results. The base
    /// implementation does nothing.
    func receive(_ response: RPCResponse) { }

    func receive(_ event: EventMessage) { }

    /// Call the RPC method with the given `name`, passing `self` as the
    /// `receiver` into the shim. `params` and `respond` are the same
    /// parameters as defined by the `RPCMethodShim` type.
    func callRPCMethod(_ name: String, params: JSONObject, respond: @escaping RPCMethodCallback) {
        if let method = rpcMethodShims[name] {
            // For a synchronous RPC methods, respond with null if respond is not
            // otherwise called.
            var respondCalled = false

            method(params) { result in
                respondCalled = true
                respond(result)
            }

            if !respondCalled {
                respond(.result(NSNull()))
            }
        } else if let method = asyncRPCMethodShims[name] {
            method(params, respond)
        } else {
            respond(.error("Method shim for \"\(name)\" is not registered."))
        }
    }

    public func addRpcMethodShim(_ name: String, shim: @escaping RPCMethodShim) {
        rpcMethodShims[name] = shim
    }

    public func addAsyncRpcMethodShim(_ name: String, shim: @escaping RPCMethodShim) {
        asyncRPCMethodShims[name] = shim
    }
}
