/*
 * @Copyright (c) Huawei Technologies Co., Ltd. 2023-2024. All rights reserved.
 */
package mqtt4cj.mqttv3

/**
 * Bridge between Receiver and the external API.
 * This class gets called by Receiver,
 * and then converts the comms-centric MQTT message objects
 * Int64o ones understood by the external API.
 */
class CommsCallback {
    private static let CLASS_NAME: String = "CommsCallback"
    private static let INBOUND_QUEUE_SIZE: Int64 = 10
    private let log: IMqttLogger = LoggerFactory.getLogger(MQTT_CLIENT_MSG_CAT, CLASS_NAME)
    private let lifecycle: Mutex = Mutex()
    private let workAvailable: Mutex = Mutex()
    private let workAvailableCondition = synchronized(workAvailable){workAvailable.condition()}
    private let spaceAvailable: Mutex = Mutex()
    private let spaceAvailableCondition = synchronized(spaceAvailable){spaceAvailable.condition()}

    private var current_state: CallbackState = CallbackState.STOPPED
    private var target_state: CallbackState = CallbackState.STOPPED
    private var manualAcks: Bool = false
    private var threadName: String = CLASS_NAME

    private let clientComms: ClientComms
    private let callbacksWildcards: ConcurrentHashMap<String, IMqttMessageListener> // topicFilter with wildcards -> messageHandler
    private let callbacksDirect: ConcurrentHashMap<String, IMqttMessageListener> // topicFilter without wildcards -> messageHandler
    private let messageQueue: ConcurrentLinkedQueue<MqttWireMessage>
    private let completeQueue: ConcurrentLinkedQueue<MqttToken>
    private let messageQueueLock = Mutex()
    private let completeQueueLock = Mutex()

    private var mqttCallback: ?MqttCallback = None
    private var reconnectInternalCallback: ?MqttCallbackExtended = None
    private var callbackThread: ?Thread = None
    private var callbackFuture: ?Future<Unit> = None
    private var clientState: ?ClientState = None

    init(clientComms: ClientComms) {
        this.clientComms = clientComms
        this.messageQueue = ConcurrentLinkedQueue<MqttWireMessage>()
        this.completeQueue = ConcurrentLinkedQueue<MqttToken>()
        this.callbacksWildcards = ConcurrentHashMap<String, IMqttMessageListener>()
        this.callbacksDirect = ConcurrentHashMap<String, IMqttMessageListener>()
        log.setResourceName(clientComms.getClient().getClientId())
    }

    public func setClientState(clientState: ClientState): Unit {
        this.clientState = clientState
    }

    /**
     * Starts up the Callback thread.
     *
     * @param threadName      The name of the thread
     */
    public func start(threadName: String): Unit {
        this.threadName = threadName

        synchronized(lifecycle) {
            if (current_state == CallbackState.STOPPED) {
                // Preparatory work before starting the background thread.
                // For safety ensure any old events are cleared.
                messageQueue.clear()
                completeQueue.clear()

                target_state = CallbackState.RUNNING
                current_state = CallbackState.RUNNING
                let future = spawn {=> this.run()}
                callbackFuture = future
                callbackThread = future.thread
            }
        }

        let stoppedStateCounter: AtomicInt64 = AtomicInt64(0)
        while (!isRunning()) {
            sleep(Duration.millisecond * 100)
            if (current_state == CallbackState.STOPPED) {
                if (stoppedStateCounter.fetchAdd(1) + 1 > CommsSender.MAX_STOPPED_STATE_TO_STOP_THREAD) {
                    break
                }
            } else {
                stoppedStateCounter.store(0)
            }
        }
    }

    /**
     * Stops the callback thread.
     * This call will block until stop has completed.
     */
    public func stop(): Unit {
        let methodName: String = "stop"

        if (isRunning()) {
            // @TRACE 700=stopping
            log.trace(CLASS_NAME, methodName, "700")
            synchronized(lifecycle) {
                target_state = CallbackState.STOPPED
            }
            // Do not allow a thread to wait for itself.
            if (Thread.currentThread != callbackThread()) {
                synchronized(workAvailable) {
                    // @TRACE 701=notify workAvailable and wait for run
                    // to finish
                    log.trace(CLASS_NAME, methodName, "701")
                    workAvailableCondition.notifyAll()
                }
                // Wait for the thread to finish.
                if (callbackFuture.isSome() && callbackFuture().thread != Thread.currentThread) {
                    try {
                        callbackFuture?.cancel()
                        callbackFuture?.get(SHUTDOWN_TIMEOUT)
                    } catch (_) {
                    }
                }
            }
            // @TRACE 703=stopped
            log.trace(CLASS_NAME, methodName, "703")
        }
    }

    public func setCallback(mqttCallback: MqttCallback): Unit {
        this.mqttCallback = mqttCallback
    }

    public func setReconnectCallback(callback: MqttCallbackExtended): Unit {
        this.reconnectInternalCallback = callback
    }

    public func setManualAcks(manualAcks: Bool): Unit {
        this.manualAcks = manualAcks
    }

    public func run(): Unit {
        let methodName: String = "run"
        Thread.currentThread.name = threadName
        callbackThread = Thread.currentThread

        while (isRunning()) {
            try {
                // If no work is currently available, then wait until there is some...
                synchronized(workAvailable) {
                    if (isRunning() && messageQueue.size == 0 && completeQueue.size == 0) {
                        // @TRACE 704=wait for workAvailable
                        log.trace(CLASS_NAME, methodName, "704")
                        workAvailableCondition.wait()
                    }
                }

                if (isRunning()) {
                    // Check for deliveryComplete callbacks...
                    var token: ?MqttToken = None
                    synchronized(completeQueueLock) {
                        if (completeQueue.size > 0) {
                            // First call the delivery arrived callback if needed
                            token = completeQueue.remove()
                        }
                    }
                    if (token.isSome()) {
                        handleActionComplete(token())
                    }

                    // Check for messageArrived callbacks...
                    var message: ?MqttPublish = None
                    synchronized(messageQueueLock) {
                        if (messageQueue.size > 0) {
                            // Note, there is a window on connect where a publish 
                            // could arrive before we've finished the connect logic.
                            message = match (messageQueue.remove()) {
                                case Some(v: MqttPublish) => v
                                case _ => None
                            }
                        }
                    }
                    if (message.isSome()) {
                        handleMessage(message())
                    }
                }

                if (isQuiescing()) {
                    clientState?.checkQuiesceLock()
                }
            } catch (ex: Exception) {
                // Users code could throw an Error or Exception e.g. in the case
                // of class NoClassDefFoundError
                // @TRACE 714=callback threw exception
                log.trace(CLASS_NAME, methodName, "714", None, ex)

                clientComms.shutdownConnection(None, MqttException(ex))
            } finally {
                synchronized(spaceAvailable) {
                    // Notify the spaceAvailable lock, to say that there's now
                    // some space on the queue...

                    // @TRACE 706=notify spaceAvailable
                    log.trace(CLASS_NAME, methodName, "706")
                    spaceAvailableCondition.notifyAll()
                }
            }
        }
        synchronized(lifecycle) {
            current_state = CallbackState.STOPPED
        }
    }

    private func handleActionComplete(token: MqttToken): Unit {
        let methodName: String = "handleActionComplete"
        synchronized(token.monitor) {
            // @TRACE 705=callback and notify for key={0}
            log.trace(CLASS_NAME, methodName, "705", [token.internalTok.getKey()])
            if (token.isComplete()) {
                // Finish by doing any post processing such as delete
                // from persistent store but only do so if the action
                // is complete
                clientState?.notifyComplete(token)
            }

            // Unblock any waiters and if pending complete now set completed
            token.internalTok.notifyComplete()

            if (!token.internalTok.isNotified()) {
                // If a callback is registered and delivery has finished
                // call delivery complete callback.
                if (mqttCallback.isSome() && token is MqttDeliveryToken && token.isComplete()) {
                    mqttCallback?.deliveryComplete((token as MqttDeliveryToken)())
                }
                // Now call async action completion callbacks
                fireActionEvent(token)
            }

            // Set notified so we don't tell the user again about this action.
            if (token.isComplete()) {
                if (token is MqttDeliveryToken) {
                    token.internalTok.setNotified(true)
                }
            }
        }
    }

    /**
     * This method is called when the connection to the server is lost.
     * If there is no cause then it was a clean disconnect.
     * The connectionLost callback will be invoked if registered and run on
     * the thread that requested shutdown e.g. receiver or sender thread.
     * If the request was a user initiated disconnect then the disconnect token will be notified.
     *
     * @param cause the reason behind the loss of connection.
     */
    public func connectionLost(cause: ?MqttException): Unit {
        let methodName: String = "connectionLost"
        // If there was a problem and a client callback has been set inform
        // the connection lost listener of the problem.
        try {
            if (mqttCallback.isSome() && cause.isSome()) {
                // @TRACE 708=call connectionLost
                log.trace(CLASS_NAME, methodName, "708", [cause])
                mqttCallback?.connectionLost(cause())
            }
            if (reconnectInternalCallback.isSome() && cause.isSome()) {
                reconnectInternalCallback?.connectionLost(cause())
            }
        } catch (t: Exception) {
            // Just log the fact that a throwable has caught connection lost
            // is called during shutdown processing so no need to do anything else
            // @TRACE 720=exception from connectionLost {0}
            log.trace(CLASS_NAME, methodName, "720", [t])
        }
    }

    /**
     * An action has completed - if a completion listener has been set on the
     * token then invoke it with the outcome of the action.
     *
     * @param token The MqttToken that has completed
     */
    public func fireActionEvent(token: MqttToken): Unit {
        let methodName: String = "fireActionEvent"

        let asyncCB: ?IMqttActionListener = token.getActionCallback()
        if (asyncCB.isSome()) {
            if (token.getException().isNone()) {
                // @TRACE 716=call onSuccess key={0}
                log.trace(CLASS_NAME, methodName, "716", [token.internalTok.getKey()])
                asyncCB?.onSuccess(token)
            } else {
                // @TRACE 717=call onFailure key {0}
                log.trace(CLASS_NAME, methodName, "716", [token.internalTok.getKey()])
                asyncCB?.onFailure(token, token.getException()())
            }
        }
    }

    /**
     * This method is called when a message arrives on a topic.
     * Messages are only added to the queue for inbound messages if the client is not quiescing.
     *
     * @param sendMessage the MQTT SEND message.
     */
    public func messageArrived(sendMessage: MqttPublish): Unit {
        let methodName: String = "messageArrived"
        if (mqttCallback.isSome() || !callbacksWildcards.isEmpty() || !callbacksDirect.isEmpty()) {
            // If we already have enough messages queued up in memory, 
            // wait until some more queue space becomes available. 
            // This helps the client protect itself from getting flooded by messages from the server.
            synchronized(spaceAvailable) {
                while (isRunning() && !isQuiescing() && messageQueue.size >= INBOUND_QUEUE_SIZE) { /*cjlint-ignore !G.EXP.03 */
                    try {
                        // @TRACE 709=wait for spaceAvailable
                        log.trace(CLASS_NAME, methodName, "709")
                        spaceAvailableCondition.wait(timeout: Duration.millisecond * 200)
                    } catch (ex: Exception) {
                    }
                }
            }
            if (!isQuiescing()) {
                messageQueue.add(sendMessage)
                // Notify the CommsCallback thread that there's work to do...
                synchronized(workAvailable) {
                    // @TRACE 710= msg avail, notify workAvailable
                    log.trace(CLASS_NAME, methodName, "710")
                    workAvailableCondition.notifyAll()
                }
            }
        }
    }

    /**
     * Let the call back thread quiesce.
     * Prevent inbound messages being added to the process queue and let existing work quiesce.
     * (until the thread is told to shutdown).
     */
    public func quiesce(): Unit {
        let methodName: String = "quiesce"
        synchronized(lifecycle) {
            if (current_state == CallbackState.RUNNING) {
                current_state = CallbackState.QUIESCING
            }
        }
        synchronized(spaceAvailable) {
            // @TRACE 711=quiesce notify spaceAvailable
            log.trace(CLASS_NAME, methodName, "711")
            // Unblock anything waiting for space...
            spaceAvailableCondition.notifyAll()
        }
    }

    public func isQuiesced(): Bool {
        return isQuiescing() && completeQueue.size == 0 && messageQueue.size == 0
    }

    private func handleMessage(publishMessage: MqttPublish): Unit {
        let methodName: String = "handleMessage"
        // If quisecing process any pending messages.

        let destName: String = publishMessage.getTopicName()

        // @TRACE 713=call messageArrived key={0} topic={1}
        log.trace(CLASS_NAME, methodName, "713", [publishMessage.getMessageId(), destName])
        deliverMessage(destName, publishMessage.getMessageId(), publishMessage.getMessage())

        if (!this.manualAcks) {
            if (publishMessage.getMessage().getQos() == 1) {
                this.clientComms.internalSend(
                    MqttPubAck(publishMessage),
                    MqttToken(clientComms.getClient().getClientId())
                )
            } else if (publishMessage.getMessage().getQos() == 2) {
                this.clientComms.deliveryComplete(publishMessage)
                let pubComp: MqttPubComp = MqttPubComp(publishMessage)
                this.clientComms.internalSend(pubComp, MqttToken(clientComms.getClient().getClientId()))
            }
        }
    }

    public func messageArrivedComplete(messageId: Int64, qos: Int64): Unit {
        if (qos == 1) {
            this.clientComms.internalSend(MqttPubAck(messageId), MqttToken(clientComms.getClient().getClientId()))
        } else if (qos == 2) {
            this.clientComms.deliveryComplete(messageId)
            let pubComp: MqttPubComp = MqttPubComp(messageId)
            this.clientComms.internalSend(pubComp, MqttToken(clientComms.getClient().getClientId()))
        }
    }

    public func asyncOperationComplete(token: MqttToken): Unit {
        let methodName: String = "asyncOperationComplete"

        if (isRunning()) {
            // invoke callbacks on callback thread
            completeQueue.add(token)
            synchronized(workAvailable) {
                // @TRACE 715= workAvailable. key={0}
                log.trace(CLASS_NAME, methodName, "715", [token.internalTok.getKey()])
                workAvailableCondition.notifyAll()
            }
        } else {
            // invoke async callback on invokers thread
            try {
                handleActionComplete(token)
            } catch (ex: Exception) {
                // Users code could throw an Error or Exception e.g. in the case
                // of class NoClassDefFoundError
                // @TRACE 719=callback threw ex:
                log.trace(CLASS_NAME, methodName, "719", None, ex)

                // Shutdown likely already in progress but no harm to confirm
                clientComms.shutdownConnection(None, MqttException(ex))
            }
        }
    }

    /**
     * Returns the thread used by this callback.
     *
     * @return The Thread
     */
    protected func getThread(): ?Thread {
        return callbackThread
    }

    public func setMessageListener(topicFilter: String, messageListener: IMqttMessageListener): Unit {
        if (topicFilter.contains("#") || topicFilter.contains("+")) {
            this.callbacksWildcards.add(topicFilter, messageListener)
        } else {
            this.callbacksDirect.add(topicFilter, messageListener)
        }
    }

    public func removeMessageListener(topicFilter: String): Unit {
        this.callbacksWildcards.remove(topicFilter) // no exception thrown if the filter was not present
        this.callbacksDirect.remove(topicFilter) // no exception thrown if the filter was not present
    }

    public func removeMessageListeners(): Unit {
        this.callbacksWildcards.clear()
        this.callbacksDirect.clear()
    }

    protected func deliverMessage(topicName: String, messageId: Int64, aMessage: MqttMessage): Bool {
        var delivered: Bool = false

        let callback: ?IMqttMessageListener = this.callbacksDirect.get(topicName)
        if (callback.isSome()) {
            aMessage.setId(messageId)
            callback?.messageArrived(topicName, aMessage)
            delivered = true
        }

        for ((topicFilter, callback) in callbacksWildcards) {
            // callback may already have been removed in the meantime, so a None check is necessary
            if (MqttTopic.isMatched(topicFilter, topicName)) {
                aMessage.setId(messageId)
                callback.messageArrived(topicName, aMessage)
                delivered = true
            }
        }

        /* if the message hasn't been delivered to a per subscription handler, give it to the default handler */
        if (mqttCallback.isSome() && !delivered) {
            aMessage.setId(messageId)
            mqttCallback?.messageArrived(topicName, aMessage)
            delivered = true
        }

        return delivered
    }

    public func isRunning(): Bool {
        let result: Bool
        synchronized(lifecycle) {
            result = ((current_state == CallbackState.RUNNING || current_state == CallbackState.QUIESCING) &&
                target_state == CallbackState.RUNNING)
        }
        return result
    }

    public func isQuiescing(): Bool {
        let result: Bool
        synchronized(lifecycle) {
            result = (current_state == CallbackState.QUIESCING)
        }
        return result
    }
}

enum CallbackState {
    | STOPPED
    | RUNNING
    | QUIESCING

    operator func ==(that: CallbackState): Bool {
        match ((this, that)) {
            case (STOPPED, STOPPED) => true
            case (RUNNING, RUNNING) => true
            case (QUIESCING, QUIESCING) => true
            case _ => false
        }
    }
}

public interface IDiscardedBufferMessageCallback {
    func messageDiscarded(message: MqttWireMessage): Unit
}

public interface IDisconnectedBufferCallback {
    func publishBufferedMessage(bufferedMessage: BufferedMessage): Unit
}