# Copyright (c) {{ year }} {{ author }}. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from functools import partial

from rclpy.lifecycle import Node
from rclpy.lifecycle import State
from rclpy.lifecycle import TransitionCallbackReturn
from rcl_interfaces.msg import ParameterDescriptor

from hri import HRIListener
from hri_actions_msgs.msg import Intent

from diagnostic_msgs.msg import DiagnosticArray, DiagnosticStatus, KeyValue


class IntentExtractorImpl(Node):
    """
    Implementation of {{id}}.

    This is the main class for the node. It is a ROS2  node that uses the
    lifecycle feature of ROS 2 to manage its states.

    The purpose of this node is to recognise *user intents* using a chatbot.
    This template is very simple, and should be used as a starting point to
    integrate your own chatbot system.
    """

    def __init__(self) -> None:
        """Construct the node."""
        super().__init__('intent_extractor_{{ id }}')

        # Declare ROS parameters. Should mimick the one listed in config/00-defaults.yaml
        self.declare_parameter(
          'my_parameter', "my_default_value.",
          ParameterDescriptor(description='Important parameter for my chatbot')
        )

        self.get_logger().info("Initialising...")

        self._intent_pub = None

        self._timer = None
        self._diag_pub = None
        self._diag_timer = None

        self._nb_requests = 0

        self.get_logger().info('Chatbot {{id}} started, but not yet configured.')

    def on_user_input(self, input, user_id):
        """
        Return the chatbot's response to a text input.

        Implement here the logic to process the natural text input
        and return a reponse made of a combination of text to be spoken back, and
        possible recognized user intents.
        """
        self.get_logger().info(f"input from {user_id}: {input}")
        self._nb_requests += 1

        # here, you need to retrieve the user intent, and map it to the
        # Intent.msg semantics.
        #
        # For now, we just try to recognise a greeting
        if "hello" in input:
            intent = Intent(intent=Intent.GREET,
                            source=user_id,
                            modality=Intent.MODALITY_SPEECH,
                            confidence=.8)
            self.get_logger().warn("I think the user want to greet me. Sending a GREET intent")
        else:
            self.get_logger().warn("Unable to recognise a particular intent! "
                                   "forwarding a 'RAW_USER_INPUT'")
            intent = Intent(intent=Intent.RAW_USER_INPUT,
                            source=user_id,
                            modality=Intent.MODALITY_SPEECH,
                            confidence=1.0,
                            data=f'"input": "{input}"')

        self._intent_pub.publish(intent)

    #################################
    #
    # Lifecycle transitions callbacks
    #
    def on_configure(self, state: State) -> TransitionCallbackReturn:

        self._intent_pub = self.create_publisher(Intent, '/intents', 1)

        # configure and start diagnostics publishing
        self._nb_requests = 0
        self._diag_pub = self.create_publisher(DiagnosticArray, '/diagnostics', 1)
        self._diag_timer = self.create_timer(1., self.publish_diagnostics)

        self.get_logger().info("Chatbot {{id}} is configured, but not yet active")
        return TransitionCallbackReturn.SUCCESS

    def on_activate(self, state: State) -> TransitionCallbackReturn:
        """
        Activate the node.

        You usually want to do the following in this state:
        - Create and start any timers performing periodic tasks
        - Start processing data, and accepting action goals, if any

        """
        self._hri_listener = HRIListener("__internal_{{id}}_hri__")
        self._hri_listener.on_voice(lambda voice:
                                    voice.on_speech(partial(self.on_user_input, user_id=voice.id))
                                    )

        # Define a timer that fires every second to call the run function
        timer_period = 1  # in sec
        self._timer = self.create_timer(timer_period, self.run)

        self.get_logger().info("Chatbot {{id}} is active and running")
        return super().on_activate(state)

    def on_deactivate(self, state: State) -> TransitionCallbackReturn:
        """Stop the timer to stop calling the `run` function (main task of your application)."""
        self.get_logger().info("Stopping chatbot...")

        self.destroy_timer(self._timer)
        self.destroy_subscription(self._intents_sub)
        self._hri_listener = None

        self.get_logger().info("Chatbot {{id}} is stopped (inactive)")
        return super().on_deactivate(state)

    def on_shutdown(self, state: State) -> TransitionCallbackReturn:
        """
        Shutdown the node, after a shutting-down transition is requested.

        :return: The state machine either invokes a transition to the
            "finalized" state or stays in the current state depending on the
            return value.
            TransitionCallbackReturn.SUCCESS transitions to "finalized".
            TransitionCallbackReturn.FAILURE remains in current state.
            TransitionCallbackReturn.ERROR or any uncaught exceptions to
            "errorprocessing"
        """
        self.get_logger().info('Shutting down {{id}} node.')
        self.destroy_timer(self._diag_timer)
        self.destroy_publisher(self._diag_pub)

        self.get_logger().info("Chatbot {{id}} finalized.")
        return TransitionCallbackReturn.SUCCESS

    #################################

    def publish_diagnostics(self):

        arr = DiagnosticArray()
        msg = DiagnosticStatus(
            level=DiagnosticStatus.OK,
            name="/intent_extractor_{{ id }}",
            message="chatbot {{id}} is running",
            values=[
                KeyValue(key="Module name", value="{{ id }}"),
                KeyValue(key="Current lifecycle state",
                         value=self._state_machine.current_state[1]),
                KeyValue(key="# requests since start", value=str(self._nb_requests)),
            ],
        )

        arr.header.stamp = self.get_clock().now().to_msg()
        arr.status = [msg]
        self._diag_pub.publish(arr)

    def run(self) -> None:
        """
        Background task of the chatbot.

        For now, we do not need to do anything here, as the chatbot is
        event-driven, and the `on_user_input` callback is called when a new
        user input is received.
        """
        pass

