//! Client module for {{stinger.name}} IPC
//!
//! This module is only available when the "client" feature is enabled.

/*
DO NOT MODIFY THIS FILE{# <-- Ignore this because you are editing the template file. #}.  It is automatically generated and changes will be over-written
on the next generation.

This is the Client for the {{stinger.name}} interface.
*/
#[cfg(feature = "client")]
use mqttier::{MqttierClient, ReceivedMessage};
{%if stinger.methods|length > 0 -%}
use std::collections::HashMap;
use uuid::Uuid;
{%endif-%}
use serde_json;

#[allow(unused_imports)]
use crate::payloads::{MethodReturnCode, *};
#[allow(unused_imports)]
use iso8601_duration::Duration as IsoDuration;
use std::sync::{Arc, Mutex};
use tokio::sync::{broadcast, mpsc{%if stinger.methods|length > 0 -%}, oneshot{%endif-%}{%if stinger.properties|length > 0 -%}, watch{%endif-%}};
use tokio::task::JoinError;
#[allow(unused_imports)]
use tracing::{debug, error, info, warn};

/// This struct is used to store all the MQTTv5 subscription ids
/// for the subscriptions the client will make.
#[derive(Clone, Debug)]
struct {{stinger.name | UpperCamelCase }}SubscriptionIds {
    {%for method_name, method in stinger.methods.items()-%}
    {{method_name | snake_case}}_method_resp: usize,
    {%endfor%}
    {%for sig_name, sig in stinger.signals.items()-%}
    {{sig_name | snake_case}}_signal: Option<usize>,
    {%-endfor%}
    {%for prop_name, prop in stinger.properties.items()-%}
    {{prop_name | snake_case}}_property_value: usize,
    {%endfor%}
}

/// This struct holds the tx side of a broadcast channels used when receiving signals.
/// The rx side of the broadcast channels can be created from the tx side later.
/// When {{stinger.rust.client_struct_name}} gets a message and determines that it
/// is a signal, it will send the signal payload via the tx channel that is in this struct.
#[derive(Clone)]
struct {{stinger.name | UpperCamelCase }}SignalChannels {
    {%for sig_name, sig in stinger.signals.items()-%}
    {{sig_name|snake_case}}_sender: broadcast::Sender<{%if sig.arg_list | length == 1 %}{{sig.arg_list[0].rust_type}}{%elif sig.arg_list | length == 0 %}(){% else %}{{sig_name | UpperCamelCase}}SignalPayload{% endif %}>,
    {%endfor%}
}

{%if stinger.properties | length > 0%}
#[derive(Clone)]
pub struct {{stinger.name | UpperCamelCase}}Properties {
    {%for prop_name, prop in stinger.properties.items()-%}
    {%-if prop.arg_list | length > 1%}
    pub {{prop_name | snake_case}}: Arc<Mutex<Option<{{prop.rust_type}}>>>,
    {%-else%}
    pub {{prop_name | snake_case}}: Arc<Mutex<Option<{{prop.arg_list[0].rust_type}}>>>,
    {%endif%}
    {{prop_name | snake_case}}_tx_channel: watch::Sender<Option<{%if prop.arg_list | length > 1 %}{{prop.rust_type}}{% else %}{{prop.arg_list[0].rust_type}}{% endif %}>>,
    {%-endfor%}
}
{%endif%}

/// This is the struct for our API client.
#[derive(Clone)]
pub struct {{stinger.rust.client_struct_name}} {
    mqttier_client: MqttierClient,
    {%if stinger.methods | length > 0 %}/// Temporarily holds oneshot channels for responses to method calls.
    pending_responses: Arc<Mutex<HashMap<Uuid, oneshot::Sender<String>>>>,
    {%endif%}

    /// Temporarily holds the receiver for the MPSC channel.  The Receiver will be moved
    /// to a process loop when it is needed.  MQTT messages will be received with this.
    msg_streamer_rx: Arc<Mutex<Option<mpsc::Receiver<ReceivedMessage>>>>,

    /// The Sender side of MQTT messages that are received from the broker.  This tx
    /// side is cloned for each subscription made.
    #[allow(dead_code)]
    msg_streamer_tx: mpsc::Sender<ReceivedMessage>,
    {%if stinger.properties|length > 0%}
    /// Struct contains all the properties.
    pub properties: {{stinger.name | UpperCamelCase}}Properties,
    {%endif%}
    /// Contains all the MQTTv5 subscription ids.
    subscription_ids: {{stinger.name | UpperCamelCase }}SubscriptionIds,

    {%-if stinger.signals | length > 0 %}
    /// Holds the channels used for sending signals to the application.
    signal_channels: {{stinger.name | UpperCamelCase }}SignalChannels,
    {%endif%}
    /// Copy of MQTT Client ID
    pub client_id: String,
    {%if stinger.methods | length > 0 %}
    /// Instance ID of the server
    service_instance_id: String,
    {%endif%}
}

impl {{stinger.rust.client_struct_name}} {

    /// Creates a new {{stinger.rust.client_struct_name}} that uses an MqttierClient.
    pub async fn new(connection: &mut MqttierClient, service_id: String) -> Self {

        // Create a channel for messages to get from the Connection object to this {{stinger.rust.client_struct_name}} object.
        // The Connection object uses a clone of the tx side of the channel.
        let (message_received_tx, message_received_rx) = mpsc::channel(64);

        {%for method_name, method in stinger.methods.items()-%}
        let topic_{{method_name | snake_case}}_method_resp = format!("{{method.response_topic('{}')}}", connection.client_id);
        let subscription_id_{{method_name | snake_case}}_method_resp = connection.subscribe(topic_{{method_name | snake_case}}_method_resp, 2, message_received_tx.clone()).await;
        let subscription_id_{{method_name | snake_case}}_method_resp = subscription_id_{{method_name | snake_case}}_method_resp.unwrap_or_else(|_| usize::MAX);
        {%endfor%}

        // Subscribe to all the topics needed for signals.
        {%for signal_name, signal in stinger.signals.items()-%}
        let topic_{{signal_name | snake_case}}_signal = format!("{{signal.topic}}", service_id);
        let subscription_id_{{signal_name | snake_case}}_signal = connection.subscribe(topic_{{signal_name | snake_case}}_signal, 2, message_received_tx.clone()).await;
        let subscription_id_{{signal_name | snake_case}}_signal = subscription_id_{{signal_name | snake_case}}_signal.unwrap_or_else(|_| usize::MAX);
        {%endfor%}

        // Subscribe to all the topics needed for properties.
        {% for prop_name, prop in stinger.properties.items() %}
        let topic_{{prop_name | snake_case}}_property_value = format!("{{prop.value_topic}}", service_id);
        let subscription_id_{{prop_name | snake_case}}_property_value = connection.subscribe(topic_{{prop_name | snake_case}}_property_value, 1, message_received_tx.clone()).await;
        let subscription_id_{{prop_name | snake_case}}_property_value = subscription_id_{{prop_name | snake_case}}_property_value.unwrap_or_else(|_| usize::MAX);
        {% endfor %}

        {%if stinger.properties|length > 0%}
        let property_values = {{stinger.name | UpperCamelCase}}Properties {
        {%-for prop_name, prop in stinger.properties.items()%}
            {%if prop.arg_list | length > 1 -%}
            {{prop_name | snake_case}}: Arc::new(Mutex::new(None)),
            {%-else%}
            {{prop_name | snake_case}}: Arc::new(Mutex::new(None)),
            {%-endif-%}
            {{prop_name | snake_case}}_tx_channel: watch::channel(None).0,
        {%-endfor%}
        };
        {%endif%}
        // Create structure for subscription ids.
        let sub_ids = {{stinger.name | UpperCamelCase }}SubscriptionIds {
            {%for method_name, method in stinger.methods.items()-%}
            {{method_name | snake_case}}_method_resp: subscription_id_{{method_name | snake_case}}_method_resp,
            {%endfor%}
            {%-for sig_name, sig in stinger.signals.items()-%}
            {{sig_name | snake_case}}_signal: Some(subscription_id_{{sig_name | snake_case}}_signal),
            {%endfor%}
            {%-for prop_name, prop in stinger.properties.items()-%}
            {{prop_name | snake_case}}_property_value: subscription_id_{{prop_name | snake_case}}_property_value,
            {%endfor%}
        };

        {%if stinger.signals | length > 0 %}// Create structure for the tx side of broadcast channels for signals.
        let signal_channels = {{stinger.name | UpperCamelCase }}SignalChannels {
            {%for sig_name, sig in stinger.signals.items()-%}
            {{sig_name|snake_case}}_sender: broadcast::channel(64).0,
            {%endfor%}
        };{%endif%}

        // Create {{stinger.rust.client_struct_name}} structure.
        let inst = {{stinger.rust.client_struct_name}} {
            mqttier_client: connection.clone(),
            {%if stinger.methods | length > 0 %}pending_responses: Arc::new(Mutex::new(HashMap::new())),{%endif%}
            msg_streamer_rx: Arc::new(Mutex::new(Some(message_received_rx))),
            msg_streamer_tx: message_received_tx,
            {%if stinger.properties|length > 0 %}
            properties: property_values,
            {%endif%}
            subscription_ids: sub_ids,
            {%if stinger.signals | length > 0 %}signal_channels: signal_channels,{%endif%}
            client_id: connection.client_id.to_string(),
            {%if stinger.methods | length > 0 %}
            service_instance_id: service_id,
            {%endif%}
        };
        inst
    }

    {%for sig_name, sig in stinger.signals.items()-%}
    /// Get the RX receiver side of the broadcast channel for the {{sig_name}} signal.
    /// The signal payload, `{{sig_name | UpperCamelCase}}SignalPayload`, will be put onto the channel whenever it is received.
    pub fn get_{{sig_name|snake_case}}_receiver(&self) -> broadcast::Receiver<{%if sig.arg_list|length == 0%}(){%elif sig.arg_list|length == 1%}{{sig.arg_list[0].rust_type}}{%else%}{{sig_name | UpperCamelCase}}SignalPayload{%endif%}> {
        self.signal_channels.{{sig_name | snake_case}}_sender.subscribe()
    }
    {%endfor%}

    {%for method_name, method in stinger.methods.items()-%}

    async fn start_{{method_name|snake_case}}(&mut self{%if method.arg_list | length > 0 %}, {%endif%}{%for arg in method.arg_list%}{{arg.name|snake_case}}: {{arg.rust_type}}{%if not loop.last%}, {%endif%}{%endfor%}) -> oneshot::Receiver<String> {
        // Setup tracking for the future response.
        let correlation_id = Uuid::new_v4();
        let correlation_data = correlation_id.as_bytes().to_vec();
        let (sender, receiver) = oneshot::channel();
        {
            let mut hashmap = self.pending_responses.lock().expect("Mutex was poisoned");
            hashmap.insert(correlation_id.clone(), sender);
        }

        let data = {{method_name | UpperCamelCase}}RequestObject {
            {%-for arg in method.arg_list%}
            {{arg.name|snake_case}}: {{arg.name|snake_case}},
            {%-endfor%}
        };

        let response_topic: String = format!("{{method.response_topic('{}')}}", self.client_id);
        let _ = self.mqttier_client.publish_request(format!("{{method.topic}}", self.service_instance_id), &data, response_topic, correlation_data).await;
        receiver
    }

    /// The `{{method_name}}` method.
    /// Method arguments are packed into a {{method_name | UpperCamelCase}}RequestObject structure
    /// and published to the `{{method.topic}}` MQTT topic.
    ///
    /// This method awaits on the response to the call before returning.
    pub async fn {{method_name|snake_case}}(&mut self{%if method.arg_list | length > 0 %}, {%endif%}{%for arg in method.arg_list%}{{arg.name|snake_case}}: {{arg.rust_type}}{%if not loop.last%}, {%endif%}{%endfor%})->Result<{{method.return_value_rust_type}}, MethodReturnCode> {

        let receiver = self.start_{{method_name|snake_case}}({%for arg in method.arg_list%}{{arg.name|snake_case}}{%if not loop.last%}, {%endif%}{%endfor%}).await;

        let {%if method.return_value_type is false %}_{%endif%}resp_str: String = receiver.await.unwrap();
        
        {%if method.return_arg_list |length > 0%}
        let return_values: {{method_name|UpperCamelCase}}ReturnValues = serde_json::from_str(&resp_str)
            .map_err(|e| MethodReturnCode::ClientDeserializationError(e.to_string()))?;
        {%endif%}
        
        {%if method.return_arg_list |length == 0 %}
        Ok(())
        {%elif method.return_arg_list |length == 1 %}
        Ok(return_values.{{method.return_arg_list[0].name}})
        {%else%}
        Ok(return_values)
        {%endif%}
    }

    {%endfor%}
    {%for prop_name, prop in stinger.properties.items()%}
    /// Watch for changes to the `{{prop_name}}` property.
    /// This returns a watch::Receiver that can be awaited on for changes to the property value.
    pub fn watch_{{prop_name | snake_case}}(&self) -> watch::Receiver<Option<{%if prop.arg_list | length > 1 %}{{prop.rust_type}}{% else %}{{prop.arg_list[0].rust_type}}{% endif %}>> {
        self.properties.{{prop_name | snake_case}}_tx_channel.subscribe()
    }
    {%if not prop.read_only %}
    pub fn set_{{prop_name | snake_case}}(&mut self, value: {%if prop.arg_list | length > 1 %}{{prop.rust_type}}{% else %}{{prop.arg_list[0].rust_type}}{% endif %}) -> Result<(), MethodReturnCode> {
        let data = {%if prop.arg_list | length > 1 %}value{% else %}{{prop_name | UpperCamelCase}}Property { 
            {{prop.arg_list[0].name}}: value
        } {%- endif %};
        let _publish_result = self.mqttier_client.publish_structure("{{prop.update_topic}}".to_string(), &data);
        Ok(())
    }
    {%endif%}
    {%endfor%}

    /// Starts the tasks that process messages received.
    pub async fn run_loop(&self) -> Result<(), JoinError> {
        // Make sure the MqttierClient is connected and running.
        let _ = self.mqttier_client.run_loop().await;

        {%if stinger.methods | length > 0%}// Clone the Arc pointer to the map.  This will be moved into the loop_task.
        let resp_map: Arc<Mutex<HashMap::<Uuid, oneshot::Sender::<String>>>> = self.pending_responses.clone();
        {%endif%}
        // Take ownership of the RX channel that receives MQTT messages.  This will be moved into the loop_task.
        let mut message_receiver = {
            let mut guard = self.msg_streamer_rx.lock().expect("Mutex was poisoned");
            guard.take().expect("msg_streamer_rx should be Some")
        };
        {%if stinger.signals | length > 0 %}
        let sig_chans = self.signal_channels.clone();
        {%endif%}
        let sub_ids = self.subscription_ids.clone();
        {%-if stinger.properties|length > 0%}
        let props = self.properties.clone();
        {%endif%}
        let _loop_task = tokio::spawn(async move {
            while let Some(msg) = message_receiver.recv().await {
                {% if stinger.methods|length > 0 %}
                let opt_corr_data: Option<Vec<u8>> = msg.correlation_data.clone();
                let opt_corr_id: Option<Uuid> = opt_corr_data.and_then(|b| Uuid::from_slice(b.as_slice()).ok());

                let payload = String::from_utf8_lossy(&msg.payload).to_string();
                {%-for method_name, method in stinger.methods.items()%}
                {%if not loop.first%}else {%endif%}if msg.subscription_id == sub_ids.{{method_name | snake_case}}_method_resp { // TODO: Simplify subscription because we'll always look up by correlation id.
                    if opt_corr_id.is_some() {
                        let opt_sender = opt_corr_id
                            .and_then(|uuid| {
                                let mut hashmap = resp_map.lock().expect("Mutex was poisoned");
                                hashmap.remove(&uuid)
                            });
                        if let Some(sender) = opt_sender {
                            let oss: oneshot::Sender<String> = sender;
                            match oss.send(payload) {
                                Ok(_) => (),
                                Err(_) => (),
                            }
                        }
                    }
                }
                {%-endfor%}
                {%endif%}
                {%-for signal_name, signal in stinger.signals.items()%}
                {%if not loop.first%}else {%endif%}if msg.subscription_id == sub_ids.{{signal_name | snake_case}}_signal.unwrap_or_default() {
                    let chan = sig_chans.{{signal_name | snake_case}}_sender.clone();
                    {%if signal.arg_list | length == 0 %}
                    let _send_result = chan.send(());
                    {%elif signal.arg_list | length == 1 %}
                    match serde_json::from_slice::<{{signal_name | UpperCamelCase}}SignalPayload>(&msg.payload) {
                        Ok(pl) => {
                            let _send_result = chan.send(pl.{{signal.arg_list[0].name|snake_case}});
                        },
                        Err(e) => {
                            warn!("Failed to deserialize '{}' into {{signal_name | UpperCamelCase}}SignalPayload: {}", String::from_utf8_lossy(&msg.payload), e);
                            continue;
                        }
                    }
                    {%else%}
                    match serde_json::from_slice::<{{signal_name | UpperCamelCase}}SignalPayload>(&msg.payload) {
                        Ok(pl) => {
                            let _send_result = chan.send(pl);
                        },
                        Err(e) => {
                            warn!("Failed to deserialize '{}' into {{signal_name | UpperCamelCase}}SignalPayload: {}", String::from_utf8_lossy(&msg.payload), e);
                            continue;
                        }
                    }
                    {%endif%}
                }
                {%endfor%}
                {%-for prop_name, prop in stinger.properties.items()%}
                {%if not loop.first%}else {%endif%}if msg.subscription_id == sub_ids.{{prop_name | snake_case}}_property_value {
                    match serde_json::from_slice::<{{prop_name | UpperCamelCase}}Property >(&msg.payload) {
                        Ok(pl) => {
                            let mut guard = props.{{prop_name | snake_case}}.lock().expect("Mutex was poisoned");
                            {%if prop.arg_list | length == 1 %}
                            *guard = Some(pl.{{prop.arg_list[0].name}}.clone());
                            let _ = props.{{prop_name | snake_case}}_tx_channel.send(Some(pl.{{prop.arg_list[0].name}}));
                            {%else%}
                            *guard = Some(pl.clone());
                            let _ = props.{{prop_name | snake_case}}_tx_channel.send(Some(pl));
                            {%endif%}
                        },
                        Err(e) => {
                            warn!("Failed to deserialize '{}' into {{signal_name | UpperCamelCase}}SignalPayload: {}", String::from_utf8_lossy(&msg.payload), e);
                            continue;
                        }
                    }
                }
                {%endfor%}
            }   
        });

        Ok(())
    }
}
