//
//  ServiceClient.swift
//  RosSwift
//
//  Created by Thomas Gustafsson on 2018-10-24.
//

import NIO
import RosTime
import StdMsgs

enum ServiceError: Error {
    case invalidInput(String)
    case noResponse
}

public final class ServiceClient {

    var serverLink: ServiceServerLink?
    let name: String
    let persistent: Bool
    let headerValue: StringStringMap?
    let serviceMd5sum: String
    var isShutdown: Bool = false
    let ros: Ros

    deinit {
        shutdown()
    }

    func shutdown() {
        if !isShutdown {
            if !persistent {
                isShutdown = true
            }
            serverLink = nil
        }
    }

    func isValid() -> Bool {
        if !persistent {
            return true
        }
        if isShutdown {
            return false
        }
        if let link = serverLink {
            return link.isValid()
        }
        return false
    }


    init(ros: Ros, name: String, md5sum: String, persistent: Bool = false, headerValues: StringStringMap? = nil) {
        self.ros = ros
        self.name = name
        self.persistent = persistent
        self.headerValue = headerValues
        self.serviceMd5sum = md5sum

        if persistent {
            serverLink = ros.serviceManager.createServiceServerLink(
                service: name,
                persistent: persistent,
                requestMd5sum: serviceMd5sum,
                responseMd5sum: serviceMd5sum,
                headerValues: headerValue)
        }
    }

    public func call<MReq: ServiceMessage, MRes: ServiceMessage>(req: MReq) -> EventLoopFuture<MRes> {
        if MReq.srvMd5sum != MRes.srvMd5sum {
            fatalError("The request and response parameters to the service " +
                "call must be autogenerated from the same " +
                "server definition file (.srv). your service call " +
                "for \(name) appeared to use request/response types " +
                "from different .srv files. (\(MReq.srvMd5sum) vs. \(MRes.srvMd5sum)")
        }
        let serializedRequest = SerializedMessage(msg: req)

        let result = call(req: serializedRequest, serviceMd5sum: MReq.srvMd5sum).flatMapResult { (ser) -> Result<MRes, Error> in
            do {
                let res: MRes = try deserializeMessage(m: ser)
                return .success(res)
            } catch {
                ROS_ERROR("Exception thrown while while deserializing service call: \(error)")
                return .failure(ServiceError.noResponse)
            }
        }

        return result
    }

    func call(req: SerializedMessage, serviceMd5sum: String) -> EventLoopFuture<SerializedMessage> {

        let eventLoop = threadGroup.next()
        let promise: EventLoopPromise<SerializedMessage> = eventLoop.makePromise()

        guard self.serviceMd5sum == serviceMd5sum else {
            let err = ServiceError.invalidInput("Call to service [\(name)]" +
                " with md5sum [\(serviceMd5sum)] does not match md5sum when the handle was" +
                " created ([\(self.serviceMd5sum)])")
            promise.fail(err)
            return promise.futureResult
        }
        var link: ServiceServerLink?

        if persistent {
            if serverLink == nil {
                serverLink = ros.serviceManager.createServiceServerLink(
                    service: name,
                    persistent: persistent,
                    requestMd5sum: serviceMd5sum,
                    responseMd5sum: serviceMd5sum,
                    headerValues: headerValue)

                if serverLink == nil {
                    promise.fail(ServiceError.invalidInput("Could not create ServiceServerLink"))
                    return promise.futureResult
                }
            }
            link = serverLink
        } else {
            link = ros.serviceManager.createServiceServerLink(
                service: name,
                persistent: persistent,
                requestMd5sum: serviceMd5sum,
                responseMd5sum: serviceMd5sum,
                headerValues: headerValue)
            if link == nil {
                promise.fail(ServiceError.invalidInput("Could not create ServiceServerLink"))
                return promise.futureResult
            }
        }
        promise.succeed(SerializedMessage())

        return link!.call(req: req)
    }


    public func waitForExistence(ros: Ros, timeout: Duration = Duration(seconds: -1)) -> Bool {
        return Service.waitForService(ros: ros, serviceName: name, timeout: timeout)
    }

    public func exists() -> Bool {
        return Service.exists(ros: ros, serviceName: name, printFailureReason: false)
    }

    public func getService() -> String {
        return name
    }

}
