import os
import time
from typing import Optional, Tuple, Union, Dict, Callable, Any

from samsungtvws import SamsungTVWS

from platypush.config import Config
from platypush.plugins import Plugin, action


class TvSamsungWsPlugin(Plugin):
    """
    Control a Samsung smart TV with Tizen OS over WiFi/ethernet. It should support any post-2016 Samsung with Tizen OS
    and enabled websocket-based connection.
    """

    def __init__(
        self,
        host: Optional[str] = None,
        port: int = 8002,
        timeout: Optional[int] = 5,
        name='platypush',
        token_file: Optional[str] = None,
        **kwargs,
    ):
        """
        :param host: IP address or host name of the smart TV.
        :param port: Websocket port (default: 8002).
        :param timeout: Connection timeout in seconds (default: 5, specify 0 or None for no timeout).
        :param name: Name of the remote device (default: platypush).
        :param token_file: Path to the token file (default: ``~/.local/share/platypush/samsungtvws/token.txt``)
        """
        super().__init__(**kwargs)
        self.workdir = os.path.join(Config.get('workdir'), 'samsungtvws')
        if not token_file:
            token_file = os.path.join(self.workdir, 'token.txt')

        self.host = host
        self.port = port
        self.timeout = timeout
        self.name = name
        self.token_file = token_file
        self._connections: Dict[Tuple[host, port], SamsungTVWS] = {}
        os.makedirs(self.workdir, mode=0o700, exist_ok=True)

    def _get_host_and_port(
        self, host: Optional[str] = None, port: Optional[int] = None
    ) -> Tuple[str, int]:
        host = host or self.host
        port = port or self.port
        if not (host and port):
            raise AssertionError('No host/port specified')
        return host, port

    def connect(
        self, host: Optional[str] = None, port: Optional[int] = None
    ) -> SamsungTVWS:
        host, port = self._get_host_and_port(host, port)
        if (host, port) not in self._connections:
            self.logger.info('Connecting to %s:%s', host, port)
            self._connections[(host, port)] = SamsungTVWS(
                host=host,
                port=port,
                token_file=self.token_file,
                timeout=self.timeout,
                name=self.name,
            )

        return self._connections[(host, port)]

    def disconnect(
        self, host: Optional[str] = None, port: Optional[int] = None
    ) -> None:
        host, port = self._get_host_and_port(host, port)
        if (host, port) in self._connections:
            self.logger.info('Disconnecting from %s:%s', host, port)
            try:
                self._connections[(host, port)].close()
            except Exception as e:
                self.logger.warning('Connection close error: %s: %s', type(e), str(e))
            finally:
                self._connections.pop((host, port), None)

    def exec(
        self,
        func: Callable[[SamsungTVWS], Any],
        host: Optional[str] = None,
        port: Optional[int] = None,
        n_tries=2,
    ) -> Any:
        tv = self.connect(host, port)

        try:
            return func(tv)
        except Exception as e:
            self.disconnect(host, port)
            self.logger.warning(str(e))
            if n_tries <= 0:
                raise e
            else:
                time.sleep(1)
                return self.exec(func, host, port, n_tries - 1)

    @action
    def power(self, host: Optional[str] = None, port: Optional[int] = None) -> None:
        """
        Send power on/off control to the device.

        :param host: Default host IP/name override.
        :param port: Default port override.
        """
        return self.exec(lambda tv: tv.shortcuts().power(), host=host, port=port)

    @action
    def volume_up(self, host: Optional[str] = None, port: Optional[int] = None) -> None:
        """
        Send volume up control to the device.

        :param host: Default host IP/name override.
        :param port: Default port override.
        """
        return self.exec(lambda tv: tv.shortcuts().volume_up(), host=host, port=port)

    @action
    def volume_down(
        self, host: Optional[str] = None, port: Optional[int] = None
    ) -> None:
        """
        Send volume down control to the device.

        :param host: Default host IP/name override.
        :param port: Default port override.
        """
        return self.exec(lambda tv: tv.shortcuts().volume_down(), host=host, port=port)

    @action
    def back(self, host: Optional[str] = None, port: Optional[int] = None) -> None:
        """
        Send back key to the device.

        :param host: Default host IP/name override.
        :param port: Default port override.
        """
        return self.exec(lambda tv: tv.shortcuts().back(), host=host, port=port)

    @action
    def channel(
        self, channel: int, host: Optional[str] = None, port: Optional[int] = None
    ) -> None:
        """
        Change to the selected channel.

        :param channel: Channel index.
        :param host: Default host IP/name override.
        :param port: Default port override.
        """
        return self.exec(
            lambda tv: tv.shortcuts().channel(channel), host=host, port=port
        )

    @action
    def channel_up(
        self, host: Optional[str] = None, port: Optional[int] = None
    ) -> None:
        """
        Send channel_up key to the device.

        :param host: Default host IP/name override.
        :param port: Default port override.
        """
        return self.exec(lambda tv: tv.shortcuts().channel_up(), host=host, port=port)

    @action
    def channel_down(
        self, host: Optional[str] = None, port: Optional[int] = None
    ) -> None:
        """
        Send channel_down key to the device.

        :param host: Default host IP/name override.
        :param port: Default port override.
        """
        return self.exec(lambda tv: tv.shortcuts().channel_down(), host=host, port=port)

    @action
    def enter(self, host: Optional[str] = None, port: Optional[int] = None) -> None:
        """
        Send enter key to the device.

        :param host: Default host IP/name override.
        :param port: Default port override.
        """
        return self.exec(lambda tv: tv.shortcuts().enter(), host=host, port=port)

    @action
    def guide(self, host: Optional[str] = None, port: Optional[int] = None) -> None:
        """
        Send guide key to the device.

        :param host: Default host IP/name override.
        :param port: Default port override.
        """
        return self.exec(lambda tv: tv.shortcuts().guide(), host=host, port=port)

    @action
    def home(self, host: Optional[str] = None, port: Optional[int] = None) -> None:
        """
        Send home key to the device.

        :param host: Default host IP/name override.
        :param port: Default port override.
        """
        return self.exec(lambda tv: tv.shortcuts().home(), host=host, port=port)

    @action
    def info(self, host: Optional[str] = None, port: Optional[int] = None) -> None:
        """
        Send info key to the device.

        :param host: Default host IP/name override.
        :param port: Default port override.
        """
        return self.exec(lambda tv: tv.shortcuts().info(), host=host, port=port)

    @action
    def menu(self, host: Optional[str] = None, port: Optional[int] = None) -> None:
        """
        Send menu key to the device.

        :param host: Default host IP/name override.
        :param port: Default port override.
        """
        return self.exec(lambda tv: tv.shortcuts().menu(), host=host, port=port)

    @action
    def mute(self, host: Optional[str] = None, port: Optional[int] = None) -> None:
        """
        Send mute key to the device.

        :param host: Default host IP/name override.
        :param port: Default port override.
        """
        return self.exec(lambda tv: tv.shortcuts().mute(), host=host, port=port)

    @action
    def source(self, host: Optional[str] = None, port: Optional[int] = None) -> None:
        """
        Send source key to the device.

        :param host: Default host IP/name override.
        :param port: Default port override.
        """
        return self.exec(lambda tv: tv.shortcuts().source(), host=host, port=port)

    @action
    def tools(self, host: Optional[str] = None, port: Optional[int] = None) -> None:
        """
        Send tools key to the device.

        :param host: Default host IP/name override.
        :param port: Default port override.
        """
        return self.exec(lambda tv: tv.shortcuts().tools(), host=host, port=port)

    @action
    def up(self, host: Optional[str] = None, port: Optional[int] = None) -> None:
        """
        Send up key to the device.

        :param host: Default host IP/name override.
        :param port: Default port override.
        """
        return self.exec(lambda tv: tv.shortcuts().up(), host=host, port=port)

    @action
    def down(self, host: Optional[str] = None, port: Optional[int] = None) -> None:
        """
        Send down key to the device.

        :param host: Default host IP/name override.
        :param port: Default port override.
        """
        return self.exec(lambda tv: tv.shortcuts().down(), host=host, port=port)

    @action
    def left(self, host: Optional[str] = None, port: Optional[int] = None) -> None:
        """
        Send left key to the device.

        :param host: Default host IP/name override.
        :param port: Default port override.
        """
        return self.exec(lambda tv: tv.shortcuts().left(), host=host, port=port)

    @action
    def right(self, host: Optional[str] = None, port: Optional[int] = None) -> None:
        """
        Send right key to the device.

        :param host: Default host IP/name override.
        :param port: Default port override.
        """
        return self.exec(lambda tv: tv.shortcuts().right(), host=host, port=port)

    @action
    def blue(self, host: Optional[str] = None, port: Optional[int] = None) -> None:
        """
        Send blue key to the device.

        :param host: Default host IP/name override.
        :param port: Default port override.
        """
        return self.exec(lambda tv: tv.shortcuts().blue(), host=host, port=port)

    @action
    def green(self, host: Optional[str] = None, port: Optional[int] = None) -> None:
        """
        Send green key to the device.

        :param host: Default host IP/name override.
        :param port: Default port override.
        """
        return self.exec(lambda tv: tv.shortcuts().green(), host=host, port=port)

    @action
    def red(self, host: Optional[str] = None, port: Optional[int] = None) -> None:
        """
        Send red key to the device.

        :param host: Default host IP/name override.
        :param port: Default port override.
        """
        return self.exec(lambda tv: tv.shortcuts().red(), host=host, port=port)

    @action
    def yellow(self, host: Optional[str] = None, port: Optional[int] = None) -> None:
        """
        Send red key to the device.

        :param host: Default host IP/name override.
        :param port: Default port override.
        """
        return self.exec(lambda tv: tv.shortcuts().yellow(), host=host, port=port)

    @action
    def digit(
        self, digit: int, host: Optional[str] = None, port: Optional[int] = None
    ) -> None:
        """
        Send a digit key to the device.

        :param digit: Digit to send.
        :param host: Default host IP/name override.
        :param port: Default port override.
        """
        return self.exec(lambda tv: tv.shortcuts().digit(digit), host=host, port=port)

    @action
    def run_app(
        self,
        app_id: Union[int, str],
        host: Optional[str] = None,
        port: Optional[int] = None,
    ) -> None:
        """
        Run an app by ID.

        :param app_id: App ID.
        :param host: Default host IP/name override.
        :param port: Default port override.
        """
        tv = self.connect(host, port)
        tv.rest_app_run(str(app_id))

    @action
    def close_app(
        self,
        app_id: Union[int, str],
        host: Optional[str] = None,
        port: Optional[int] = None,
    ) -> None:
        """
        Close an app.

        :param app_id: App ID.
        :param host: Default host IP/name override.
        :param port: Default port override.
        """
        tv = self.connect(host, port)
        tv.rest_app_close(str(app_id))

    @action
    def install_app(
        self,
        app_id: Union[int, str],
        host: Optional[str] = None,
        port: Optional[int] = None,
    ) -> None:
        """
        Install an app.

        :param app_id: App ID.
        :param host: Default host IP/name override.
        :param port: Default port override.
        """
        tv = self.connect(host, port)
        tv.rest_app_install(str(app_id))

    @action
    def status_app(
        self,
        app_id: Union[int, str],
        host: Optional[str] = None,
        port: Optional[int] = None,
    ) -> dict:
        """
        Get the status of an app.

        :param app_id: App ID.
        :param host: Default host IP/name override.
        :param port: Default port override.
        """
        tv = self.connect(host, port)
        return tv.rest_app_status(str(app_id))

    @action
    def list_apps(self, host: Optional[str] = None, port: Optional[int] = None) -> list:
        """
        Get the list of installed apps.

        :param host: Default host IP/name override.
        :param port: Default port override.
        """
        return self.exec(lambda tv: tv.app_list(), host=host, port=port)

    @action
    def open_browser(
        self, url: str, host: Optional[str] = None, port: Optional[int] = None
    ) -> None:
        """
        Open a URL in the browser.

        :param url: URL to open.
        :param host: Default host IP/name override.
        :param port: Default port override.
        """
        return self.exec(lambda tv: tv.open_browser(url), host=host, port=port)

    @action
    def device_info(
        self, host: Optional[str] = None, port: Optional[int] = None
    ) -> dict:
        """
        Return the info of the device.

        :param host: Default host IP/name override.
        :param port: Default port override.
        """
        tv = self.connect(host, port)
        return tv.rest_device_info()


# vim:sw=4:ts=4:et:
