import threading
from functools import wraps
from types import TracebackType
from typing import Callable, Optional

import jarvis_os.yapper.meta as meta
from jarvis_os.yapper.enhancer import BaseEnhancer, NoEnhancer
from jarvis_os.yapper.speaker import BaseSpeaker, PiperSpeaker


class Yapper:
    def __init__(
        self,
        enhancer: Optional[BaseEnhancer] = None,
        speaker: Optional[BaseSpeaker] = None,
        plain: bool = False,
        block: bool = True,
        use_stdout: bool = False,
    ):
        """
        Parameters
        ----------
        enhancer : Optional[BaseEnhancer]
            enhancer to be used for enhancing text
            (default: DefaultEnhancer).
        speaker : Optional[BaseSpeaker]
            speaker to be used for speaking the text
            (default: DefaultSpeaker).
        plain : bool, optional
            Do not enhance text, say it as it is (default: False).
        block: bool, optional
            wait for speech-syntheis to complete (default: True).
        use_stdout: bool, optional
            print the enhanced text before saying it (default: False).
        """
        self.enhancer = enhancer or NoEnhancer()
        self.speaker = speaker or PiperSpeaker()
        self.plain = plain
        self.block = block
        self.use_stdout = use_stdout

    def yap(
        self,
        text: str,
        plain: Optional[bool] = None,
        block: Optional[bool] = None,
        use_stdout: Optional[bool] = None,
    ):
        """
        Speaks the given text.

        Parameters
        ----------
        text : str
            The text to speak.
        plain : bool, optional
            Do not enhance text, say it as it is.
        block: Optional[bool]
            wait for speech-syntheis to complete.
        use_stdout: bool, optional
            print the enhanced text before saying it.
        """
        if plain is None:
            plain = self.plain
        if block is None:
            block = self.block
        if use_stdout is None:
            use_stdout = self.use_stdout

        def func(
            text: str,
            plain: bool,
            use_stdout: bool,
        ):
            if not plain:
                text = self.enhancer.enhance(text)
            if use_stdout:
                print(f"{meta.name}: {text}")
            self.speaker.say(text)

        if block:
            func(text, plain, use_stdout)
        else:
            threading.Thread(
                target=func, args=(text, plain, use_stdout)
            ).start()

    def __enter__(self):
        return self

    def __exit__(
        self,
        exc_type: Optional[Exception],
        exc_value: Optional[str],
        tb: Optional[TracebackType],
    ):
        if exc_type:
            err = exc_type.__name__ + (f": {exc_value}" if exc_value else "")
            self.yap(err)
        return not exc_type

    def __call__(self, on_call: bool = False, pass_yapper: bool = False):
        """
        Parameters
        ----------
        on_call : bool, optional
            announce when the decorated function starts running
            (default: False).
        pass_yapper: bool, optional
            pass the yapper instance to the decorated function as a
            keyword argument 'yapper' when it is called (default: False).
        """

        def decorator(func: Callable):
            @wraps(func)
            def wrapper(*args, **kwargs):
                if on_call:
                    f_desc = f"running function {func.__name__}"
                    if func.__doc__ is not None:
                        f_desc += "\n" + f"description: {func.__doc__}"
                    self.yap(f_desc)
                try:
                    if not pass_yapper:
                        return func(*args, **kwargs)
                    return func(*args, **kwargs, yapper=self)
                except Exception as e:
                    err = e.__class__.__name__
                    desc = str(e)
                    if desc:
                        err = f"{err}: {desc}"
                    self.yap(err)
                    raise e

            return wrapper

        return decorator
